Android Permission Manager

Bayar Şahintekin
SisalDigitalHubTurkiye
8 min readNov 27, 2023

Hello world.

Introduction: In the vast realm of Android app development, where innovation meets user experience, one integral aspect stands as the guardian of privacy and security: Permission Management. Navigating the Android permission system is not merely a procedural requirement but a crucial element in fostering user trust and creating responsible, user-centric applications. In this exploration of the Android seas, we’ll embark on a journey into the world of permission management — a voyage essential for every developer aiming to build applications that respect user privacy and deliver seamless functionality. Let’s set sail into the intricacies of Android’s Permission Manager and unravel the secrets of responsible app development.

In this article, my objective is to present the most streamlined approach for managing Android permissions — a method that I find particularly clear and efficient (at least from my perspective).

Let’s see some code

This is the simple usage of the permission manager.

// RequestPermissionLauncher needs lifecycleowner as constructor parameter
// lifecycleowner must be Activity or Fragment.
private val requestPermissionLauncher = RequestPermissionLauncher.from(lifecycleOwner = this)
requestPermissionLauncher.launch(
arrayListOf(
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION),
onShowRational = {
showPermissionRationaleDialog(
title = "Rationale",
description = "You need to give permission to go on"
)
}, onPermanentlyDenied = {
showPermanentlyDeniedPermissionDialog(
title = "Permanently Denied",
description = "You need to open settings and give permission to go on"
)
},
onResult = { permissionsCurrentState ->
// We can see permissions list states on each state.
}
)
  • Create instance of RequestPermissionLauncher (You will see what is that)
  • launch() : We prepare list includes permissions which we want to request and get permission. We send list with this method.
  • onShowRational { } : When user denied the permission(s) for the first time, we need to show user a dialog. That callback helps us.
  • onPermanentlyDenied { } : When user denied the permission(s) twice, that permission(s) state will be permanently denied. We need to show a dialog to inform user has to give permission from phone settings.
  • onResult{ } : With this callback, we see the status of the permission request to obtain at every step. (GRANTED, DENIED, PERMANENTLY_DENIED)

This is the example usage of PermissionManager. We can create an instance regardless of whether it is a Fragment or Activity.

Let’s dive a little deeper and see some code:

  1. PermissionResult.class

We need an enum class to put permission states

enum class PermissionResult {
GRANTED, DENIED, PERMANENTLY_DENIED
}

2. PermissionData.class

We need a data class to handle permissions and their states.

data class PermissionData(var permission: String, var state: PermissionResult)

3. Having an activity object

We need to activity object to run permission process. But we are working with lifecycle. So need to decide which reference we get.

// Activity object
private var activity: Activity? = null
// This object comes from Activity Result API.
// We need this object to request permission and handle the result.
private lateinit var permissionCheck: ActivityResultLauncher<Array<String>>
// We need Shared Preferences to know user denied the permission once or twice.
// If user denied once, we will show rationale dialog
// If user denied twice or more, the permission is permanently denied.
private lateinit var preferences: SharedPreferences
override fun onCreate(owner: LifecycleOwner) {
permissionCheck = when (owner) {
is AppCompatActivity -> {
owner.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
this
)
}
is ComponentActivity -> {
owner.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
this
)
}
else -> {
(owner as Fragment).registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
this
)
}
}
activity = when (lifecycleOwner.get()) {
is Fragment -> {
(lifecycleOwner.get() as? Fragment)?.context?.scanForActivity()
}
is ComponentActivity -> {
lifecycleOwner.get() as ComponentActivity
}
else -> {
lifecycleOwner.get() as? AppCompatActivity
}
}
activity?.applicationContext?.let {
preferences = PreferencesHandler.build(it)
}
super.onCreate(owner)
}

With this way we decide which reference we have (Fragment or Activity).

4. Request permission:

This is the main process of the permission handling. We are managing permission is granted, denied or permanently denied and inform caller permission result at each step.

We are using Shared Preferences for handling permanently denied state. If user declines permission twice that means permission state is permanently denied. We are putting that data in the local store.

