diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e200170..0949bb5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ mokoMvvmVersion = "0.16.0" mokoPermissionsVersion = "0.17.0" composeJetBrainsVersion = "1.5.1" lifecycleRuntime = "2.6.1" +activityKtxVersion = "1.7.2" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } @@ -33,3 +34,4 @@ 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 980e567..c0f1b62 100644 --- a/permissions-compose/build.gradle.kts +++ b/permissions-compose/build.gradle.kts @@ -22,6 +22,5 @@ dependencies { commonMainApi(projects.permissions) commonMainApi(compose.runtime) - androidMainImplementation(libs.appCompat) androidMainImplementation(libs.composeActivity) } diff --git a/permissions-compose/src/androidMain/kotlin/dev/icerock/moko/permissions/compose/BindEffect.android.kt b/permissions-compose/src/androidMain/kotlin/dev/icerock/moko/permissions/compose/BindEffect.android.kt index 2bbfdbb..df0c82b 100644 --- a/permissions-compose/src/androidMain/kotlin/dev/icerock/moko/permissions/compose/BindEffect.android.kt +++ b/permissions-compose/src/androidMain/kotlin/dev/icerock/moko/permissions/compose/BindEffect.android.kt @@ -9,8 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.fragment.app.FragmentActivity -import androidx.fragment.app.FragmentManager +import androidx.activity.ComponentActivity import androidx.lifecycle.LifecycleOwner import dev.icerock.moko.permissions.PermissionsController @@ -21,10 +20,10 @@ actual fun BindEffect(permissionsController: PermissionsController) { val context: Context = LocalContext.current LaunchedEffect(permissionsController, lifecycleOwner, context) { - val fragmentManager: FragmentManager = checkNotNull(context as? FragmentActivity) { - "$context context is not instance of FragmentActivity" - }.supportFragmentManager + val activity: ComponentActivity = checkNotNull(context as? ComponentActivity) { + "$context context is not instance of ComponentActivity" + } - permissionsController.bind(lifecycleOwner.lifecycle, fragmentManager) + permissionsController.bind(activity) } } diff --git a/permissions-test/src/androidMain/kotlin/dev/icerock/moko/permissions/test/PermissionsControllerMock.kt b/permissions-test/src/androidMain/kotlin/dev/icerock/moko/permissions/test/PermissionsControllerMock.kt index def18d5..13c3d77 100755 --- a/permissions-test/src/androidMain/kotlin/dev/icerock/moko/permissions/test/PermissionsControllerMock.kt +++ b/permissions-test/src/androidMain/kotlin/dev/icerock/moko/permissions/test/PermissionsControllerMock.kt @@ -4,6 +4,7 @@ package dev.icerock.moko.permissions.test +import androidx.activity.ComponentActivity import dev.icerock.moko.permissions.Permission import dev.icerock.moko.permissions.PermissionsController @@ -13,8 +14,7 @@ actual abstract class PermissionsControllerMock : PermissionsController { actual abstract override suspend fun isPermissionGranted(permission: Permission): Boolean override fun bind( - lifecycle: androidx.lifecycle.Lifecycle, - fragmentManager: androidx.fragment.app.FragmentManager + activity: ComponentActivity ) { TODO("Not yet implemented") } diff --git a/permissions/build.gradle.kts b/permissions/build.gradle.kts index 1b6b4ad..273d798 100644 --- a/permissions/build.gradle.kts +++ b/permissions/build.gradle.kts @@ -15,6 +15,6 @@ android { dependencies { commonMainImplementation(libs.coroutines) - androidMainImplementation(libs.appCompat) + androidMainImplementation(libs.activityKtx) androidMainImplementation(libs.lifecycleRuntime) } \ No newline at end of file diff --git a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt b/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt index bddc77d..1ff1f90 100755 --- a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt +++ b/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt @@ -5,8 +5,7 @@ package dev.icerock.moko.permissions import android.content.Context -import androidx.fragment.app.FragmentManager -import androidx.lifecycle.Lifecycle +import androidx.activity.ComponentActivity actual interface PermissionsController { actual suspend fun providePermission(permission: Permission) @@ -14,15 +13,13 @@ actual interface PermissionsController { actual suspend fun getPermissionState(permission: Permission): PermissionState actual fun openAppSettings() - fun bind(lifecycle: Lifecycle, fragmentManager: FragmentManager) + fun bind(activity: ComponentActivity) companion object { operator fun invoke( - resolverFragmentTag: String = "PermissionsControllerResolver", applicationContext: Context ): PermissionsController { return PermissionsControllerImpl( - resolverFragmentTag = resolverFragmentTag, applicationContext = applicationContext ) } 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 73b71e2..c76c904 100755 --- a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt +++ b/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt @@ -5,57 +5,96 @@ 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 import android.os.Build import android.provider.Settings +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.ActivityResultRegistryOwner +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ActivityCompat +import androidx.activity.ComponentActivity import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager 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 @Suppress("TooManyFunctions") class PermissionsControllerImpl( - private val resolverFragmentTag: String = "PermissionsControllerResolver", private val applicationContext: Context, ) : PermissionsController { - private val fragmentManagerHolder = MutableStateFlow(null) + private val activityHolder = MutableStateFlow(null) + private val mutex: Mutex = Mutex() - override fun bind(lifecycle: Lifecycle, fragmentManager: FragmentManager) { - this.fragmentManagerHolder.value = fragmentManager + private var launcher: ActivityResultLauncher>? = null + + private var permissionCallback: PermissionCallback? = null + + override fun bind(activity: ComponentActivity) { + this.activityHolder.value = activity + val activityResultRegistryOwner = activity as ActivityResultRegistryOwner + + val key = UUID.randomUUID().toString() + + launcher = activityResultRegistryOwner.activityResultRegistry.register( + key, + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + val isCancelled = permissions.isEmpty() + + val permissionCallback = permissionCallback ?: return@register + + if (isCancelled) { + permissionCallback.callback.invoke( + Result.failure(RequestCanceledException(permissionCallback.permission)) + ) + return@register + } + + val success = permissions.values.all { it } + + if (success) { + permissionCallback.callback.invoke(Result.success(Unit)) + } else { + if (shouldShowRequestPermissionRationale(permissions.keys.first())) { + permissionCallback.callback.invoke( + Result.failure(DeniedException(permissionCallback.permission)) + ) + } else { + permissionCallback.callback.invoke( + Result.failure(DeniedAlwaysException(permissionCallback.permission)) + ) + } + } + } val observer = object : LifecycleEventObserver { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { if (event == Lifecycle.Event.ON_DESTROY) { - this@PermissionsControllerImpl.fragmentManagerHolder.value = null + this@PermissionsControllerImpl.activityHolder.value = null source.lifecycle.removeObserver(this) } } } - lifecycle.addObserver(observer) + activity.lifecycle.addObserver(observer) } override suspend fun providePermission(permission: Permission) { mutex.withLock { - val fragmentManager: FragmentManager = awaitFragmentManager() - val resolverFragment: ResolverFragment = getOrCreateResolverFragment(fragmentManager) - val platformPermission = permission.toPlatformPermission() suspendCoroutine { continuation -> - resolverFragment.requestPermission( + requestPermission( permission, platformPermission ) { continuation.resumeWith(it) } @@ -63,6 +102,15 @@ class PermissionsControllerImpl( } } + private fun requestPermission( + permission: Permission, + permissions: List, + callback: (Result) -> Unit + ) { + permissionCallback = PermissionCallback(permission, callback) + launcher?.launch(permissions.toTypedArray()) + } + override suspend fun isPermissionGranted(permission: Permission): Boolean { return getPermissionState(permission) == PermissionState.Granted } @@ -87,16 +135,27 @@ class PermissionsControllerImpl( val isAllGranted: Boolean = status.all { it == PackageManager.PERMISSION_GRANTED } if (isAllGranted) return PermissionState.Granted - val fragmentManager: FragmentManager = awaitFragmentManager() - val resolverFragment: ResolverFragment = getOrCreateResolverFragment(fragmentManager) - val isAllRequestRationale: Boolean = permissions.all { - resolverFragment.shouldShowRequestPermissionRationale(it) + shouldShowRequestPermissionRationale(it).not() } return if (isAllRequestRationale) PermissionState.Denied 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" + } + return ActivityCompat.shouldShowRequestPermissionRationale( + activity, + permission + ) + } + override fun openAppSettings() { val intent = Intent().apply { action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS @@ -106,35 +165,6 @@ class PermissionsControllerImpl( applicationContext.startActivity(intent) } - private suspend fun awaitFragmentManager(): FragmentManager { - val fragmentManager: FragmentManager? = fragmentManagerHolder.value - if (fragmentManager != null) return fragmentManager - - return withTimeoutOrNull(AWAIT_FRAGMENT_MANAGER_TIMEOUT_DURATION_MS) { - fragmentManagerHolder.filterNotNull().first() - } ?: error( - "fragmentManager is null, `bind` function was never called," + - " consider calling permissionsController.bind(lifecycle, fragmentManager)" + - " or BindEffect(permissionsController) in the composable function," + - " check the documentation for more info: " + - "https://github.com/icerockdev/moko-permissions/blob/master/README.md" - ) - } - - private fun getOrCreateResolverFragment(fragmentManager: FragmentManager): ResolverFragment { - val currentFragment: Fragment? = fragmentManager.findFragmentByTag(resolverFragmentTag) - return if (currentFragment != null) { - currentFragment as ResolverFragment - } else { - ResolverFragment().also { fragment -> - fragmentManager - .beginTransaction() - .add(fragment, resolverFragmentTag) - .commit() - } - } - } - @Suppress("CyclomaticComplexMethod") private fun Permission.toPlatformPermission(): List { return when (this) { @@ -273,6 +303,10 @@ class PermissionsControllerImpl( private companion object { val VERSIONS_WITHOUT_NOTIFICATION_PERMISSION = Build.VERSION_CODES.KITKAT until Build.VERSION_CODES.TIRAMISU - private const val AWAIT_FRAGMENT_MANAGER_TIMEOUT_DURATION_MS = 2000L } } + +private class PermissionCallback( + val permission: Permission, + val callback: (Result) -> Unit +) diff --git a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/ResolverFragment.kt b/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/ResolverFragment.kt deleted file mode 100644 index 813a1c2..0000000 --- a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/ResolverFragment.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.permissions - -import android.content.pm.PackageManager -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.launch - -internal class ResolverFragment : Fragment() { - - init { - retainInstance = true - } - - private var permissionCallback: PermissionCallback? = null - - private val requestPermissionLauncher = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissionResults -> - val permissionCallback = permissionCallback ?: return@registerForActivityResult - this.permissionCallback = null - - val isCancelled = permissionResults.isEmpty() - if (isCancelled) { - permissionCallback.callback.invoke( - Result.failure(RequestCanceledException(permissionCallback.permission)) - ) - return@registerForActivityResult - } - - val success = permissionResults.values.all { it } - if (success) { - permissionCallback.callback.invoke(Result.success(Unit)) - } else { - if (shouldShowRequestPermissionRationale(permissionResults.keys.first())) { - permissionCallback.callback.invoke( - Result.failure(DeniedException(permissionCallback.permission)) - ) - } else { - permissionCallback.callback.invoke( - Result.failure(DeniedAlwaysException(permissionCallback.permission)) - ) - } - } - } - - fun requestPermission( - permission: Permission, - permissions: List, - callback: (Result) -> Unit - ) { - lifecycleScope.launch { - lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { - val toRequest = permissions.filter { - ContextCompat.checkSelfPermission( - requireContext(), - it - ) != PackageManager.PERMISSION_GRANTED - } - - if (toRequest.isEmpty()) { - callback.invoke(Result.success(Unit)) - return@repeatOnLifecycle - } - - permissionCallback?.let { - it.callback.invoke(Result.failure(RequestCanceledException(it.permission))) - permissionCallback = null - } - - permissionCallback = PermissionCallback(permission, callback) - - requestPermissionLauncher.launch(toRequest.toTypedArray()) - } - } - } - - private class PermissionCallback( - val permission: Permission, - val callback: (Result) -> Unit - ) -} diff --git a/sample/android-app/src/main/java/com/icerockdev/MainActivity.kt b/sample/android-app/src/main/java/com/icerockdev/MainActivity.kt index 2c0a23b..c034cc5 100755 --- a/sample/android-app/src/main/java/com/icerockdev/MainActivity.kt +++ b/sample/android-app/src/main/java/com/icerockdev/MainActivity.kt @@ -30,7 +30,7 @@ class MainActivity : AppCompatActivity(), SampleViewModel.EventListener { permissionType = Permission.CONTACTS ) }.also { - it.permissionsController.bind(lifecycle, supportFragmentManager) + it.permissionsController.bind(this) it.eventsDispatcher.bind(this, this) } }