From 7a9df2156909cee3789efc7593758eedef04024a Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 29 Feb 2024 13:41:54 +0100 Subject: [PATCH 1/9] Show `PermissionState` in sample app --- .../src/main/java/com/icerockdev/MainActivity.kt | 14 ++++++++++++-- .../com/icerockdev/library/SampleViewModel.kt | 15 +++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt b/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt index e42c1c8..d10d7d3 100644 --- a/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt +++ b/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt @@ -4,7 +4,9 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.Button @@ -17,6 +19,9 @@ import androidx.compose.material.Text import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -29,6 +34,7 @@ import dev.icerock.moko.mvvm.getViewModel import dev.icerock.moko.permissions.DeniedAlwaysException import dev.icerock.moko.permissions.DeniedException import dev.icerock.moko.permissions.Permission +import dev.icerock.moko.permissions.PermissionState import dev.icerock.moko.permissions.PermissionsController import dev.icerock.moko.permissions.compose.BindEffect import kotlinx.coroutines.launch @@ -100,16 +106,20 @@ fun TestScreen(viewModel: SampleViewModel) { LaunchedEffect(true) { viewModel.eventsDispatcher.bind(lifecycleOwner, eventsListener) } + val permissionState by viewModel.permissionState.collectAsState() BindEffect(viewModel.permissionsController) Scaffold(scaffoldState = scaffoldState) { - Box( + Column( modifier = Modifier .fillMaxSize() .padding(it), - contentAlignment = Alignment.Center + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { + Text("Permission state: $permissionState") + Button( onClick = viewModel::onRequestPermissionButtonPressed, content = { Text("Request permission") } diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/SampleViewModel.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/SampleViewModel.kt index e561af0..433bea5 100755 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/SampleViewModel.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/SampleViewModel.kt @@ -6,7 +6,10 @@ import dev.icerock.moko.mvvm.viewmodel.ViewModel import dev.icerock.moko.permissions.DeniedAlwaysException import dev.icerock.moko.permissions.DeniedException import dev.icerock.moko.permissions.Permission +import dev.icerock.moko.permissions.PermissionState import dev.icerock.moko.permissions.PermissionsController +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class SampleViewModel( @@ -15,10 +18,12 @@ class SampleViewModel( private val permissionType: Permission ) : ViewModel(), EventsDispatcherOwner { + val permissionState = MutableStateFlow(PermissionState.NotDetermined) + init { viewModelScope.launch { - val startState = permissionsController.getPermissionState(permissionType) - println(startState) + permissionState.update { permissionsController.getPermissionState(permissionType) } + println(permissionState) } } @@ -45,8 +50,10 @@ class SampleViewModel( } catch (deniedException: DeniedException) { eventsDispatcher.dispatchEvent { onDenied(deniedException) } } finally { - permissionsController.getPermissionState(permission) - .also { println("post provide $it") } + permissionState.update { + permissionsController.getPermissionState(permission) + .also { println("post provide $it") } + } } } } From 1ed21083a3d90c9ded92f01f6d52c2024acf42cc Mon Sep 17 00:00:00 2001 From: Jacob Ras Date: Thu, 29 Feb 2024 13:45:31 +0100 Subject: [PATCH 2/9] Add `NotGranted` item to `PermissionState` This fixes the issue with [deny and don't ask again] returning an incorrect state. Unfortunately, it's not possible on Android to determine the difference. One has to keep track of whether a permission has been requested in the app already, so that's up to consumers. --- .../permissions/PermissionsControllerImpl.kt | 6 +++--- .../moko/permissions/PermissionState.kt | 19 +++++++++++++++++++ .../moko/permissions/PermissionsController.kt | 7 ++++--- 3 files changed, 26 insertions(+), 6 deletions(-) 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 d8e6562..4b35db3 100755 --- a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt +++ b/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt @@ -91,10 +91,10 @@ class PermissionsControllerImpl( val resolverFragment: ResolverFragment = getOrCreateResolverFragment(fragmentManager) val isAllRequestRationale: Boolean = permissions.all { - !resolverFragment.shouldShowRequestPermissionRationale(it) + resolverFragment.shouldShowRequestPermissionRationale(it) } - return if (isAllRequestRationale) PermissionState.NotDetermined - else PermissionState.Denied + return if (isAllRequestRationale) PermissionState.Denied + else PermissionState.NotGranted } override fun openAppSettings() { diff --git a/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionState.kt b/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionState.kt index 075241f..414f9b6 100644 --- a/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionState.kt +++ b/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionState.kt @@ -5,8 +5,27 @@ package dev.icerock.moko.permissions enum class PermissionState { + + /** + * Starting state for each permission. + */ NotDetermined, + + /** + * Android-only. This could mean [NotDetermined] or [DeniedAlways], but the OS doesn't + * expose which of the two it is in all scenarios. + */ + NotGranted, + Granted, + + /** + * Android-only. + */ Denied, + + /** + * On Android only applicable to Push Notifications. + */ DeniedAlways } diff --git a/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt b/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt index 1ac8381..6505e01 100755 --- a/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt +++ b/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/PermissionsController.kt @@ -24,12 +24,13 @@ expect interface PermissionsController { /** * Returns current state of permission. Can be suspended because on - * android detection of Denied/NotDetermined case require binded FragmentManager. + * Android detection of `Denied`/`NotDetermined` requires a bound FragmentManager. * * @param permission state of what permission we want * - * @return current state. On Android can't be DeniedAlways (except push notifications). - * On iOS can't be Denied. + * @return current state. On Android can't be `DeniedAlways` (except push notifications). + * On iOS can't be `Denied`. + * @see PermissionState for a detailed description. */ suspend fun getPermissionState(permission: Permission): PermissionState From 4811ba867eef825cb05eae5388d50cc041d3cc45 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Fri, 12 Apr 2024 14:06:03 +0700 Subject: [PATCH 3/9] Up kotlin version --- README.md | 2 +- gradle/libs.versions.toml | 4 ++-- .../moko/permissions/ios/BluetoothPermissionDelegate.kt | 3 +++ sample/ios-app/Podfile | 2 +- sample/mpp-library/MultiPlatformLibrary.podspec | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fe8decb..2e0b91c 100755 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ ## Requirements - Gradle version 6.8+ - Android API 16+ -- iOS version 11.0+ +- iOS version 12.0+ ## Installation root **build.gradle** diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0b8373d..e200170 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlinVersion = "1.8.10" +kotlinVersion = "1.9.10" androidAppCompatVersion = "1.6.1" composeMaterialVersion = "1.4.1" composeActivityVersion = "1.7.0" @@ -9,7 +9,7 @@ androidCoreTestingVersion = "2.2.0" coroutinesVersion = "1.6.4" mokoMvvmVersion = "0.16.0" mokoPermissionsVersion = "0.17.0" -composeJetBrainsVersion = "1.3.1" +composeJetBrainsVersion = "1.5.1" lifecycleRuntime = "2.6.1" [libraries] diff --git a/permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/BluetoothPermissionDelegate.kt b/permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/BluetoothPermissionDelegate.kt index 0d343df..c6a2d23 100644 --- a/permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/BluetoothPermissionDelegate.kt +++ b/permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/BluetoothPermissionDelegate.kt @@ -8,6 +8,7 @@ import dev.icerock.moko.permissions.DeniedAlwaysException import dev.icerock.moko.permissions.DeniedException import dev.icerock.moko.permissions.Permission import dev.icerock.moko.permissions.PermissionState +import kotlinx.cinterop.ExperimentalForeignApi import platform.CoreBluetooth.CBCentralManager import platform.CoreBluetooth.CBCentralManagerDelegateProtocol import platform.CoreBluetooth.CBManager @@ -31,6 +32,7 @@ import kotlin.coroutines.suspendCoroutine internal class BluetoothPermissionDelegate( private val permission: Permission ) : PermissionDelegate { + @OptIn(ExperimentalForeignApi::class) override suspend fun providePermission() { // To maintain compatibility with iOS 12 (@see https://developer.apple.com/documentation/corebluetooth/cbmanagerauthorization) val isNotDetermined: Boolean = @@ -72,6 +74,7 @@ internal class BluetoothPermissionDelegate( } } + @OptIn(ExperimentalForeignApi::class) override suspend fun getPermissionState(): PermissionState { // To maintain compatibility with iOS 12 (@see https://developer.apple.com/documentation/corebluetooth/cbmanagerauthorization) if (CBManager.resolveClassMethod(NSSelectorFromString("authorization"))) { diff --git a/sample/ios-app/Podfile b/sample/ios-app/Podfile index 04ec21c..cc40912 100644 --- a/sample/ios-app/Podfile +++ b/sample/ios-app/Podfile @@ -4,7 +4,7 @@ source 'https://cdn.cocoapods.org/' inhibit_all_warnings! use_frameworks! -platform :ios, '11.0' +platform :ios, '12.0' # workaround for https://github.com/CocoaPods/CocoaPods/issues/8073 # need for correct invalidate of cache MultiPlatformLibrary.framework diff --git a/sample/mpp-library/MultiPlatformLibrary.podspec b/sample/mpp-library/MultiPlatformLibrary.podspec index 0f908b0..e736430 100644 --- a/sample/mpp-library/MultiPlatformLibrary.podspec +++ b/sample/mpp-library/MultiPlatformLibrary.podspec @@ -11,7 +11,7 @@ Pod::Spec.new do |spec| spec.libraries = "c++" spec.module_name = "#{spec.name}_umbrella" - spec.ios.deployment_target = '11.0' + spec.ios.deployment_target = '12.0' spec.osx.deployment_target = '10.6' spec.pod_target_xcconfig = { From 24664f694ba8e09bc015ab3e3a4c63de6a615f47 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Thu, 11 Apr 2024 19:06:54 +0700 Subject: [PATCH 4/9] Add BACKGROUND_LOCATION Permission --- README.md | 1 + .../permissions/PermissionsControllerImpl.kt | 17 +++++++++++++++++ .../dev/icerock/moko/permissions/Permission.kt | 1 + .../permissions/ios/PermissionsController.kt | 2 +- sample/android-app/src/main/AndroidManifest.xml | 1 + .../src/main/AndroidManifest.xml | 1 + 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fe8decb..c5591b2 100755 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ The full list can be found in `dev.icerock.moko.permissions.Permission` enum. * Storage write: **Permission.WRITE_STORAGE** * Fine location: **Permission.LOCATION** * Coarse location: **Permission.COARSE_LOCATION** +* Background location: **Permission.BACKGROUND_LOCATION** * Remote notifications: **Permission.REMOTE_NOTIFICATION** * Audio recording: **Permission.RECORD_AUDIO** * Bluetooth LE: **Permission.BLUETOOTH_LE** 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 d8e6562..03c9572 100755 --- a/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt +++ b/permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/PermissionsControllerImpl.kt @@ -144,6 +144,7 @@ class PermissionsControllerImpl( Permission.WRITE_STORAGE -> listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) Permission.LOCATION -> fineLocationCompat() Permission.COARSE_LOCATION -> listOf(Manifest.permission.ACCESS_COARSE_LOCATION) + Permission.BACKGROUND_LOCATION -> backgroundLocationCompat() Permission.REMOTE_NOTIFICATION -> remoteNotificationsPermissions() Permission.RECORD_AUDIO -> listOf(Manifest.permission.RECORD_AUDIO) Permission.BLUETOOTH_LE -> allBluetoothPermissions() @@ -192,6 +193,22 @@ class PermissionsControllerImpl( listOf(Manifest.permission.ACCESS_FINE_LOCATION) } + private fun backgroundLocationCompat() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_BACKGROUND_LOCATION, + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + listOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_BACKGROUND_LOCATION, + ) + } else { + listOf(Manifest.permission.ACCESS_FINE_LOCATION) + } + /** * Bluetooth permissions * diff --git a/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/Permission.kt b/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/Permission.kt index f375001..8dbb22b 100755 --- a/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/Permission.kt +++ b/permissions/src/commonMain/kotlin/dev/icerock/moko/permissions/Permission.kt @@ -11,6 +11,7 @@ enum class Permission { WRITE_STORAGE, LOCATION, COARSE_LOCATION, + BACKGROUND_LOCATION, BLUETOOTH_LE, REMOTE_NOTIFICATION, RECORD_AUDIO, diff --git a/permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/PermissionsController.kt b/permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/PermissionsController.kt index bc04d58..45485a2 100755 --- a/permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/PermissionsController.kt +++ b/permissions/src/iosMain/kotlin/dev/icerock/moko/permissions/ios/PermissionsController.kt @@ -41,7 +41,7 @@ class PermissionsController : PermissionsControllerProtocol { Permission.CAMERA -> AVCapturePermissionDelegate(AVMediaTypeVideo, permission) Permission.GALLERY -> GalleryPermissionDelegate() Permission.STORAGE, Permission.WRITE_STORAGE -> AlwaysGrantedPermissionDelegate() - Permission.LOCATION, Permission.COARSE_LOCATION -> + Permission.LOCATION, Permission.COARSE_LOCATION, Permission.BACKGROUND_LOCATION -> LocationPermissionDelegate(locationManagerDelegate, permission) Permission.RECORD_AUDIO -> AVCapturePermissionDelegate(AVMediaTypeAudio, permission) diff --git a/sample/android-app/src/main/AndroidManifest.xml b/sample/android-app/src/main/AndroidManifest.xml index b7ee64f..0f42c30 100755 --- a/sample/android-app/src/main/AndroidManifest.xml +++ b/sample/android-app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + diff --git a/sample/compose-android-app/src/main/AndroidManifest.xml b/sample/compose-android-app/src/main/AndroidManifest.xml index c9491fb..9cd3c98 100755 --- a/sample/compose-android-app/src/main/AndroidManifest.xml +++ b/sample/compose-android-app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + Date: Thu, 11 Apr 2024 16:29:06 +0700 Subject: [PATCH 5/9] Change base class for MainActivity --- .../src/main/java/com/icerockdev/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt b/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt index d10d7d3..ad4b3d9 100644 --- a/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt +++ b/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt @@ -2,7 +2,6 @@ package com.icerockdev import android.os.Bundle import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -28,6 +27,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.fragment.app.FragmentActivity import com.icerockdev.library.SampleViewModel import dev.icerock.moko.mvvm.dispatcher.eventsDispatcherOnMain import dev.icerock.moko.mvvm.getViewModel @@ -39,7 +39,7 @@ import dev.icerock.moko.permissions.PermissionsController import dev.icerock.moko.permissions.compose.BindEffect import kotlinx.coroutines.launch -class MainActivity : AppCompatActivity() { +class MainActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { From 3e50aac0d7b831dff5cf484253cde990b850cd0f Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Fri, 12 Apr 2024 11:34:35 +0700 Subject: [PATCH 6/9] Add error message at attempt to get fragmentManager --- .../icerock/moko/permissions/compose/BindEffect.android.kt | 4 +++- .../src/main/java/com/icerockdev/MainActivity.kt | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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 d404a65..2bbfdbb 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 @@ -21,7 +21,9 @@ actual fun BindEffect(permissionsController: PermissionsController) { val context: Context = LocalContext.current LaunchedEffect(permissionsController, lifecycleOwner, context) { - val fragmentManager: FragmentManager = (context as FragmentActivity).supportFragmentManager + val fragmentManager: FragmentManager = checkNotNull(context as? FragmentActivity) { + "$context context is not instance of FragmentActivity" + }.supportFragmentManager permissionsController.bind(lifecycleOwner.lifecycle, fragmentManager) } diff --git a/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt b/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt index ad4b3d9..d10d7d3 100644 --- a/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt +++ b/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt @@ -2,6 +2,7 @@ package com.icerockdev import android.os.Bundle import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -27,7 +28,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.fragment.app.FragmentActivity import com.icerockdev.library.SampleViewModel import dev.icerock.moko.mvvm.dispatcher.eventsDispatcherOnMain import dev.icerock.moko.mvvm.getViewModel @@ -39,7 +39,7 @@ import dev.icerock.moko.permissions.PermissionsController import dev.icerock.moko.permissions.compose.BindEffect import kotlinx.coroutines.launch -class MainActivity : FragmentActivity() { +class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { From f4921fa1bd5be68d0de7867f02cd4c4f88c33e25 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Mon, 15 Apr 2024 17:23:12 +0700 Subject: [PATCH 7/9] Remove appCompat dependency --- gradle/libs.versions.toml | 2 + permissions-compose/build.gradle.kts | 1 - .../permissions/compose/BindEffect.android.kt | 11 +- .../test/PermissionsControllerMock.kt | 4 +- permissions/build.gradle.kts | 2 +- .../moko/permissions/PermissionsController.kt | 7 +- .../permissions/PermissionsControllerImpl.kt | 132 +++++++++++------- .../moko/permissions/ResolverFragment.kt | 88 ------------ .../main/java/com/icerockdev/MainActivity.kt | 2 +- 9 files changed, 96 insertions(+), 153 deletions(-) delete mode 100644 permissions/src/androidMain/kotlin/dev/icerock/moko/permissions/ResolverFragment.kt 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) } } From 2251a75e205e0864769f79e8deacbea7f4670548 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Thu, 18 Apr 2024 13:44:37 +0700 Subject: [PATCH 8/9] #106 Remove activityKtx dependency --- README.md | 2 +- gradle/libs.versions.toml | 6 +- permissions-compose/build.gradle.kts | 5 +- permissions/build.gradle.kts | 4 +- .../permissions/PermissionsControllerImpl.kt | 67 ++++++++++++++----- .../main/java/com/icerockdev/MainActivity.kt | 7 +- 6 files changed, 64 insertions(+), 27 deletions(-) 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 } } diff --git a/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt b/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt index d10d7d3..09653a0 100644 --- a/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt +++ b/sample/compose-android-app/src/main/java/com/icerockdev/MainActivity.kt @@ -1,11 +1,10 @@ package com.icerockdev import android.os.Bundle +import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -21,7 +20,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -34,12 +32,11 @@ import dev.icerock.moko.mvvm.getViewModel import dev.icerock.moko.permissions.DeniedAlwaysException import dev.icerock.moko.permissions.DeniedException import dev.icerock.moko.permissions.Permission -import dev.icerock.moko.permissions.PermissionState import dev.icerock.moko.permissions.PermissionsController import dev.icerock.moko.permissions.compose.BindEffect import kotlinx.coroutines.launch -class MainActivity : AppCompatActivity() { +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { From 9881ca88e41c5fad4d77b12a27304db13349fad2 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Fri, 19 Apr 2024 11:07:29 +0700 Subject: [PATCH 9/9] up version --- README.md | 6 +++--- gradle/libs.versions.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2e39626..8c8c0f9 100755 --- a/README.md +++ b/README.md @@ -39,12 +39,12 @@ allprojects { project **build.gradle** ```groovy dependencies { - commonMainApi("dev.icerock.moko:permissions:0.17.0") + commonMainApi("dev.icerock.moko:permissions:0.18.0") // compose multiplatform - commonMainApi("dev.icerock.moko:permissions-compose:0.17.0") // permissions api + compose extensions + commonMainApi("dev.icerock.moko:permissions-compose:0.18.0") // permissions api + compose extensions - commonTestImplementation("dev.icerock.moko:permissions-test:0.17.0") + commonTestImplementation("dev.icerock.moko:permissions-test:0.18.0") } ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fff4b40..f7359ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ androidLifecycleVersion = "2.2.0" androidCoreTestingVersion = "2.2.0" coroutinesVersion = "1.6.4" mokoMvvmVersion = "0.16.0" -mokoPermissionsVersion = "0.17.0" +mokoPermissionsVersion = "0.18.0" composeJetBrainsVersion = "1.5.1" lifecycleRuntime = "2.6.1" composeUiVersion = "1.0.1"