fun launch(
permissions: ArrayList<String>,
onShowRational: (deniedPermissions: ArrayList<PermissionData>) -> Unit,
onPermanentlyDenied: (permanentlyDeniedPermissions: ArrayList<PermissionData>) -> Unit,
onResult: (permissions: ArrayList<PermissionData>) -> Unit
) {

val result = arrayListOf<PermissionData>()
permissions.forEach {
if (checkSelfPermission(it)) {
result.add(PermissionData(it, PermissionResult.GRANTED))
preferences[it + activityName] = true
} else if (shouldShowRequestPermissionRationale(it)) {
preferences[it + activityName] = false
result.add(PermissionData(it, PermissionResult.DENIED))
} else {
if (preferences[it + activityName]) {
result.add(PermissionData(it, PermissionResult.PERMANENTLY_DENIED))
} else
permissionCheck.launch(permissions.map { it }.toTypedArray())
}
}
// We kept denied permissions inside denied list to decide that;
// will we show rationale or not
denied = result.filter { it.state == PermissionResult.DENIED }
val permanentlyDenied = result.filter { it.state == PermissionResult.PERMANENTLY_DENIED }
// Here we check denied list is empty or not. If it's empty there is no need to show rationale
// else we inform user via callback
if (denied.isNotEmpty())
onShowRational.invoke(denied as ArrayList<PermissionData>)
// Here we check permanentlyDenied list is empty or not.
// If it's empty there is no need to show permanently denied dialog
// else we inform user via callback to show dialog
if (permanentlyDenied.isNotEmpty())
onPermanentlyDenied.invoke(permanentlyDenied as ArrayList<PermissionData>)
// In every step we inform the user about permission states in that time.
onResult.invoke(result)
}

5. Check permission:

This method check given permission is granted or not and returns boolean.

private fun checkSelfPermission(vararg permissions: String): Boolean {
for (permission in permissions) if (activity?.let {
ContextCompat.checkSelfPermission(
it,
perm
)
} != PackageManager.PERMISSION_GRANTED
) return false

return true
}

6. Check should show rationale

With this method, we check user denied the permission once or not.

private fun shouldShowRequestPermissionRationale(vararg permissions: String): Boolean {
for (perm in permissions)
if (ActivityCompat.shouldShowRequestPermissionRationale(activity!!, perm)) return true
return false
}

7. Shared Preference Handler:

We need shared preferences to store if users denied the permission more than one. If so user denied the permission more than one, that means that permission is permanently denied. If user denied just once, we will show rationale dialog to try again to get the permission.

import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager

/**
* Created by sahintekin on 13.11.2023.
*/
object PreferencesHandler {
fun build(context: Context, name: String? = null): SharedPreferences =
if (name.isNullOrEmpty())
context.getSharedPreferences(name, Context.MODE_PRIVATE)
else
PreferenceManager.getDefaultSharedPreferences(context)

private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {
val editor = this.edit()
operation(editor)
editor.apply()
}
/**
* puts a key value pair in shared prefs if doesn't exists, otherwise updates value on given [key]
*/
operator fun SharedPreferences.set(key: String, value: Any?) {
when (value) {
is String? -> edit { it.putString(key, value) }
is Int -> edit { it.putInt(key, value) }
is Boolean -> edit { it.putBoolean(key, value) }
is Float -> edit { it.putFloat(key, value) }
is Long -> edit { it.putLong(key, value) }
else -> throw UnsupportedOperationException("Not yet implemented")
}
}
/**
* finds value on given key.
* [T] is the type of value
* @param defaultValue optional default value - will take null for strings, false for bool and -1 for numeric values if [defaultValue] is not specified
*/
inline operator fun <reified T : Any> SharedPreferences.get(
key: String,
defaultValue: T? = null
): T {
return when (T::class) {
String::class -> getString(key, defaultValue as? String ?: "") as T
Int::class -> getInt(key, defaultValue as? Int ?: -1) as T
Boolean::class -> getBoolean(key, defaultValue as? Boolean ?: false) as T
Float::class -> getFloat(key, defaultValue as? Float ?: -1f) as T
Long::class -> getLong(key, defaultValue as? Long ?: -1) as T
else -> throw UnsupportedOperationException("Not yet implemented")
}
}
}

8. Scan for Activity Extension:

This code aims to return an Activity object by scanning over a Context object.

  • If the current object is an Activity, it directly returns this object.
  • If the current object is a ContextWrapper, it scans over the base Context.
  • In any other case (for example, if it is null or of an unexpected type), it returns null.
fun Context.scanForActivity(): Activity? {
return when (this) {
is Activity -> this
is ContextWrapper -> baseContext.scanForActivity()
else -> {
null
}
}
}

9. Clear Permanently Denied stored data:

With this method, we clear shared preferences data if we need.

fun clearAllPermanentlyDeniedData(){
preferences.edit().clear().apply()
}

Final code:

