Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Permissions] Permissions enhancements #1793

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ import java.util.EnumSet
public class PermissionsLaunchDetector : Detector(), SourceCodeScanner {

override fun getApplicableMethodNames(): List<String> = listOf(
LaunchPermissionRequest.shortName, LaunchMultiplePermissionsRequest.shortName
LaunchPermissionRequest.shortName, LaunchMultiplePermissionsRequest.shortName,
LaunchPermissionRequestOrAppSettings.shortName,
LaunchMultiplePermissionsRequestOrAppSettings.shortName,
OpenAppSettings.shortName
)

override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
Expand Down Expand Up @@ -87,5 +90,11 @@ private fun PsiMethod.isInPackageName(packageName: PackageName): Boolean =
private val PermissionsPackageName = Package("com.google.accompanist.permissions")
private val LaunchPermissionRequest =
Name(PermissionsPackageName, "launchPermissionRequest")
private val LaunchPermissionRequestOrAppSettings =
Name(PermissionsPackageName, "launchPermissionRequestOrAppSettings")
private val LaunchMultiplePermissionsRequest =
Name(PermissionsPackageName, "launchMultiplePermissionRequest")
private val LaunchMultiplePermissionsRequestOrAppSettings =
Name(PermissionsPackageName, "launchMultiplePermissionRequestOrAppSettings")
private val OpenAppSettings =
Name(PermissionsPackageName, "openAppSettings")
46 changes: 36 additions & 10 deletions permissions/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,31 @@ package com.google.accompanist.permissions {
}

@androidx.compose.runtime.Stable @com.google.accompanist.permissions.ExperimentalPermissionsApi public interface MultiplePermissionsState {
method public boolean getAllNotGrantedPermissionsArePermanentlyDenied();
method public boolean getAllPermissionsGranted();
method public java.util.List<com.google.accompanist.permissions.PermissionState> getPermissions();
method public java.util.List<com.google.accompanist.permissions.PermissionState> getRevokedPermissions();
method public boolean getShouldShowRationale();
method public boolean isNotRequested();
method public void launchMultiplePermissionRequest();
method public void openAppSettings();
property public abstract boolean allNotGrantedPermissionsArePermanentlyDenied;
property public abstract boolean allPermissionsGranted;
property public abstract boolean isNotRequested;
property public abstract java.util.List<com.google.accompanist.permissions.PermissionState> permissions;
property public abstract java.util.List<com.google.accompanist.permissions.PermissionState> revokedPermissions;
property public abstract boolean shouldShowRationale;
}

public final class MultiplePermissionsStateKt {
method public static inline java.util.List<com.google.accompanist.permissions.PermissionState> getDeniedPermissions(com.google.accompanist.permissions.MultiplePermissionsState);
method public static inline java.util.List<com.google.accompanist.permissions.PermissionState> getGrantedPermissions(com.google.accompanist.permissions.MultiplePermissionsState);
method public static inline java.util.List<com.google.accompanist.permissions.PermissionState> getNotGrantedPermissions(com.google.accompanist.permissions.MultiplePermissionsState);
method public static inline java.util.List<com.google.accompanist.permissions.PermissionState> getPermanentlyDeniedPermissions(com.google.accompanist.permissions.MultiplePermissionsState);
method @com.google.accompanist.permissions.ExperimentalPermissionsApi public static boolean isDenied(com.google.accompanist.permissions.MultiplePermissionsState, String permission);
method @com.google.accompanist.permissions.ExperimentalPermissionsApi public static boolean isGranted(com.google.accompanist.permissions.MultiplePermissionsState, String permission);
method @com.google.accompanist.permissions.ExperimentalPermissionsApi public static boolean isNotGranted(com.google.accompanist.permissions.MultiplePermissionsState, String permission);
method @com.google.accompanist.permissions.ExperimentalPermissionsApi public static boolean isPermanentlyDenied(com.google.accompanist.permissions.MultiplePermissionsState, String permission);
method @com.google.accompanist.permissions.ExperimentalPermissionsApi public static void launchMultiplePermissionRequestOrAppSettings(com.google.accompanist.permissions.MultiplePermissionsState);
method @androidx.compose.runtime.Composable public static com.google.accompanist.permissions.MultiplePermissionsState rememberMultiplePermissionsState(String permission, String![] otherPermissions, optional kotlin.jvm.functions.Function1<? super java.util.Map<java.lang.String,java.lang.Boolean>,kotlin.Unit> onPermissionsResult);
method @androidx.compose.runtime.Composable @com.google.accompanist.permissions.ExperimentalPermissionsApi public static com.google.accompanist.permissions.MultiplePermissionsState rememberMultiplePermissionsState(java.util.List<java.lang.String> permissions, optional kotlin.jvm.functions.Function1<? super java.util.Map<java.lang.String,java.lang.Boolean>,kotlin.Unit> onPermissionsResult);
}

Expand All @@ -30,32 +43,45 @@ package com.google.accompanist.permissions {
method public String getPermission();
method public com.google.accompanist.permissions.PermissionStatus getStatus();
method public void launchPermissionRequest();
method public void openAppSettings();
property public abstract String permission;
property public abstract com.google.accompanist.permissions.PermissionStatus status;
}

public final class PermissionStateKt {
method @com.google.accompanist.permissions.ExperimentalPermissionsApi public static void launchPermissionRequestOrAppSettings(com.google.accompanist.permissions.PermissionState);
method @androidx.compose.runtime.Composable @com.google.accompanist.permissions.ExperimentalPermissionsApi public static com.google.accompanist.permissions.PermissionState rememberPermissionState(String permission, optional kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onPermissionResult);
}

@androidx.compose.runtime.Stable @com.google.accompanist.permissions.ExperimentalPermissionsApi public sealed interface PermissionStatus {
}

public static final class PermissionStatus.Denied implements com.google.accompanist.permissions.PermissionStatus {
ctor public PermissionStatus.Denied(boolean shouldShowRationale);
method public boolean component1();
method public com.google.accompanist.permissions.PermissionStatus.Denied copy(boolean shouldShowRationale);
method public boolean getShouldShowRationale();
property public final boolean shouldShowRationale;
}

public static final class PermissionStatus.Granted implements com.google.accompanist.permissions.PermissionStatus {
field public static final com.google.accompanist.permissions.PermissionStatus.Granted INSTANCE;
}

public static sealed interface PermissionStatus.NotGranted extends com.google.accompanist.permissions.PermissionStatus {
}

public static final class PermissionStatus.NotGranted.Denied implements com.google.accompanist.permissions.PermissionStatus.NotGranted {
field public static final com.google.accompanist.permissions.PermissionStatus.NotGranted.Denied INSTANCE;
}

public static final class PermissionStatus.NotGranted.NotRequested implements com.google.accompanist.permissions.PermissionStatus.NotGranted {
field public static final com.google.accompanist.permissions.PermissionStatus.NotGranted.NotRequested INSTANCE;
}

public static final class PermissionStatus.NotGranted.PermanentlyDenied implements com.google.accompanist.permissions.PermissionStatus.NotGranted {
field public static final com.google.accompanist.permissions.PermissionStatus.NotGranted.PermanentlyDenied INSTANCE;
}

public final class PermissionsUtilKt {
method public static boolean getShouldShowRationale(com.google.accompanist.permissions.PermissionStatus);
method public static boolean isDenied(com.google.accompanist.permissions.PermissionStatus);
method public static boolean isGranted(com.google.accompanist.permissions.PermissionStatus);
method public static boolean isNotGranted(com.google.accompanist.permissions.PermissionStatus);
method public static boolean isNotRequested(com.google.accompanist.permissions.PermissionStatus);
method public static boolean isPermanentlyDenied(com.google.accompanist.permissions.PermissionStatus);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,29 @@
package com.google.accompanist.permissions

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.util.fastMap

/**
* Creates a [MultiplePermissionsState] that is remembered across compositions.
*
* @param permission a permission to control and observe.
* @param otherPermissions additional permissions to control and observe.
* @param onPermissionsResult will be called with whether or not the user granted the permissions
* after [MultiplePermissionsState.launchMultiplePermissionRequest] is called.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Composable
public fun rememberMultiplePermissionsState(
permission: String,
vararg otherPermissions: String,
onPermissionsResult: (Map<String, Boolean>) -> Unit = {}
): MultiplePermissionsState = rememberMultiplePermissionsState(
permissions = listOf(permission) + otherPermissions.toList(),
onPermissionsResult = onPermissionsResult,
)

/**
* Creates a [MultiplePermissionsState] that is remembered across compositions.
Expand All @@ -35,7 +57,10 @@ public fun rememberMultiplePermissionsState(
permissions: List<String>,
onPermissionsResult: (Map<String, Boolean>) -> Unit = {}
): MultiplePermissionsState {
return rememberMutableMultiplePermissionsState(permissions, onPermissionsResult)
return when {
LocalInspectionMode.current -> PreviewMultiplePermissionsState(permissions)
else -> rememberMutableMultiplePermissionsState(permissions, onPermissionsResult)
}
}

/**
Expand All @@ -56,15 +81,20 @@ public interface MultiplePermissionsState {
public val permissions: List<PermissionState>

/**
* List of permissions revoked by the user.
* When `true`, the user hasn't requested [permissions] yet.
*/
public val revokedPermissions: List<PermissionState>
public val isNotRequested: Boolean

/**
* When `true`, the user has granted all [permissions].
*/
public val allPermissionsGranted: Boolean

/**
* When `true`, the user has permanently denied all [permissions] that haven't been granted.
*/
public val allNotGrantedPermissionsArePermanentlyDenied: Boolean

/**
* When `true`, the user should be presented with a rationale.
*/
Expand All @@ -82,4 +112,118 @@ public interface MultiplePermissionsState {
* This behavior varies depending on the Android level API.
*/
public fun launchMultiplePermissionRequest(): Unit

/**
* Open the app settings page.
*
* If the first request permission in [permissions] is [android.Manifest.permission.POST_NOTIFICATIONS] then
* the notification settings will be opened. Otherwise the app's settings will be opened.
*
* This should always be triggered from non-composable scope, for example, from a side-effect
* or a non-composable callback. Otherwise, this will result in an IllegalStateException.
*/
public fun openAppSettings(): Unit
}

/**
* Calls [MultiplePermissionsState.openAppSettings] when
* [MultiplePermissionsState.allNotGrantedPermissionsArePermanentlyDenied] is `true`; otherwise calls
* [MultiplePermissionsState.launchMultiplePermissionRequest].
*
* This should always be triggered from non-composable scope, for example, from a side-effect
* or a non-composable callback. Otherwise, this will result in an IllegalStateException.
*/
@ExperimentalPermissionsApi
public fun MultiplePermissionsState.launchMultiplePermissionRequestOrAppSettings() {
when {
allNotGrantedPermissionsArePermanentlyDenied -> openAppSettings()
else -> launchMultiplePermissionRequest()
}
}

/**
* List of permissions granted by the user.
*/
@ExperimentalPermissionsApi
public inline val MultiplePermissionsState.grantedPermissions: List<PermissionState>
get() = permissions.filter { it.status.isGranted }

/**
* List of permissions not granted by the user.
*/
@ExperimentalPermissionsApi
public inline val MultiplePermissionsState.notGrantedPermissions: List<PermissionState>
get() = permissions.filter { it.status.isNotGranted }

/**
* List of permissions denied by the user.
*/
@ExperimentalPermissionsApi
public inline val MultiplePermissionsState.deniedPermissions: List<PermissionState>
get() = permissions.filter { it.status.isDenied }

/**
* List of permissions permanently denied by the user.
*/
@ExperimentalPermissionsApi
public inline val MultiplePermissionsState.permanentlyDeniedPermissions: List<PermissionState>
get() = permissions.filter { it.status.isPermanentlyDenied }

/**
* Returns `true` if [permission] was granted, otherwise `false`.
*
* If [permission] wasn't requested a [IllegalArgumentException] will be thrown.
*/
@ExperimentalPermissionsApi
public fun MultiplePermissionsState.isGranted(permission: String): Boolean =
requireNotNull(permissions.find { it.permission == permission }) {
"$permission is not present in the list of requested permissions"
}.status.isGranted

/**
* Returns `true` if [permission] was not granted, otherwise `false`.
*
* If [permission] wasn't requested a [IllegalArgumentException] will be thrown.
*/
@ExperimentalPermissionsApi
public fun MultiplePermissionsState.isNotGranted(permission: String): Boolean =
requireNotNull(permissions.find { it.permission == permission }) {
"$permission is not present in the list of requested permissions"
}.status.isNotGranted

/**
* Returns `true` if [permission] was denied, otherwise `false`.
*
* If [permission] wasn't requested a [IllegalArgumentException] will be thrown.
*/
@ExperimentalPermissionsApi
public fun MultiplePermissionsState.isDenied(permission: String): Boolean =
requireNotNull(permissions.find { it.permission == permission }) {
"$permission is not present in the list of requested permissions"
}.status.isDenied

/**
* Returns `true` if [permission] was permanently denied, otherwise `false`.
*
* If [permission] wasn't requested a [IllegalArgumentException] will be thrown.
*/
@ExperimentalPermissionsApi
public fun MultiplePermissionsState.isPermanentlyDenied(permission: String): Boolean =
requireNotNull(permissions.find { it.permission == permission }) {
"$permission is not present in the list of requested permissions"
}.status.isPermanentlyDenied

@OptIn(ExperimentalPermissionsApi::class)
@Immutable
private class PreviewMultiplePermissionsState(
permissions: List<String>
) : MultiplePermissionsState {
override val permissions: List<PermissionState> = permissions.fastMap(::PreviewPermissionState)
override val isNotRequested: Boolean = true
override val allPermissionsGranted: Boolean = false
override val allNotGrantedPermissionsArePermanentlyDenied: Boolean = false
override val shouldShowRationale: Boolean = false

override fun launchMultiplePermissionRequest() {}
override fun openAppSettings() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.util.fastAll

/**
* Creates a [MultiplePermissionsState] that is remembered across compositions.
Expand Down Expand Up @@ -57,6 +58,7 @@ internal fun rememberMutableMultiplePermissionsState(
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissionsResult ->
multiplePermissionsState.isNotRequested = false
multiplePermissionsState.updatePermissionsStatus(permissionsResult)
onPermissionsResult(permissionsResult)
}
Expand Down Expand Up @@ -117,15 +119,16 @@ internal class MutableMultiplePermissionsState(

override val permissions: List<PermissionState> = mutablePermissions

override val revokedPermissions: List<PermissionState> by derivedStateOf {
permissions.filter { it.status != PermissionStatus.Granted }
}
override var isNotRequested: Boolean = true

override val allPermissionsGranted: Boolean by derivedStateOf {
permissions.all { it.status.isGranted } || // Up to date when the lifecycle is resumed
revokedPermissions.isEmpty() // Up to date when the user launches the action
notGrantedPermissions.isEmpty() // Up to date when the user launches the action
}

override val allNotGrantedPermissionsArePermanentlyDenied: Boolean
get() = permissions.fastAll { it.status.isGranted || it.status.isPermanentlyDenied }

override val shouldShowRationale: Boolean by derivedStateOf {
permissions.any { it.status.shouldShowRationale }
}
Expand All @@ -136,6 +139,10 @@ internal class MutableMultiplePermissionsState(
) ?: throw IllegalStateException("ActivityResultLauncher cannot be null")
}

override fun openAppSettings() {
permanentlyDeniedPermissions.firstOrNull()?.openAppSettings()
}

internal var launcher: ActivityResultLauncher<Array<String>>? = null

internal fun updatePermissionsStatus(permissionsStatus: Map<String, Boolean>) {
Expand Down
Loading