Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,6 +66,30 @@ launch {
}
```

Need to inspect a permission's current state **before requesting** it? Use `checkPermissionsState()`.
It returns a `Flow<PermissionState>` **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.

Expand All @@ -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?
Expand Down
8 changes: 1 addition & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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/' }
}
}

Expand Down
214 changes: 156 additions & 58 deletions examples/src/main/java/com/markodevcic/samples/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand All @@ -23,23 +24,29 @@ 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)
setSupportActionBar(toolbar)

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)
Expand All @@ -49,66 +56,154 @@ 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()
}
}

private fun requestPermission(vararg permissions: String) {
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) {
Expand All @@ -118,6 +213,7 @@ class MainActivity : AppCompatActivity() {
}
}


override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
Expand All @@ -130,3 +226,5 @@ class MainActivity : AppCompatActivity() {
}
}
}

private const val cancelled = "CANCELLED"
14 changes: 14 additions & 0 deletions examples/src/main/java/com/markodevcic/samples/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,6 +15,9 @@ class MainViewModel(private val permissionRequester: PermissionRequester) : View

val liveData = MutableLiveData<PermissionResult>()

private val _permissionStateChannel : Channel<PermissionState> = Channel()
val permissionStateFlow = _permissionStateChannel.receiveAsFlow()

private val permissionChannel: Channel<PermissionResult> = Channel()

val permissionsFlow: Flow<PermissionResult> = permissionChannel.receiveAsFlow()
Expand All @@ -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
Expand Down
Loading