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
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,6 +70,31 @@ launch {
}
```

Need to inspect the current permission state **before requesting** anything? Use `checkPermissionState()`.
It returns a `Flow<PermissionResult>` 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.

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down
195 changes: 143 additions & 52 deletions examples/src/main/java/com/markodevcic/samples/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions examples/src/main/java/com/markodevcic/samples/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class MainViewModel(private val permissionRequester: PermissionRequester) : View

val liveData = MutableLiveData<PermissionResult>()

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

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

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