import android.app.Activity
import android.content.SharedPreferences
import android.content.pm.PackageManager
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.bayarsahintekin.permission.PreferencesHandler.get
import com.bayarsahintekin.permission.PreferencesHandler.set
import java.lang.ref.WeakReference

class RequestPermissionLauncher private constructor(private val lifecycleOwner: WeakReference<LifecycleOwner>) :
DefaultLifecycleObserver, ActivityResultCallback<Map<String, Boolean>> {

private lateinit var permissionCheck: ActivityResultLauncher<Array<String>>
private var activity: Activity? = null
private var denied: List<PermissionData> = arrayListOf()
private lateinit var preferences: SharedPreferences
private var activityName: String = "Unknown Activity"

init {
lifecycleOwner.get()?.lifecycle?.addObserver(this)
activity?.let {
activityName = it::class.java.simpleName
}
}

companion object {
fun from(lifecycleOwner: LifecycleOwner) =
RequestPermissionLauncher(WeakReference(lifecycleOwner))
}

override fun onCreate(owner: LifecycleOwner) {
permissionCheck = when (owner) {
is AppCompatActivity -> {
owner.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
this
)
}
is ComponentActivity -> {
owner.registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
this
)
}
else -> {
(owner as Fragment).registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
this
)
}
}

activity = when (lifecycleOwner.get()) {
is Fragment -> {
(lifecycleOwner.get() as? Fragment)?.context?.scanForActivity()
}
is ComponentActivity -> {
lifecycleOwner.get() as ComponentActivity
}
else -> {
lifecycleOwner.get() as? AppCompatActivity
}
}

activity?.applicationContext?.let {
preferences = PreferencesHandler.build(it)
}

super.onCreate(owner)
}

override fun onActivityResult(result: Map<String, Boolean>) {}

private fun checkSelfPermission(vararg permissions: String): Boolean {
for (perm in permissions) if (activity?.let {
ContextCompat.checkSelfPermission(
it,
perm
)
} != PackageManager.PERMISSION_GRANTED
) return false
return true
}

private fun shouldShowRequestPermissionRationale(vararg permissions: String): Boolean {
for (perm in permissions)
if (ActivityCompat.shouldShowRequestPermissionRationale(activity!!, perm)) return true
return false
}

fun requestPermission() = permissionCheck.launch(denied.map { it.permission }.toTypedArray())

fun clearAllPermanentlyDeniedData(){
preferences.edit().clear().apply()
}

fun launch(
permissions: ArrayList<String>,
onShowRational: (deniedPermissions: ArrayList<PermissionData>) -> Unit,
onPermanentlyDenied: (permanentlyDeniedPermissions: ArrayList<PermissionData>) -> Unit,
onResult: (permissions: ArrayList<PermissionData>) -> Unit
) {
val result = arrayListOf<PermissionData>()
permissions.forEach {
if (checkSelfPermission(it)) {
result.add(PermissionData(it, PermissionResult.GRANTED))
preferences[it + activityName] = true
} else if (shouldShowRequestPermissionRationale(it)) {
preferences[it + activityName] = false
result.add(PermissionData(it, PermissionResult.DENIED))
} else {
if (preferences[it + activityName]) {
result.add(PermissionData(it, PermissionResult.PERMANENTLY_DENIED))
} else
permissionCheck.launch(permissions.map { it }.toTypedArray())
}
}
// We kept denied permissions inside denied list to decide that;
// will we show rationale or not
denied = result.filter { it.state == PermissionResult.DENIED }
val permanentlyDenied = result.filter { it.state == PermissionResult.PERMANENTLY_DENIED }
// Here we check denied list is empty or not. If it's empty there is no need to show rationale
// else we inform user via callback
if (denied.isNotEmpty())
onShowRational.invoke(denied as ArrayList<PermissionData>)
// Here we check permanentlyDenied list is empty or not.
// If it's empty there is no need to show permanently denied dialog
// else we inform user via callback to show dialog
if (permanentlyDenied.isNotEmpty())
onPermanentlyDenied.invoke(permanentlyDenied as ArrayList<PermissionData>)
// In every step we inform user permission states in that time.
onResult.invoke(result)
}
}

enum class PermissionResult {
GRANTED, DENIED, PERMANENTLY_DENIED
}

data class PermissionData(var permission: String, var state: PermissionResult)

Find Me

LinkedIn: https://www.linkedin.com/in/bayar-%C5%9Fahintekin-b70540104/?originalSubdomain=tr

Github: https://github.com/bayarsahintekin0

Reference:

Project in GitHub:

Sources:

--

--