diff --git a/README.md b/README.md index ee0928d..707b0c7 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,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 @@ -66,6 +66,30 @@ launch { } ``` +Need to inspect a permission's current state **before requesting** it? Use `checkPermissionsState()`. +It returns a `Flow` **without triggering the system permission dialog**. + +This is useful for handling sensitive UX flows or deciding whether to show a rationale before requesting permission. + +```kotlin +launch { + requester.checkPermissionsState( + Manifest.permission.CAMERA, + Manifest.permission.READ_CONTACTS + ).collect { p -> + when (p) { + is PermissionState.Granted -> print("${p.permission} granted") // already granted + is PermissionState.NeedsRationale -> print("${p.permission} needs rationale") // show rationale + is PermissionState.NeverAskedOrDeniedPermanently -> print("${p.permission} never asked or permanently denied") // ambiguous state + } + } +} +``` +⚠️ `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 is planned to 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. @@ -75,7 +99,7 @@ val granted: Boolean = requester.areGranted(Manifest.permission.CAMERA, Manifest ``` Or are any of the requested granted? ```kotlin -val anyGranted: Boolean = requester.anyGranted(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS) +val anyGranted: Boolean = requester.isAnyGranted(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS) ``` ### Why Flows? diff --git a/build.gradle b/build.gradle index 3d7d0af..2bfc1af 100644 --- a/build.gradle +++ b/build.gradle @@ -6,12 +6,9 @@ buildscript { repositories { google() mavenCentral() - maven { url 'https://jitpack.io' } - maven { url 'https://plugins.gradle.org/m2/' } - maven { url 'https://groovy.jfrog.io/artifactory/libs-release/' } } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0" classpath "com.github.dcendents:android-maven-gradle-plugin:2.1" @@ -22,9 +19,6 @@ allprojects { repositories { google() mavenCentral() - maven { url 'https://jitpack.io' } - maven { url 'https://plugins.gradle.org/m2/' } - maven { url 'https://groovy.jfrog.io/artifactory/libs-release/' } } } diff --git a/examples/src/main/java/com/markodevcic/samples/MainActivity.kt b/examples/src/main/java/com/markodevcic/samples/MainActivity.kt index 8091e8e..83fde85 100644 --- a/examples/src/main/java/com/markodevcic/samples/MainActivity.kt +++ b/examples/src/main/java/com/markodevcic/samples/MainActivity.kt @@ -10,9 +10,10 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import com.markodevcic.peko.PermissionRequester import com.markodevcic.peko.PermissionResult +import com.markodevcic.peko.PermissionState +import com.markodevcic.peko.allGranted import kotlinx.android.synthetic.main.activity_main.* - -private const val cancelled = "CANCELLED" +import kotlinx.coroutines.launch class MainActivity : AppCompatActivity() { @@ -23,8 +24,8 @@ class MainActivity : AppCompatActivity() { PermissionRequester.initialize(applicationContext) viewModel = ViewModelProvider( - this@MainActivity, - MainViewModelFactory(PermissionRequester.instance()) + this@MainActivity, + MainViewModelFactory(PermissionRequester.instance()) )[MainViewModel::class.java] setContentView(R.layout.activity_main) @@ -32,14 +33,20 @@ class MainActivity : AppCompatActivity() { lifecycleScope.launchWhenStarted { viewModel.permissionsFlow - .collect { setResult(it) } + .collect { setResult(it) } } + lifecycleScope.launchWhenStarted { + viewModel.permissionStateFlow.collect { + setState(it) + } + } + 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) @@ -49,11 +56,38 @@ class MainActivity : AppCompatActivity() { } btnAll.setOnClickListener { viewModel.requestPermissions( - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.CAMERA, - Manifest.permission.ACCESS_BACKGROUND_LOCATION, - Manifest.permission.READ_CONTACTS + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.CAMERA, + 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 { + val allGranted = viewModel.flowPermissions(*permissions).allGranted() } } @@ -61,54 +95,115 @@ 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) { + if (result is PermissionResult.Granted) { + + val granted = "GRANTED" + if (Manifest.permission.ACCESS_FINE_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 setState(result: PermissionState) { + if (result is PermissionState.Granted) { + + val granted = "GRANTED" + if (Manifest.permission.ACCESS_FINE_LOCATION == result.permission) { + textLocationState.text = granted + textLocationState.setTextColor(Color.GREEN) + } + else if (Manifest.permission.WRITE_EXTERNAL_STORAGE == result.permission) { + textFileState.text = granted + textFileState.setTextColor(Color.GREEN) + } + else if (Manifest.permission.CAMERA == result.permission) { + textCameraState.text = granted + textCameraState.setTextColor(Color.GREEN) + } + else if (Manifest.permission.READ_CONTACTS == result.permission) { + textContactsState.text = granted + textContactsState.setTextColor(Color.GREEN) + } + } else if (result is PermissionState.NeedsRationale) { + val denied = "NEEDS RATIONALE" + if (Manifest.permission.ACCESS_FINE_LOCATION == result.permission) { + textLocationState.text = denied + textLocationState.setTextColor(Color.RED) + } + else if (Manifest.permission.WRITE_EXTERNAL_STORAGE == result.permission) { + textFileState.text = denied + textFileState.setTextColor(Color.RED) + } + else if (Manifest.permission.CAMERA == result.permission) { + textCameraState.text = denied + textCameraState.setTextColor(Color.RED) + } + else if (Manifest.permission.READ_CONTACTS == result.permission) { + textContactsState.text = denied + textContactsState.setTextColor(Color.RED) + } + } else if (result is PermissionState.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) + } + + } + } private fun deniedReasonText(result: PermissionResult): String { return when (result) { @@ -118,6 +213,7 @@ class MainActivity : AppCompatActivity() { } } + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_main, menu) return true @@ -130,3 +226,5 @@ class MainActivity : AppCompatActivity() { } } } + +private const val cancelled = "CANCELLED" \ No newline at end of file diff --git a/examples/src/main/java/com/markodevcic/samples/MainViewModel.kt b/examples/src/main/java/com/markodevcic/samples/MainViewModel.kt index 4016ee5..fbbd0dc 100644 --- a/examples/src/main/java/com/markodevcic/samples/MainViewModel.kt +++ b/examples/src/main/java/com/markodevcic/samples/MainViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.markodevcic.peko.PermissionRequester import com.markodevcic.peko.PermissionResult +import com.markodevcic.peko.PermissionState import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -14,6 +15,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 +37,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" /> - + + + +