Skip to content

Commit

Permalink
Merge pull request #83 from icerockdev/develop
Browse files Browse the repository at this point in the history
Release 0.16.0
  • Loading branch information
Alex009 authored Apr 15, 2023
2 parents bbadb95 + 9ac8d6f commit 343bb41
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 68 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ allprojects {
project **build.gradle**
```groovy
dependencies {
commonMainApi("dev.icerock.moko:permissions:0.15.0")
commonMainApi("dev.icerock.moko:permissions:0.16.0")
// compose multiplatform
commonMainApi("dev.icerock.moko:permissions-compose:0.15.0") // permissions api + compose extensions
commonMainApi("dev.icerock.moko:permissions-compose:0.16.0") // permissions api + compose extensions
commonTestImplementation("dev.icerock.moko:permissions-test:0.15.0")
commonTestImplementation("dev.icerock.moko:permissions-test:0.16.0")
}
```

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ androidLifecycleVersion = "2.2.0"
androidCoreTestingVersion = "2.2.0"
coroutinesVersion = "1.6.4"
mokoMvvmVersion = "0.16.0"
mokoPermissionsVersion = "0.15.0"
mokoPermissionsVersion = "0.16.0"
composeJetBrainsVersion = "1.3.1"
lifecycleRuntime = "2.6.1"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
Expand All @@ -37,11 +36,12 @@ class PermissionsControllerImpl(
override fun bind(lifecycle: Lifecycle, fragmentManager: FragmentManager) {
this.fragmentManagerHolder.value = fragmentManager

val observer = object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroyed(source: LifecycleOwner) {
this@PermissionsControllerImpl.fragmentManagerHolder.value = null
source.lifecycle.removeObserver(this)
val observer = object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY){
this@PermissionsControllerImpl.fragmentManagerHolder.value = null
source.lifecycle.removeObserver(this)
}
}
}
lifecycle.addObserver(observer)
Expand All @@ -53,7 +53,7 @@ class PermissionsControllerImpl(
val resolverFragment: ResolverFragment = getOrCreateResolverFragment(fragmentManager)

val platformPermission = permission.toPlatformPermission()
suspendCoroutine<Unit> { continuation ->
suspendCoroutine { continuation ->
resolverFragment.requestPermission(
permission,
platformPermission
Expand Down Expand Up @@ -129,10 +129,10 @@ class PermissionsControllerImpl(
private fun Permission.toPlatformPermission(): List<String> {
return when (this) {
Permission.CAMERA -> listOf(Manifest.permission.CAMERA)
Permission.GALLERY -> listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
Permission.STORAGE -> listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
Permission.GALLERY -> galleryCompat()
Permission.STORAGE -> allStoragePermissions()
Permission.WRITE_STORAGE -> listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
Permission.LOCATION -> listOf(Manifest.permission.ACCESS_FINE_LOCATION)
Permission.LOCATION -> fineLocationCompat()
Permission.COARSE_LOCATION -> listOf(Manifest.permission.ACCESS_COARSE_LOCATION)
Permission.REMOTE_NOTIFICATION -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Expand All @@ -151,8 +151,48 @@ class PermissionsControllerImpl(
}

/**
* Behavior changes: Apps targeting Android 13 or higher
*
* @see https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions
*/

private fun allStoragePermissions() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}

private fun galleryCompat() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
listOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
)
} else {
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}

private fun fineLocationCompat() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
} else {
listOf(Manifest.permission.ACCESS_FINE_LOCATION)
}

/**
* Bluetooth permissions
*
* @see https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
*/

private fun allBluetoothPermissions() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,78 +5,78 @@
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 val permissionCallbackMap = mutableMapOf<Int, PermissionCallback>()
private var permissionCallback: PermissionCallback? = null

fun requestPermission(
permission: Permission,
permissions: List<String>,
callback: (Result<Unit>) -> Unit
) {
lifecycleScope.launchWhenCreated {
val context = requireContext()
val toRequest = permissions.filter {
ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissionResults ->
val permissionCallback = permissionCallback ?: return@registerForActivityResult
this.permissionCallback = null

if (toRequest.isEmpty()) {
callback.invoke(Result.success(Unit))
return@launchWhenCreated
val isCancelled = permissionResults.isEmpty()
if (isCancelled) {
permissionCallback.callback.invoke(
Result.failure(RequestCanceledException(permissionCallback.permission))
)
return@registerForActivityResult
}

val requestCode = (permissionCallbackMap.keys.maxOrNull() ?: 0) + 1
permissionCallbackMap[requestCode] = PermissionCallback(permission, callback)

requestPermissions(toRequest.toTypedArray(), requestCode)
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))
)
}
}
}
}

@Suppress("UnreachableCode")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
fun requestPermission(
permission: Permission,
permissions: List<String>,
callback: (Result<Unit>) -> Unit
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
val toRequest = permissions.filter {
ContextCompat.checkSelfPermission(
requireContext(),
it
) != PackageManager.PERMISSION_GRANTED
}

val permissionCallback = permissionCallbackMap[requestCode] ?: return
permissionCallbackMap.remove(requestCode)
if (toRequest.isEmpty()) {
callback.invoke(Result.success(Unit))
return@repeatOnLifecycle
}

managePermissions(permissionCallback, permissions, grantResults)
}
permissionCallback?.let {
it.callback.invoke(Result.failure(RequestCanceledException(it.permission)))
permissionCallback = null
}

private fun managePermissions(
permissionCallback: PermissionCallback,
permissions: Array<out String>,
grantResults: IntArray
) {
val isCancelled = grantResults.isEmpty() || permissions.isEmpty()
if (isCancelled) {
permissionCallback.callback.invoke(
Result.failure(RequestCanceledException(permissionCallback.permission))
)
return
}
val success = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (success) {
permissionCallback.callback.invoke(Result.success(Unit))
} else {
if (shouldShowRequestPermissionRationale(permissions.first())) {
permissionCallback.callback.invoke(
Result.failure(DeniedException(permissionCallback.permission))
)
} else {
permissionCallback.callback.invoke(
Result.failure(DeniedAlwaysException(permissionCallback.permission))
)
permissionCallback = PermissionCallback(permission, callback)

requestPermissionLauncher.launch(toRequest.toTypedArray())
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions sample/android-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

<application
android:label="moko-permissions test app"
Expand Down

0 comments on commit 343bb41

Please sign in to comment.