diff --git a/README.md b/README.md index d34b422..2e39626 100755 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ override fun onCreate(savedInstanceState: Bundle?) { } // Binds the permissions controller to the activity lifecycle. - viewModel.permissionsController.bind(lifecycle, supportFragmentManager) + viewModel.permissionsController.bind(activity) } ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0949bb5..fff4b40 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ kotlinVersion = "1.9.10" androidAppCompatVersion = "1.6.1" composeMaterialVersion = "1.4.1" composeActivityVersion = "1.7.0" +activityVersion = "1.7.0" materialDesignVersion = "1.8.0" androidLifecycleVersion = "2.2.0" androidCoreTestingVersion = "2.2.0" @@ -11,13 +12,15 @@ mokoMvvmVersion = "0.16.0" mokoPermissionsVersion = "0.17.0" composeJetBrainsVersion = "1.5.1" lifecycleRuntime = "2.6.1" -activityKtxVersion = "1.7.2" +composeUiVersion = "1.0.1" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } material = { module = "com.google.android.material:material", version.ref = "materialDesignVersion" } composeMaterial = { module = "androidx.compose.material:material", version.ref = "composeMaterialVersion" } composeActivity = { module = "androidx.activity:activity-compose", version.ref = "composeActivityVersion" } +activity = { module = "androidx.activity:activity", version.ref = "activityVersion" } +composeUi = { module = "androidx.compose.ui:ui", version.ref = "composeUiVersion" } lifecycle = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "androidLifecycleVersion" } lifecycleRuntime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntime" } coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } @@ -34,4 +37,3 @@ mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", kotlinSerializationGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinVersion" } composeJetBrainsGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeJetBrainsVersion" } detektGradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.22.0" } -activityKtx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtxVersion" } diff --git a/permissions-compose/build.gradle.kts b/permissions-compose/build.gradle.kts index c0f1b62..381ca67 100644 --- a/permissions-compose/build.gradle.kts +++ b/permissions-compose/build.gradle.kts @@ -21,6 +21,7 @@ android { dependencies { commonMainApi(projects.permissions) commonMainApi(compose.runtime) - - androidMainImplementation(libs.composeActivity) + androidMainImplementation(libs.activity) + androidMainImplementation(libs.composeUi) + androidMainImplementation(libs.lifecycleRuntime) } diff --git a/permissions/build.gradle.kts b/permissions/build.gradle.kts index 273d798..980ab40 100644 --- a/permissions/build.gradle.kts +++ b/permissions/build.gradle.kts @@ -15,6 +15,6 @@ android { dependencies { commonMainImplementation(libs.coroutines) - androidMainImplementation(libs.activityKtx) + androidMainImplementation(libs.activity) androidMainImplementation(libs.lifecycleRuntime) -} \ No newline at end of file +} diff --git a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt b/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt index c76c904..326deb0 100755 --- a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt +++ b/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt @@ -7,7 +7,6 @@ package dev.icerock.moko.permissions import android.Manifest import android.app.Activity import android.content.Context -import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -24,8 +23,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull import java.util.UUID import kotlin.coroutines.suspendCoroutine @@ -37,17 +39,17 @@ class PermissionsControllerImpl( private val mutex: Mutex = Mutex() - private var launcher: ActivityResultLauncher>? = null + private val launcherHolder = MutableStateFlow>?>(null) private var permissionCallback: PermissionCallback? = null + private val key = UUID.randomUUID().toString() + override fun bind(activity: ComponentActivity) { this.activityHolder.value = activity val activityResultRegistryOwner = activity as ActivityResultRegistryOwner - val key = UUID.randomUUID().toString() - - launcher = activityResultRegistryOwner.activityResultRegistry.register( + val launcher = activityResultRegistryOwner.activityResultRegistry.register( key, ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> @@ -67,7 +69,7 @@ class PermissionsControllerImpl( if (success) { permissionCallback.callback.invoke(Result.success(Unit)) } else { - if (shouldShowRequestPermissionRationale(permissions.keys.first())) { + if (shouldShowRequestPermissionRationale(activity, permissions.keys.first())) { permissionCallback.callback.invoke( Result.failure(DeniedException(permissionCallback.permission)) ) @@ -79,10 +81,13 @@ class PermissionsControllerImpl( } } + launcherHolder.value = launcher + val observer = object : LifecycleEventObserver { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { if (event == Lifecycle.Event.ON_DESTROY) { this@PermissionsControllerImpl.activityHolder.value = null + this@PermissionsControllerImpl.launcherHolder.value = null source.lifecycle.removeObserver(this) } } @@ -92,9 +97,11 @@ class PermissionsControllerImpl( override suspend fun providePermission(permission: Permission) { mutex.withLock { + val launcher = awaitActivityResultLauncher() val platformPermission = permission.toPlatformPermission() suspendCoroutine { continuation -> requestPermission( + launcher, permission, platformPermission ) { continuation.resumeWith(it) } @@ -103,12 +110,43 @@ class PermissionsControllerImpl( } private fun requestPermission( + launcher: ActivityResultLauncher>, permission: Permission, permissions: List, callback: (Result) -> Unit ) { permissionCallback = PermissionCallback(permission, callback) - launcher?.launch(permissions.toTypedArray()) + launcher.launch(permissions.toTypedArray()) + } + + private suspend fun awaitActivityResultLauncher(): ActivityResultLauncher> { + val activityResultLauncher = launcherHolder.value + if (activityResultLauncher != null) return activityResultLauncher + + return withTimeoutOrNull(AWAIT_ACTIVITY_TIMEOUT_DURATION_MS) { + launcherHolder.filterNotNull().first() + } ?: error( + "activityResultLauncher is null, `bind` function was never called," + + " consider calling permissionsController.bind(activity)" + + " or BindEffect(permissionsController) in the composable function," + + " check the documentation for more info: " + + "https://github.com/icerockdev/moko-permissions/blob/master/README.md" + ) + } + + private suspend fun awaitActivity(): Activity { + val activity = activityHolder.value + if (activity != null) return activity + + return withTimeoutOrNull(AWAIT_ACTIVITY_TIMEOUT_DURATION_MS) { + activityHolder.filterNotNull().first() + } ?: error( + "activity is null, `bind` function was never called," + + " consider calling permissionsController.bind(activity)" + + " or BindEffect(permissionsController) in the composable function," + + " check the documentation for more info: " + + "https://github.com/icerockdev/moko-permissions/blob/master/README.md" + ) } override suspend fun isPermissionGranted(permission: Permission): Boolean { @@ -142,14 +180,12 @@ class PermissionsControllerImpl( else PermissionState.NotGranted } - private fun shouldShowRequestPermissionRationale(permission: String): Boolean { - val activity: Activity = checkNotNull(this.activityHolder.value) { - "${this.activityHolder.value} activity is null, `bind` function was never called," + - " consider calling permissionsController.bind(activity)" + - " or BindEffect(permissionsController) in the composable function," + - " check the documentation for more info: " + - "https://github.com/icerockdev/moko-permissions/blob/master/README.md" - } + private suspend fun shouldShowRequestPermissionRationale(permission: String): Boolean { + val activity = awaitActivity() + return shouldShowRequestPermissionRationale(activity, permission) + } + + private fun shouldShowRequestPermissionRationale(activity: Activity, permission: String): Boolean { return ActivityCompat.shouldShowRequestPermissionRationale( activity, permission @@ -303,6 +339,7 @@ class PermissionsControllerImpl( private companion object { val VERSIONS_WITHOUT_NOTIFICATION_PERMISSION = Build.VERSION_CODES.KITKAT until Build.VERSION_CODES.TIRAMISU + private const val AWAIT_ACTIVITY_TIMEOUT_DURATION_MS = 2000L } }