-
-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24 from MohamedRejeb/0.2.x
0.2.x
- Loading branch information
Showing
27 changed files
with
1,285 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
139 changes: 139 additions & 0 deletions
139
...dMain/kotlin/com/mohamedrejeb/calf/permissions/MutableMultiplePermissionsState.android.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package com.mohamedrejeb.calf.permissions | ||
|
||
import androidx.activity.compose.rememberLauncherForActivityResult | ||
import androidx.activity.result.ActivityResultLauncher | ||
import androidx.activity.result.contract.ActivityResultContracts | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.DisposableEffect | ||
import androidx.compose.runtime.Stable | ||
import androidx.compose.runtime.derivedStateOf | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.key | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.platform.LocalContext | ||
|
||
/** | ||
* Creates a [MultiplePermissionsState] that is remembered across compositions. | ||
* | ||
* It's recommended that apps exercise the permissions workflow as described in the | ||
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions). | ||
* | ||
* @param permissions the permissions to control and observe. | ||
* @param onPermissionsResult will be called with whether or not the user granted the permissions | ||
* after [MultiplePermissionsState.launchMultiplePermissionRequest] is called. | ||
*/ | ||
@ExperimentalPermissionsApi | ||
@Composable | ||
internal actual fun rememberMutableMultiplePermissionsState( | ||
permissions: List<Permission>, | ||
onPermissionsResult: (Map<Permission, Boolean>) -> Unit | ||
): MultiplePermissionsState { | ||
// Create mutable permissions that can be requested individually | ||
val mutablePermissions = rememberMutablePermissionsState(permissions) | ||
// Refresh permissions when the lifecycle is resumed. | ||
PermissionsLifecycleCheckerEffect(mutablePermissions) | ||
|
||
val multiplePermissionsState = remember(permissions) { | ||
MutableMultiplePermissionsState(mutablePermissions) | ||
} | ||
|
||
// Remember RequestMultiplePermissions launcher and assign it to multiplePermissionsState | ||
val launcher = rememberLauncherForActivityResult( | ||
ActivityResultContracts.RequestMultiplePermissions() | ||
) { permissionsResult -> | ||
val result = permissionsResult | ||
.mapKeys { getPermissionFromAndroidPermission(it.key) } | ||
.filter { it.key != null } | ||
.mapKeys { it.key!! } | ||
multiplePermissionsState.updatePermissionsStatus(result) | ||
onPermissionsResult(result) | ||
} | ||
DisposableEffect(multiplePermissionsState, launcher) { | ||
multiplePermissionsState.launcher = launcher | ||
onDispose { | ||
multiplePermissionsState.launcher = null | ||
} | ||
} | ||
|
||
return multiplePermissionsState | ||
} | ||
|
||
@ExperimentalPermissionsApi | ||
@Composable | ||
private fun rememberMutablePermissionsState( | ||
permissions: List<Permission> | ||
): List<MutablePermissionState> { | ||
// Create list of MutablePermissionState for each permission | ||
val context = LocalContext.current | ||
val activity = context.findActivity() | ||
val mutablePermissions: List<MutablePermissionState> = remember(permissions) { | ||
return@remember permissions.map { MutablePermissionState(it, context, activity) } | ||
} | ||
// Update each permission with its own launcher | ||
for (permissionState in mutablePermissions) { | ||
key(permissionState.permission) { | ||
// Remember launcher and assign it to the permissionState | ||
val launcher = rememberLauncherForActivityResult( | ||
ActivityResultContracts.RequestPermission() | ||
) { | ||
permissionState.refreshPermissionStatus() | ||
} | ||
DisposableEffect(launcher) { | ||
permissionState.launcher = launcher | ||
onDispose { | ||
permissionState.launcher = null | ||
} | ||
} | ||
} | ||
} | ||
|
||
return mutablePermissions | ||
} | ||
|
||
/** | ||
* A state object that can be hoisted to control and observe multiple permission status changes. | ||
* | ||
* In most cases, this will be created via [rememberMutableMultiplePermissionsState]. | ||
* | ||
* @param mutablePermissions list of mutable permissions to control and observe. | ||
*/ | ||
@ExperimentalPermissionsApi | ||
@Stable | ||
internal actual class MutableMultiplePermissionsState actual constructor( | ||
private val mutablePermissions: List<MutablePermissionState> | ||
) : MultiplePermissionsState { | ||
|
||
override val permissions: List<PermissionState> = mutablePermissions | ||
|
||
override val revokedPermissions: List<PermissionState> by derivedStateOf { | ||
permissions.filter { it.status != PermissionStatus.Granted } | ||
} | ||
|
||
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 | ||
} | ||
|
||
override val shouldShowRationale: Boolean by derivedStateOf { | ||
permissions.any { it.status.shouldShowRationale } | ||
} | ||
|
||
override fun launchMultiplePermissionRequest() { | ||
launcher?.launch( | ||
permissions.map { it.permission.toAndroidPermission() }.toTypedArray() | ||
) ?: throw IllegalStateException("ActivityResultLauncher cannot be null") | ||
} | ||
|
||
internal var launcher: ActivityResultLauncher<Array<String>>? = null | ||
|
||
internal actual fun updatePermissionsStatus(permissionsStatus: Map<Permission, Boolean>) { | ||
// Update all permissions with the result | ||
for (permission in permissionsStatus.keys) { | ||
mutablePermissions.firstOrNull { it.permission == permission }?.apply { | ||
permissionsStatus[permission]?.let { | ||
this.refreshPermissionStatus() | ||
} | ||
} | ||
} | ||
} | ||
} |
112 changes: 112 additions & 0 deletions
112
...rc/androidMain/kotlin/com/mohamedrejeb/calf/permissions/MutablePermissionState.android.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package com.mohamedrejeb.calf.permissions | ||
|
||
import android.app.Activity | ||
import android.content.Context | ||
import android.os.Build | ||
import androidx.activity.compose.rememberLauncherForActivityResult | ||
import androidx.activity.result.ActivityResultLauncher | ||
import androidx.activity.result.contract.ActivityResultContracts | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.DisposableEffect | ||
import androidx.compose.runtime.Stable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.platform.LocalContext | ||
|
||
/** | ||
* Creates a [MutablePermissionState] that is remembered across compositions. | ||
* | ||
* It's recommended that apps exercise the permissions workflow as described in the | ||
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions). | ||
* | ||
* @param permission the permission to control and observe. | ||
* @param onPermissionResult will be called with whether or not the user granted the permission | ||
* after [PermissionState.launchPermissionRequest] is called. | ||
*/ | ||
@ExperimentalPermissionsApi | ||
@Composable | ||
internal actual fun rememberMutablePermissionState( | ||
permission: Permission, | ||
onPermissionResult: (Boolean) -> Unit | ||
): MutablePermissionState { | ||
val context = LocalContext.current | ||
val permissionState = remember(permission) { | ||
MutablePermissionState(permission, context, context.findActivity()) | ||
} | ||
|
||
// Refresh the permission status when the lifecycle is resumed | ||
PermissionLifecycleCheckerEffect(permissionState) | ||
|
||
// Remember RequestPermission launcher and assign it to permissionState | ||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { | ||
permissionState.refreshPermissionStatus() | ||
onPermissionResult(it) | ||
} | ||
DisposableEffect(permissionState, launcher) { | ||
permissionState.launcher = launcher | ||
onDispose { | ||
permissionState.launcher = null | ||
} | ||
} | ||
|
||
return permissionState | ||
} | ||
|
||
/** | ||
* A mutable state object that can be used to control and observe permission status changes. | ||
* | ||
* In most cases, this will be created via [rememberMutablePermissionState]. | ||
* | ||
* @param permission the permission to control and observe. | ||
* @param context to check the status of the [permission]. | ||
* @param activity to check if the user should be presented with a rationale for [permission]. | ||
*/ | ||
@ExperimentalPermissionsApi | ||
@Stable | ||
internal actual class MutablePermissionState( | ||
override val permission: Permission, | ||
private val context: Context?, | ||
private val activity: Activity? | ||
) : PermissionState { | ||
|
||
actual constructor( | ||
permission: Permission, | ||
) : this( | ||
permission, | ||
null, | ||
null | ||
) | ||
|
||
private val androidPermission = permission.toAndroidPermission() | ||
|
||
override var status: PermissionStatus by mutableStateOf(getPermissionStatus()) | ||
|
||
override fun launchPermissionRequest() { | ||
if (androidPermission.isEmpty()) return | ||
|
||
launcher?.launch( | ||
androidPermission | ||
) ?: throw IllegalStateException("ActivityResultLauncher cannot be null") | ||
} | ||
|
||
internal var launcher: ActivityResultLauncher<String>? = null | ||
|
||
internal actual fun refreshPermissionStatus() { | ||
status = getPermissionStatus() | ||
} | ||
|
||
private fun getPermissionStatus(): PermissionStatus { | ||
if (context == null || activity == null || androidPermission.isEmpty()) { | ||
return PermissionStatus.Denied(false) | ||
} | ||
|
||
val hasPermission = context.checkPermission(androidPermission) | ||
return if (hasPermission) { | ||
PermissionStatus.Granted | ||
} else { | ||
PermissionStatus.Denied(activity.shouldShowRationale(androidPermission)) | ||
} | ||
} | ||
} |
Oops, something went wrong.