diff --git a/README.md b/README.md index b0e7c23..ff2af81 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Sponsored by [CloudBit](https://www.cloudbit.hr) Hosted on [Maven Central](https://search.maven.org/artifact/com.markodevcic/peko) ``` -implementation 'com.markodevcic:peko:3.0.5' +implementation 'com.markodevcic:peko:3.1.+' ``` ### Example @@ -70,6 +70,31 @@ launch { } ``` +Need to inspect the current permission state **before requesting** anything? Use `checkPermissionState()`. +It returns a `Flow` just like `request()`, but **without triggering the system permission dialog**. + +This is useful for handling sensitive UX flows or proactively deciding whether to show rationale. + +```kotlin +launch { + requester.checkPermissionState( + Manifest.permission.CAMERA, + Manifest.permission.READ_CONTACTS + ).collect { p -> + when (p) { + is PermissionResult.Granted -> print("${p.permission} granted") // already granted + is PermissionResult.Denied.NeedsRationale -> print("${p.permission} needs rationale") // show rationale + is PermissionResult.NeverAskedOrDeniedPermanently -> print("${p.permission} never asked or permanently denied") // ambiguous state + is PermissionResult.Cancelled -> print("check cancelled") + } + } +} +``` +⚠️ `NeverAskedOrDeniedPermanently` reflects Android's behavior, where a permission that was never requested and one that was permanently denied both return the same state. +If needed, you can still call `request()` afterward to resolve the actual condition. + +🚧 **Coming in Version 4.0.0**: PEKO will introduce internal permission state tracking to help eliminate this ambiguity and become a **Single Source of Truth (SSOT)** for permission management. + Need to check only if permissions are granted? Let's skip the horrible Android API. No coroutine required. diff --git a/build.gradle b/build.gradle index 16ead46..9cbb897 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0" +// classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0" classpath "com.github.dcendents:android-maven-gradle-plugin:2.1" } } diff --git a/examples/src/main/java/com/markodevcic/samples/MainActivity.kt b/examples/src/main/java/com/markodevcic/samples/MainActivity.kt index e65da95..ec05898 100644 --- a/examples/src/main/java/com/markodevcic/samples/MainActivity.kt +++ b/examples/src/main/java/com/markodevcic/samples/MainActivity.kt @@ -35,11 +35,17 @@ class MainActivity : AppCompatActivity() { .collect { setResult(it) } } + lifecycleScope.launchWhenStarted { + viewModel.permissionStateFlow.collect { + setResult(it, false) + } + } + btnContacts.setOnClickListener { requestPermission(Manifest.permission.READ_CONTACTS) } btnFineLocation.setOnClickListener { - requestPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + requestPermission(Manifest.permission.ACCESS_FINE_LOCATION) } btnFile.setOnClickListener { requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) @@ -51,11 +57,34 @@ class MainActivity : AppCompatActivity() { viewModel.requestPermissions( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA, - Manifest.permission.ACCESS_BACKGROUND_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_CONTACTS ) - } - } + } + + btnContactsState.setOnClickListener { + viewModel.permissionState(Manifest.permission.READ_CONTACTS) + } + btnFineLocationState.setOnClickListener { + viewModel.permissionState(Manifest.permission.ACCESS_FINE_LOCATION) + } + btnFileState.setOnClickListener { + viewModel.permissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + btnCameraState.setOnClickListener { + viewModel.permissionState(Manifest.permission.CAMERA) + } + btnAllSates.setOnClickListener { + viewModel.permissionState( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.CAMERA, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.READ_CONTACTS + ) + } + } + + private fun checkAllGranted(vararg permissions: String) { lifecycleScope.launch { @@ -67,54 +96,116 @@ class MainActivity : AppCompatActivity() { viewModel.requestPermissions(*permissions) } - private fun setResult(result: PermissionResult) { - if (result is PermissionResult.Granted) { - - val granted = "GRANTED" - if (Manifest.permission.ACCESS_BACKGROUND_LOCATION == result.permission) { - textLocationResult.text = granted - textLocationResult.setTextColor(Color.GREEN) - } - if (Manifest.permission.WRITE_EXTERNAL_STORAGE == result.permission) { - textFileResult.text = granted - textFileResult.setTextColor(Color.GREEN) - } - if (Manifest.permission.CAMERA == result.permission) { - textCameraResult.text = granted - textCameraResult.setTextColor(Color.GREEN) - } - if (Manifest.permission.READ_CONTACTS == result.permission) { - textContactsResult.text = granted - textContactsResult.setTextColor(Color.GREEN) - } - } else if (result is PermissionResult.Denied) { - if (Manifest.permission.ACCESS_BACKGROUND_LOCATION == result.permission) { - textLocationResult.text = deniedReasonText(result) - textLocationResult.setTextColor(Color.RED) - } - if (Manifest.permission.WRITE_EXTERNAL_STORAGE == result.permission) { - textFileResult.text = deniedReasonText(result) - textFileResult.setTextColor(Color.RED) - } - if (Manifest.permission.CAMERA == result.permission) { - textCameraResult.text = deniedReasonText(result) - textCameraResult.setTextColor(Color.RED) - } - if (Manifest.permission.READ_CONTACTS == result.permission) { - textContactsResult.text = deniedReasonText(result) - textContactsResult.setTextColor(Color.RED) - } - } else if (result is PermissionResult.Cancelled) { - textLocationResult.text = cancelled - textLocationResult.setTextColor(Color.RED) - textFileResult.text = cancelled - textFileResult.setTextColor(Color.RED) - textCameraResult.text = cancelled - textCameraResult.setTextColor(Color.RED) - textContactsResult.text = cancelled - textContactsResult.setTextColor(Color.RED) - } - } + private fun setResult(result: PermissionResult, isRequest: Boolean = true) { + if (result is PermissionResult.Granted) { + + val granted = "GRANTED" + if (Manifest.permission.ACCESS_FINE_LOCATION == result.permission) { + textLocationState.text = granted + textLocationState.setTextColor(Color.GREEN) + if (isRequest) { + textLocationResult.text = granted + textLocationResult.setTextColor(Color.GREEN) + } + } + else if (Manifest.permission.WRITE_EXTERNAL_STORAGE == result.permission) { + textFileState.text = granted + textFileState.setTextColor(Color.GREEN) + if (isRequest) { + textFileResult.text = granted + textFileResult.setTextColor(Color.GREEN) + } + } + else if (Manifest.permission.CAMERA == result.permission) { + textCameraState.text = granted + textCameraState.setTextColor(Color.GREEN) + if (isRequest) { + textCameraResult.text = granted + textCameraResult.setTextColor(Color.GREEN) + } + } + else if (Manifest.permission.READ_CONTACTS == result.permission) { + textContactsState.text = granted + textContactsState.setTextColor(Color.GREEN) + if (isRequest) { + textContactsResult.text = granted + textContactsResult.setTextColor(Color.GREEN) + } + } + } else if (result is PermissionResult.Denied) { + if (Manifest.permission.ACCESS_FINE_LOCATION == result.permission) { + textLocationState.text = deniedReasonText(result) + textLocationState.setTextColor(Color.RED) + if (isRequest) { + textLocationResult.text = deniedReasonText(result) + textLocationResult.setTextColor(Color.RED) + } + } + else if (Manifest.permission.WRITE_EXTERNAL_STORAGE == result.permission) { + textFileState.text = deniedReasonText(result) + textFileState.setTextColor(Color.RED) + if (isRequest) { + textFileResult.text = deniedReasonText(result) + textFileResult.setTextColor(Color.RED) + } + } + else if (Manifest.permission.CAMERA == result.permission) { + textCameraState.text = deniedReasonText(result) + textCameraState.setTextColor(Color.RED) + if (isRequest) { + textCameraResult.text = deniedReasonText(result) + textCameraResult.setTextColor(Color.RED) + } + } + else if (Manifest.permission.READ_CONTACTS == result.permission) { + textContactsState.text = deniedReasonText(result) + textContactsState.setTextColor(Color.RED) + if (isRequest) { + textContactsResult.text = deniedReasonText(result) + textContactsResult.setTextColor(Color.RED) + } + } + } else if (result is PermissionResult.NeverAskedOrDeniedPermanently) { + val condition = "Never Asked Or Denied Permanently" + + if (Manifest.permission.ACCESS_FINE_LOCATION == result.permission) { + textLocationState.text = condition + textLocationState.setTextColor(Color.BLACK) + } + else if (Manifest.permission.WRITE_EXTERNAL_STORAGE == result.permission) { + textFileState.text = condition + textFileState.setTextColor(Color.BLACK) + } + else if (Manifest.permission.CAMERA == result.permission) { + textCameraState.text = condition + textCameraState.setTextColor(Color.BLACK) + } + else if (Manifest.permission.READ_CONTACTS == result.permission) { + textContactsState.text = condition + textContactsState.setTextColor(Color.BLACK) + } + + } else if (result is PermissionResult.Cancelled) { + textLocationState.text = cancelled + textLocationState.setTextColor(Color.RED) + textFileState.text = cancelled + textFileState.setTextColor(Color.RED) + textCameraState.text = cancelled + textCameraState.setTextColor(Color.RED) + textContactsState.text = cancelled + textContactsState.setTextColor(Color.RED) + if (isRequest) { + textLocationResult.text = cancelled + textLocationResult.setTextColor(Color.RED) + textFileResult.text = cancelled + textFileResult.setTextColor(Color.RED) + textCameraResult.text = cancelled + textCameraResult.setTextColor(Color.RED) + textContactsResult.text = cancelled + textContactsResult.setTextColor(Color.RED) + } + } + } private fun deniedReasonText(result: PermissionResult): String { return when (result) { diff --git a/examples/src/main/java/com/markodevcic/samples/MainViewModel.kt b/examples/src/main/java/com/markodevcic/samples/MainViewModel.kt index 0f2067f..84c9bec 100644 --- a/examples/src/main/java/com/markodevcic/samples/MainViewModel.kt +++ b/examples/src/main/java/com/markodevcic/samples/MainViewModel.kt @@ -14,6 +14,9 @@ class MainViewModel(private val permissionRequester: PermissionRequester) : View val liveData = MutableLiveData() + private val _permissionStateChannel : Channel = Channel() + val permissionStateFlow = _permissionStateChannel.receiveAsFlow() + private val permissionChannel: Channel = Channel() val permissionsFlow: Flow = permissionChannel.receiveAsFlow() @@ -33,6 +36,16 @@ class MainViewModel(private val permissionRequester: PermissionRequester) : View } } + fun permissionState(vararg permission: String) { + viewModelScope.launch { + permissionRequester.checkPermissionsState(*permission) + .onEach { + _permissionStateChannel.send(it) + } + .collect() + } + } + suspend fun isPermissionGranted(permission: String): Boolean { return permissionRequester.request(permission) .first() is PermissionResult.Granted diff --git a/examples/src/main/res/layout/activity_main.xml b/examples/src/main/res/layout/activity_main.xml index 53e9974..f93e468 100644 --- a/examples/src/main/res/layout/activity_main.xml +++ b/examples/src/main/res/layout/activity_main.xml @@ -21,7 +21,9 @@ app:popupTheme="@style/AppTheme.PopupOverlay" /> - + + + +