From 52da812332019f42475858d32674bf5dc104205f Mon Sep 17 00:00:00 2001 From: Bahman Bazoo Date: Sun, 2 Feb 2025 16:07:19 +0330 Subject: [PATCH 1/3] Refactor to be able to check permission state in detail without requesting permission + naming: + note: PermissionRequester remains as it was, for backward compatibility. - NativeRequester -> NativeActivity - NativeRequesterFactory -> NativeActivityFactory - getRequesterAsync -> getActivityAsync - PermissionRequest -> PermissionState - PermissionRequestBuilder -> PermissionStateBuilder - PekoPermissionRequestBuilder -> PekoPermissionStateBuilder - PekoPermissionRequester -> PekoPermissionManager + permissionState() added to PermissionRequester. + nativeActivity cached to make available handling of permission requests and permission state checking Simultaneously. + As, NativeActivity is ot necessarily disposable, PermissionManager sends Channel and requestId dynamically. - channel in PekoViewModel replaced with map of request id and its channel. + PermissionResult now has data class NeverAskedOrDeniedPermanently for checking permissions without request. --- .../com/markodevcic/peko/NativeActivity.kt | 9 ++ ...terFactory.kt => NativeActivityFactory.kt} | 14 +- .../com/markodevcic/peko/NativeRequester.kt | 9 -- .../java/com/markodevcic/peko/PekoActivity.kt | 50 ++++--- .../com/markodevcic/peko/PekoViewModel.kt | 15 ++- .../markodevcic/peko/PermissionRequester.kt | 123 ++++++++++++++---- .../com/markodevcic/peko/PermissionResult.kt | 8 +- ...ermissionRequest.kt => PermissionState.kt} | 2 +- ...stBuilder.kt => PermissionStateBuilder.kt} | 12 +- .../peko/PermissionRequesterTest.kt | 29 ++--- 10 files changed, 192 insertions(+), 79 deletions(-) create mode 100644 peko/src/main/java/com/markodevcic/peko/NativeActivity.kt rename peko/src/main/java/com/markodevcic/peko/{NativeRequesterFactory.kt => NativeActivityFactory.kt} (60%) delete mode 100644 peko/src/main/java/com/markodevcic/peko/NativeRequester.kt rename peko/src/main/java/com/markodevcic/peko/{PermissionRequest.kt => PermissionState.kt} (71%) rename peko/src/main/java/com/markodevcic/peko/{PermissionRequestBuilder.kt => PermissionStateBuilder.kt} (50%) diff --git a/peko/src/main/java/com/markodevcic/peko/NativeActivity.kt b/peko/src/main/java/com/markodevcic/peko/NativeActivity.kt new file mode 100644 index 0000000..3a3cbce --- /dev/null +++ b/peko/src/main/java/com/markodevcic/peko/NativeActivity.kt @@ -0,0 +1,9 @@ +package com.markodevcic.peko + +import kotlinx.coroutines.channels.Channel + +internal interface NativeActivity { + fun requestPermissions(permissions: Array, channel: Pair>) + fun checkStateOfDeniedPermissions(permissions: Array, channel: Channel) + fun finish() +} \ No newline at end of file diff --git a/peko/src/main/java/com/markodevcic/peko/NativeRequesterFactory.kt b/peko/src/main/java/com/markodevcic/peko/NativeActivityFactory.kt similarity index 60% rename from peko/src/main/java/com/markodevcic/peko/NativeRequesterFactory.kt rename to peko/src/main/java/com/markodevcic/peko/NativeActivityFactory.kt index b2bc2e7..8b8a59b 100644 --- a/peko/src/main/java/com/markodevcic/peko/NativeRequesterFactory.kt +++ b/peko/src/main/java/com/markodevcic/peko/NativeActivityFactory.kt @@ -6,17 +6,17 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import java.util.concurrent.ThreadLocalRandom -internal interface NativeRequesterFactory { - fun getRequesterAsync(context: Context, vararg permissions: String): Deferred +internal interface NativeActivityFactory { + fun getActivityAsync(context: Context): Deferred companion object { - fun default(): NativeRequesterFactory = NativeRequesterFactoryImpl() + fun default(): NativeActivityFactory = NativeActivityFactoryImpl() } } -private class NativeRequesterFactoryImpl : NativeRequesterFactory { - override fun getRequesterAsync(context: Context, vararg permissions: String): Deferred { - val completableDeferred = CompletableDeferred() +private class NativeActivityFactoryImpl : NativeActivityFactory { + override fun getActivityAsync(context: Context): Deferred { + val completableDeferred = CompletableDeferred() val requestId = getRequestId() PekoActivity.idToRequesterMap[requestId] = completableDeferred val intent = Intent(context, PekoActivity::class.java) @@ -30,4 +30,4 @@ private class NativeRequesterFactoryImpl : NativeRequesterFactory { val random = ThreadLocalRandom.current().nextInt(Int.MAX_VALUE) return random.hashCode().toString() } -} +} \ No newline at end of file diff --git a/peko/src/main/java/com/markodevcic/peko/NativeRequester.kt b/peko/src/main/java/com/markodevcic/peko/NativeRequester.kt deleted file mode 100644 index 35bc133..0000000 --- a/peko/src/main/java/com/markodevcic/peko/NativeRequester.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.markodevcic.peko - -import kotlinx.coroutines.channels.ReceiveChannel - -internal interface NativeRequester { - fun requestPermissions(permissions: Array) - fun finish() - val resultsChannel: ReceiveChannel -} diff --git a/peko/src/main/java/com/markodevcic/peko/PekoActivity.kt b/peko/src/main/java/com/markodevcic/peko/PekoActivity.kt index b6a01bd..d418896 100644 --- a/peko/src/main/java/com/markodevcic/peko/PekoActivity.kt +++ b/peko/src/main/java/com/markodevcic/peko/PekoActivity.kt @@ -7,18 +7,15 @@ import androidx.core.content.PermissionChecker import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelProvider import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.Channel import java.util.concurrent.ConcurrentHashMap internal class PekoActivity : FragmentActivity(), ActivityCompat.OnRequestPermissionsResultCallback, - NativeRequester { + NativeActivity { private lateinit var viewModel: PekoViewModel - override val resultsChannel: ReceiveChannel - get() = viewModel.channel - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) @@ -33,13 +30,35 @@ internal class PekoActivity : FragmentActivity(), completableDeferred?.complete(this) } - override fun requestPermissions(permissions: Array) { - ActivityCompat.requestPermissions(this@PekoActivity, permissions, REQUEST_CODE) + override fun requestPermissions(permissions: Array, channel: Pair>) { + viewModel.putChannel(channel) + ActivityCompat.requestPermissions(this@PekoActivity, permissions, channel.first) + } + + override fun checkStateOfDeniedPermissions(permissions: Array,channel: Channel) { + if (permissions.isEmpty()){ + channel.trySend(PermissionResult.Cancelled) + channel.close() + } + val needRationalePermissions = permissions + .filter { p -> ActivityCompat.shouldShowRequestPermissionRationale(this@PekoActivity,p) } + val permanentlyDeniedPermissions = permissions + .filter { p -> !needRationalePermissions.contains(p) } + + needRationalePermissions.forEach { permission -> + channel.trySend(PermissionResult.Denied.NeedsRationale(permission)) + } + permanentlyDeniedPermissions.forEach { permission -> + channel.trySend(PermissionResult.NeverAskedOrDeniedPermanently(permission)) + } + channel.close() + } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == REQUEST_CODE) { + val channel = viewModel.getChannel(requestCode) + if (channel != null) { val grantedPermissions = mutableSetOf() val deniedPermissions = mutableSetOf() for (i in permissions.indices) { @@ -55,29 +74,28 @@ internal class PekoActivity : FragmentActivity(), deniedPermissions.filter { p -> ActivityCompat.shouldShowRequestPermissionRationale(this, p) } val doNotAskAgainPermissions = deniedPermissions.filter { p -> !needsRationalePermissions.contains(p) } if (permissions.isEmpty()) { - viewModel.channel.trySend(PermissionResult.Cancelled) + channel.trySend(PermissionResult.Cancelled) } else { for (p in grantedPermissions) { - viewModel.channel.trySend(PermissionResult.Granted(p)) + channel.trySend(PermissionResult.Granted(p)) } for (p in needsRationalePermissions) { - viewModel.channel.trySend(PermissionResult.Denied.NeedsRationale(p)) + channel.trySend(PermissionResult.Denied.NeedsRationale(p)) } for (p in doNotAskAgainPermissions) { - viewModel.channel.trySend(PermissionResult.Denied.DeniedPermanently(p)) + channel.trySend(PermissionResult.Denied.DeniedPermanently(p)) } } - viewModel.channel.close() + channel.close() } } override fun finish() { super.finish() - viewModel.channel.close() + viewModel.closeAllChannels() } companion object { - private const val REQUEST_CODE = 931 - internal var idToRequesterMap = ConcurrentHashMap>() + internal var idToRequesterMap = ConcurrentHashMap>() } } \ No newline at end of file diff --git a/peko/src/main/java/com/markodevcic/peko/PekoViewModel.kt b/peko/src/main/java/com/markodevcic/peko/PekoViewModel.kt index 139c97f..5f64a17 100644 --- a/peko/src/main/java/com/markodevcic/peko/PekoViewModel.kt +++ b/peko/src/main/java/com/markodevcic/peko/PekoViewModel.kt @@ -2,12 +2,23 @@ package com.markodevcic.peko import androidx.lifecycle.ViewModel import kotlinx.coroutines.channels.Channel +import java.util.concurrent.ConcurrentHashMap internal class PekoViewModel : ViewModel() { - val channel = Channel(Channel.UNLIMITED) + private val channels = ConcurrentHashMap>() + + fun putChannel(channel: Pair>) = channels.put(channel.first, channel.second) + + fun getChannel(executor: Int): Channel? = channels.remove(executor) + + fun closeAllChannels() { + channels.forEach { channel -> + channel.value.close() + } + } override fun onCleared() { super.onCleared() - channel.close() + closeAllChannels() } } \ No newline at end of file diff --git a/peko/src/main/java/com/markodevcic/peko/PermissionRequester.kt b/peko/src/main/java/com/markodevcic/peko/PermissionRequester.kt index bebf798..8a2cf38 100644 --- a/peko/src/main/java/com/markodevcic/peko/PermissionRequester.kt +++ b/peko/src/main/java/com/markodevcic/peko/PermissionRequester.kt @@ -2,10 +2,14 @@ package com.markodevcic.peko import android.app.Activity import android.content.Context -import com.markodevcic.peko.PermissionRequester.Companion.instance import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import kotlin.random.Random /** * Interface for requesting or checking if permissions are granted. @@ -25,6 +29,12 @@ interface PermissionRequester { */ fun isAnyGranted(vararg permissions: String): Boolean + /** + * checks and returns state of all permissions + * @return [Flow] + */ + fun permissionsState(vararg permissions: String): Flow + /** * Starts the permission request flow. * The result is a [Flow] of [PermissionResult] for each permission requested. @@ -47,53 +57,122 @@ interface PermissionRequester { private var appContext: Context? = null - internal var requesterFactory = NativeRequesterFactory.default() - internal var requestBuilder = PermissionRequestBuilder.default() + internal var activityFactory = NativeActivityFactory.default() + internal var permissionStateBuilder = PermissionStateBuilder.default() + /** * Default Peko implementation of the [PermissionRequester] * @return [PermissionRequester] */ - fun instance(): PermissionRequester = PekoPermissionRequester(requesterFactory, requestBuilder) + fun instance(): PermissionRequester = PekoPermissionManager(activityFactory, permissionStateBuilder) } - private class PekoPermissionRequester( - private val requesterFactory: NativeRequesterFactory, - private val permissionRequestBuilder: PermissionRequestBuilder + private class PekoPermissionManager ( + private val activityFactory: NativeActivityFactory, + private val permissionStateBuilder: PermissionStateBuilder ) : PermissionRequester { + var nativeActivity: NativeActivity? = null + private val nativeActivityExecutors: MutableSet = mutableSetOf() + private val nativeActivityMutex = Mutex() // Mutex for synchronization + + suspend fun initNativeActivity(): Int { + nativeActivityMutex.withLock { + var executorId: Int + do { + executorId = Random.nextInt(0, Int.MAX_VALUE) + } while (nativeActivityExecutors.contains(executorId)) + nativeActivityExecutors.add(executorId) + + if (nativeActivity == null) { + nativeActivity = withContext(Dispatchers.Main) { + activityFactory.getActivityAsync(requireContext()).await() + } + } + + return executorId + } + } + + suspend fun finishNativeActivity(executorId:Int) { + nativeActivityMutex.withLock{ + nativeActivityExecutors.remove(executorId) + if (nativeActivityExecutors.isEmpty()){ + nativeActivity?.finish() + nativeActivity = null + } + } + } override fun areGranted(vararg permissions: String): Boolean { - val request = permissionRequestBuilder.createPermissionRequest(requireContext(), *permissions) - return request.denied.isEmpty() + val permissionState = permissionStateBuilder.createPermissionState(requireContext(), *permissions) + return permissionState.denied.isEmpty() } override fun request(vararg permissions: String): Flow { - val request = permissionRequestBuilder.createPermissionRequest(requireContext(), *permissions) + val permissionState = permissionStateBuilder.createPermissionState(requireContext(), *permissions) val flow = channelFlow { - for (granted in request.granted) { + for (granted in permissionState.granted) { trySend(PermissionResult.Granted(granted)) } - if (request.denied.isNotEmpty()) { - val requester = withContext(Dispatchers.Main) { - requesterFactory.getRequesterAsync(requireContext()).await() - } - requester.requestPermissions(request.denied.toTypedArray()) - for (result in requester.resultsChannel) { + if (permissionState.denied.isNotEmpty()) { + val requestId = initNativeActivity() + val channel = Channel(Channel.UNLIMITED) + val resultsChannel : ReceiveChannel = channel + nativeActivity!!.requestPermissions( + permissionState.denied.toTypedArray(), + Pair(requestId,channel) + ) + for (result in resultsChannel) { trySend(result) } - requester.finish() - channel.close() + + finishNativeActivity(requestId) + this.close() } else { - channel.close() + this.close() } } return flow } override fun isAnyGranted(vararg permissions: String): Boolean { - val request = permissionRequestBuilder.createPermissionRequest(requireContext(), *permissions) - return permissions.isNotEmpty() && request.granted.isNotEmpty() + val permissionState = permissionStateBuilder.createPermissionState(requireContext(), *permissions) + return permissions.isNotEmpty() && permissionState.granted.isNotEmpty() + } + + override fun permissionsState(vararg permissions: String): Flow { + return if (permissions.isEmpty()) { + flowOf(PermissionResult.Cancelled) + } else { + val permissionList = permissions.toMutableList() + val permissionState = permissionStateBuilder + .createPermissionState(requireContext(), *permissions) + return channelFlow { + permissionState.granted.forEach { granted -> + trySend(PermissionResult.Granted(granted)) + } + + if (permissionState.denied.isNotEmpty()) { + val checkStateId = initNativeActivity() + val channel = Channel(Channel.UNLIMITED) + val receiverChannel: ReceiveChannel = channel + nativeActivity!!.checkStateOfDeniedPermissions( + permissionState.denied.toTypedArray(), + channel + ) + for (state in receiverChannel) { + trySend(state) + } + + finishNativeActivity(checkStateId) + this.close() + } else { + this.close() + } + } + } } private fun requireContext() = diff --git a/peko/src/main/java/com/markodevcic/peko/PermissionResult.kt b/peko/src/main/java/com/markodevcic/peko/PermissionResult.kt index 4281d25..1977961 100644 --- a/peko/src/main/java/com/markodevcic/peko/PermissionResult.kt +++ b/peko/src/main/java/com/markodevcic/peko/PermissionResult.kt @@ -33,9 +33,15 @@ sealed class PermissionResult { data class DeniedPermanently(override val permission: String) : Denied(permission) } + /** + * Represents a permission, not granted nor denied + * @param permission, the permission which is untouched + */ + data class NeverAskedOrDeniedPermanently(val permission: String) : PermissionResult() + /** * Represents a permission request that was cancelled. * It is safe to repeat the request. */ object Cancelled : PermissionResult() -} +} \ No newline at end of file diff --git a/peko/src/main/java/com/markodevcic/peko/PermissionRequest.kt b/peko/src/main/java/com/markodevcic/peko/PermissionState.kt similarity index 71% rename from peko/src/main/java/com/markodevcic/peko/PermissionRequest.kt rename to peko/src/main/java/com/markodevcic/peko/PermissionState.kt index 0d91102..3b6feff 100644 --- a/peko/src/main/java/com/markodevcic/peko/PermissionRequest.kt +++ b/peko/src/main/java/com/markodevcic/peko/PermissionState.kt @@ -1,6 +1,6 @@ package com.markodevcic.peko -internal class PermissionRequest( +internal class PermissionState( val granted: List, val denied: List ) \ No newline at end of file diff --git a/peko/src/main/java/com/markodevcic/peko/PermissionRequestBuilder.kt b/peko/src/main/java/com/markodevcic/peko/PermissionStateBuilder.kt similarity index 50% rename from peko/src/main/java/com/markodevcic/peko/PermissionRequestBuilder.kt rename to peko/src/main/java/com/markodevcic/peko/PermissionStateBuilder.kt index 374966e..278ca11 100644 --- a/peko/src/main/java/com/markodevcic/peko/PermissionRequestBuilder.kt +++ b/peko/src/main/java/com/markodevcic/peko/PermissionStateBuilder.kt @@ -4,20 +4,20 @@ import android.content.Context import android.content.pm.PackageManager import androidx.core.content.ContextCompat -internal interface PermissionRequestBuilder { - fun createPermissionRequest(context: Context, vararg permissions: String): PermissionRequest +internal interface PermissionStateBuilder { + fun createPermissionState(context: Context, vararg permissions: String): PermissionState - private class PekoPermissionRequestBuilder : PermissionRequestBuilder { - override fun createPermissionRequest(context: Context, vararg permissions: String): PermissionRequest { + private class PekoPermissionStateBuilder : PermissionStateBuilder { + override fun createPermissionState(context: Context, vararg permissions: String): PermissionState { val permissionsGroup = permissions.groupBy { p -> ContextCompat.checkSelfPermission(context, p) } val denied = permissionsGroup[PackageManager.PERMISSION_DENIED] ?: listOf() val granted = permissionsGroup[PackageManager.PERMISSION_GRANTED] ?: listOf() - return PermissionRequest(granted, denied) + return PermissionState(granted, denied) } } companion object { - fun default(): PermissionRequestBuilder = PekoPermissionRequestBuilder() + fun default(): PermissionStateBuilder = PekoPermissionStateBuilder() } } \ No newline at end of file diff --git a/peko/src/test/java/com/markodevcic/peko/PermissionRequesterTest.kt b/peko/src/test/java/com/markodevcic/peko/PermissionRequesterTest.kt index 7e3e897..b221874 100644 --- a/peko/src/test/java/com/markodevcic/peko/PermissionRequesterTest.kt +++ b/peko/src/test/java/com/markodevcic/peko/PermissionRequesterTest.kt @@ -12,10 +12,10 @@ import org.junit.Test import org.mockito.Mockito class PermissionRequesterTest { - private val requesterFactory = Mockito.mock(NativeRequesterFactory::class.java) + private val activityFactory = Mockito.mock(NativeActivityFactory::class.java) private val context = Mockito.mock(Context::class.java) - private val nativeRequester = Mockito.mock(NativeRequester::class.java) - private val requestBuilder = Mockito.mock(PermissionRequestBuilder::class.java) + private val nativeActivity = Mockito.mock(NativeActivity::class.java) + private val permissionStateBuilder = Mockito.mock(PermissionStateBuilder::class.java) private lateinit var permissionChannel: Channel @@ -25,12 +25,11 @@ class PermissionRequesterTest { fun setup() { permissionChannel = Channel() - PermissionRequester.requesterFactory = requesterFactory - PermissionRequester.requestBuilder = requestBuilder + PermissionRequester.activityFactory = activityFactory + PermissionRequester.permissionStateBuilder = permissionStateBuilder PermissionRequester.initialize(context) - Mockito.`when`(requesterFactory.getRequesterAsync(context)).thenReturn(CompletableDeferred(nativeRequester)) - Mockito.`when`(nativeRequester.resultsChannel).thenReturn(permissionChannel) + Mockito.`when`(activityFactory.getActivityAsync(context)).thenReturn(CompletableDeferred(nativeActivity)) sut = PermissionRequester.instance() } @@ -39,8 +38,8 @@ class PermissionRequesterTest { fun testGranted() { val permission = "CONTACTS" - Mockito.`when`(requestBuilder.createPermissionRequest(context, permission)).thenReturn( - PermissionRequest( + Mockito.`when`(permissionStateBuilder.createPermissionState(context, permission)).thenReturn( + PermissionState( listOf(), listOf( permission ) @@ -62,8 +61,8 @@ class PermissionRequesterTest { fun testAlreadyGranted() { val permission = "CONTACTS" - Mockito.`when`(requestBuilder.createPermissionRequest(context, permission)).thenReturn( - PermissionRequest( + Mockito.`when`(permissionStateBuilder.createPermissionState(context, permission)).thenReturn( + PermissionState( listOf(permission), listOf() ) ) @@ -79,8 +78,8 @@ class PermissionRequesterTest { val denied = "COARSE_LOCATION" val granted = "FUSED_LOCATION" - Mockito.`when`(requestBuilder.createPermissionRequest(context, denied, granted)).thenReturn( - PermissionRequest( + Mockito.`when`(permissionStateBuilder.createPermissionState(context, denied, granted)).thenReturn( + PermissionState( granted = listOf(granted), denied = listOf(denied) ) @@ -96,8 +95,8 @@ class PermissionRequesterTest { val denied = "COARSE_LOCATION" val granted = "FUSED_LOCATION" - Mockito.`when`(requestBuilder.createPermissionRequest(context, denied, granted)).thenReturn( - PermissionRequest( + Mockito.`when`(permissionStateBuilder.createPermissionState(context, denied, granted)).thenReturn( + PermissionState( granted = listOf(granted), denied = listOf(denied) ) From 0ee1c945ab4229756b58c8b719e79d45920564e7 Mon Sep 17 00:00:00 2001 From: Bahman Bazoo Date: Tue, 22 Apr 2025 09:38:27 +0330 Subject: [PATCH 2/3] update peko version to 3.1.0 + example updated --- .../com/markodevcic/samples/MainActivity.kt | 195 +++++++++++++----- .../com/markodevcic/samples/MainViewModel.kt | 13 ++ .../src/main/res/layout/activity_main.xml | 109 +++++++++- peko/build.gradle | 2 +- 4 files changed, 265 insertions(+), 54 deletions(-) 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..77527f7 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.permissionsState(*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" /> - + + + +