Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix/replace fragment manager #114

Merged
merged 4 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
```

Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -11,12 +12,15 @@ mokoMvvmVersion = "0.16.0"
mokoPermissionsVersion = "0.17.0"
composeJetBrainsVersion = "1.5.1"
lifecycleRuntime = "2.6.1"
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" }
Expand Down
6 changes: 3 additions & 3 deletions permissions-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ android {
dependencies {
commonMainApi(projects.permissions)
commonMainApi(compose.runtime)

androidMainImplementation(libs.appCompat)
androidMainImplementation(libs.composeActivity)
androidMainImplementation(libs.activity)
androidMainImplementation(libs.composeUi)
androidMainImplementation(libs.lifecycleRuntime)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -21,8 +20,10 @@ actual fun BindEffect(permissionsController: PermissionsController) {
val context: Context = LocalContext.current

LaunchedEffect(permissionsController, lifecycleOwner, context) {
val fragmentManager: FragmentManager = (context as FragmentActivity).supportFragmentManager
val activity: ComponentActivity = checkNotNull(context as? ComponentActivity) {
"$context context is not instance of ComponentActivity"
}

permissionsController.bind(lifecycleOwner.lifecycle, fragmentManager)
permissionsController.bind(activity)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
}
Expand Down
4 changes: 2 additions & 2 deletions permissions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ android {

dependencies {
commonMainImplementation(libs.coroutines)
androidMainImplementation(libs.appCompat)
androidMainImplementation(libs.activity)
androidMainImplementation(libs.lifecycleRuntime)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,21 @@
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)
actual suspend fun isPermissionGranted(permission: Permission): Boolean
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
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
package dev.icerock.moko.permissions

import android.Manifest
import android.app.Activity
import android.content.Context
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
Expand All @@ -24,45 +28,127 @@ 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<FragmentManager?>(null)
private val activityHolder = MutableStateFlow<Activity?>(null)

private val mutex: Mutex = Mutex()

override fun bind(lifecycle: Lifecycle, fragmentManager: FragmentManager) {
this.fragmentManagerHolder.value = fragmentManager
private val launcherHolder = MutableStateFlow<ActivityResultLauncher<Array<String>>?>(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 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(activity, permissions.keys.first())) {
permissionCallback.callback.invoke(
Result.failure(DeniedException(permissionCallback.permission))
)
} else {
permissionCallback.callback.invoke(
Result.failure(DeniedAlwaysException(permissionCallback.permission))
)
}
}
}

launcherHolder.value = launcher

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
this@PermissionsControllerImpl.launcherHolder.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 launcher = awaitActivityResultLauncher()
val platformPermission = permission.toPlatformPermission()
suspendCoroutine { continuation ->
resolverFragment.requestPermission(
requestPermission(
launcher,
permission,
platformPermission
) { continuation.resumeWith(it) }
}
}
}

private fun requestPermission(
launcher: ActivityResultLauncher<Array<String>>,
permission: Permission,
permissions: List<String>,
callback: (Result<Unit>) -> Unit
) {
permissionCallback = PermissionCallback(permission, callback)
launcher.launch(permissions.toTypedArray())
}

private suspend fun awaitActivityResultLauncher(): ActivityResultLauncher<Array<String>> {
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 {
return getPermissionState(permission) == PermissionState.Granted
}
Expand All @@ -87,16 +173,25 @@ 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 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
)
}

override fun openAppSettings() {
val intent = Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
Expand All @@ -106,35 +201,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<String> {
return when (this) {
Expand Down Expand Up @@ -273,6 +339,11 @@ 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 const val AWAIT_ACTIVITY_TIMEOUT_DURATION_MS = 2000L
}
}

private class PermissionCallback(
val permission: Permission,
val callback: (Result<Unit>) -> Unit
)
Loading
Loading