Skip to content

Commit

Permalink
Merge pull request #114 from ZiXOps/bugfix/replace-fragment-manager
Browse files Browse the repository at this point in the history
Bugfix/replace fragment manager
  • Loading branch information
Alex009 authored Apr 18, 2024
2 parents ec4c235 + 2251a75 commit 3b2770c
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 157 deletions.
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

0 comments on commit 3b2770c

Please sign in to comment.