Skip to content

Commit

Permalink
Merge pull request #24 from MohamedRejeb/0.2.x
Browse files Browse the repository at this point in the history
0.2.x
  • Loading branch information
MohamedRejeb authored Sep 27, 2023
2 parents bc468f2 + a53ad6a commit 6c2b5dc
Show file tree
Hide file tree
Showing 27 changed files with 1,285 additions and 9 deletions.
2 changes: 1 addition & 1 deletion calf-file-picker/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.androidLibrary)
id("module.publication")
// id("module.publication")
}

kotlin {
Expand Down
2 changes: 1 addition & 1 deletion calf-io/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
// id("module.publication")
id("module.publication")
}

kotlin {
Expand Down
9 changes: 6 additions & 3 deletions calf-permissions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ kotlin {
jvm("desktop") {
jvmToolchain(11)
}
// js(IR) {
// browser()
// }
js(IR) {
browser()
}
iosX64()
iosArm64()
iosSimulatorArm64()
Expand All @@ -33,6 +33,9 @@ kotlin {
sourceSets.commonTest.get().dependencies {
implementation(libs.kotlin.test)
}
sourceSets.androidMain.get().dependencies {
implementation(libs.activity.compose)
}
}

android {
Expand Down
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()
}
}
}
}
}
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))
}
}
}
Loading

0 comments on commit 6c2b5dc

Please sign in to comment.