Last active
May 10, 2025 04:49
-
-
Save mkovalyk/1e80a975a7f7fa9a895c7e6915041009 to your computer and use it in GitHub Desktop.
Make Android permission easier
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.myapplication | |
import android.content.pm.PackageManager | |
import androidx.activity.result.ActivityResultLauncher | |
import androidx.activity.result.contract.ActivityResultContracts | |
import androidx.core.app.ActivityCompat | |
import androidx.core.content.ContextCompat | |
import androidx.fragment.app.Fragment | |
import androidx.lifecycle.Lifecycle | |
import androidx.lifecycle.LifecycleObserver | |
import androidx.lifecycle.OnLifecycleEvent | |
/** | |
* It should be one-per-fragment manager. | |
* Immediately subscribes to activity callbacks | |
*/ | |
class PermissionManagerImpl( | |
private var fragment: Fragment? | |
) : PermissionManager, LifecycleObserver { | |
private val launchers = mutableMapOf<Permission, ActivityResultLauncher<String>>() | |
private val observers = mutableMapOf<Permission, PermissionInfo>() | |
/** | |
* Stores whether Rationale has been shown for specific [Permission] of permission. | |
* Should be reset after every access granting | |
*/ | |
private val shownRationale = mutableMapOf<Permission, Boolean>() | |
init { | |
// it should be non-null here | |
bind(fragment) | |
} | |
private fun bind(fragment: Fragment?) { | |
this.fragment = fragment | |
} | |
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) | |
override fun unbind() { | |
val previousValues = launchers.toMap() | |
previousValues.values.forEach { it.unregister() } | |
launchers.clear() | |
observers.clear() | |
fragment = null | |
} | |
private fun processPermissionReply(type: Permission, isGranted: Boolean) { | |
observers[type]?.let { observer -> | |
if (isGranted) { | |
observer.granted?.invoke() | |
} else { | |
// by indicating whether rationale should be shown we can assume that user | |
// clicked "Deny&don't ask again" | |
fragment?.let { | |
val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale( | |
it.requireActivity(), | |
type.name | |
) | |
if (shouldShowRationale) | |
observer.denied?.invoke() | |
else | |
observer.permanentlyDenied?.invoke() | |
} | |
} | |
// in any case it should be reset because user made a decision | |
shownRationale[type] = false | |
} | |
} | |
override fun hasPermission(permission: Permission): Boolean { | |
return fragment?.let { | |
ContextCompat.checkSelfPermission( | |
it.requireContext(), | |
permission.name | |
) == PackageManager.PERMISSION_GRANTED | |
} ?: false | |
} | |
override fun requestPermission(info: PermissionInfo) { | |
observers[info.type] = info | |
if (hasPermission(info.type)) { | |
info.granted?.invoke() | |
} else { | |
val activity = fragment?.activity ?: return | |
val shouldShowRationale = ActivityCompat.shouldShowRequestPermissionRationale( | |
activity, | |
info.type.name | |
) && !(shownRationale[info.type] ?: false) | |
if (shouldShowRationale) { | |
shownRationale[info.type] = true | |
info.rationale?.invoke() | |
} else { | |
launchers[info.type]?.launch(info.type.name) | |
} | |
} | |
} | |
override fun forPermission(type: Permission): PermissionInfo { | |
return PermissionInfo(type, this) | |
} | |
override fun subscribe(info: PermissionInfo) { | |
fragment?.let { | |
val launcher = | |
it.registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> | |
processPermissionReply(info.type, isGranted) | |
} | |
launchers[info.type] = launcher | |
observers[info.type] = info | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@file:Suppress("unused") | |
package com.example.myapplication | |
import android.Manifest | |
import android.util.Log | |
import androidx.lifecycle.Lifecycle | |
import androidx.lifecycle.LifecycleObserver | |
import androidx.lifecycle.LifecycleOwner | |
import androidx.lifecycle.OnLifecycleEvent | |
/** | |
* Class which holds callbacks for permission and some other information | |
*/ | |
open class PermissionInfo(val type: Permission, val permissionManager: PermissionManager) { | |
constructor(info: PermissionInfo) : this(info.type, info.permissionManager) { | |
granted = info.granted | |
denied = info.denied | |
rationale = info.rationale | |
permanentlyDenied = info.permanentlyDenied | |
} | |
var granted: PermissionAction? = null | |
private set | |
var denied: PermissionAction? = null | |
private set | |
var permanentlyDenied: PermissionAction? = null | |
private set | |
var rationale: PermissionAction? = null | |
private set | |
/** | |
* Use this to pass all callbacks at once | |
*/ | |
fun withCallback( | |
onGranted: PermissionAction? = null, | |
onDenied: PermissionAction? = null, | |
onPermanentlyDenied: PermissionAction? = null, | |
onRationale: PermissionAction? = null | |
): PermissionInfo { | |
granted = onGranted | |
denied = onDenied | |
rationale = onRationale | |
permanentlyDenied = onPermanentlyDenied | |
return this | |
} | |
fun onGranted(action: PermissionAction): PermissionInfo { | |
return this.apply { | |
granted = action | |
} | |
} | |
fun onDenied(action: PermissionAction): PermissionInfo { | |
return this.apply { | |
denied = action | |
if (permanentlyDenied == null) { | |
permanentlyDenied = denied | |
} | |
} | |
} | |
fun onPermanentlyDenied(action: PermissionAction): PermissionInfo { | |
return this.apply { | |
permanentlyDenied = action | |
} | |
} | |
fun onRationale(action: PermissionAction): PermissionInfo { | |
return this.apply { | |
rationale = action | |
} | |
} | |
/** | |
* Removes all callbacks | |
*/ | |
fun clearCallbacks() { | |
granted = null | |
denied = null | |
rationale = null | |
permanentlyDenied = null | |
} | |
fun subscribe(lifecycleOwner: LifecycleOwner? = null): PermissionRequester { | |
val info = if (lifecycleOwner == null) { | |
this | |
} else { | |
LifecycleAwareInfo(this, lifecycleOwner) | |
} | |
return PermissionRequester(info).also { info.permissionManager.subscribe(info) } | |
} | |
} | |
/** | |
* It automatically subscribes to the lifecycle and clears actions after onDestroy event | |
*/ | |
class LifecycleAwareInfo( | |
private val info: PermissionInfo, | |
lifecycleOwner: LifecycleOwner | |
) : PermissionInfo(info), LifecycleObserver { | |
var lifecycleOwner: LifecycleOwner? = null | |
init { | |
this.lifecycleOwner = lifecycleOwner | |
lifecycleOwner.lifecycle.addObserver(this) | |
} | |
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) | |
fun removeActions() { | |
Log.d("QQQ", "removeActions") | |
info.permissionManager.unbind() | |
lifecycleOwner?.lifecycle?.removeObserver(this) | |
} | |
} | |
typealias PermissionAction = () -> Unit | |
/** | |
* Essentially, it is the class to postpone to request itself | |
*/ | |
class PermissionRequester(private val info: PermissionInfo) { | |
fun request() { | |
info.permissionManager.requestPermission(info) | |
} | |
} | |
/** | |
* Description of the permission. Name for now | |
*/ | |
open class Permission(val name: String) | |
/** | |
* Possible types of Dangerous permission. Might be extended in future | |
*/ | |
object Permissions { | |
object Camera : Permission(Manifest.permission.CAMERA) | |
object WriteExternalStorage : Permission(Manifest.permission.WRITE_EXTERNAL_STORAGE) | |
object FineLocation : Permission(Manifest.permission.ACCESS_FINE_LOCATION) | |
object CoarseLocation : Permission(Manifest.permission.ACCESS_COARSE_LOCATION) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class FirstFragment : Fragment() { | |
private val permissionManager = PermissionManagerImpl(this) | |
private val cameraRequester: PermissionRequester = | |
permissionManager.forPermission(Permissions.Camera) | |
.onDenied { -> | |
Log.d("QQQ", "denied") | |
} | |
.onPermanentlyDenied { | |
Log.d("QQQ", "permanently denied") | |
val packageName = requireActivity().packageName | |
Intent( | |
Settings.ACTION_APPLICATION_DETAILS_SETTINGS, | |
Uri.parse("package:$packageName") | |
).apply { | |
addCategory(Intent.CATEGORY_DEFAULT) | |
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | |
requireActivity().startActivity(this) | |
} | |
} | |
.onGranted { | |
Log.d("QQQ", "granted") | |
} | |
.onRationale { | |
AlertDialog.Builder(requireActivity()) | |
.setTitle("Title") | |
.setMessage("We need camera to make a photo") | |
.setPositiveButton("OK") { _, _ -> requestCameraPermission() } | |
.setNegativeButton("No") { _, _ -> } | |
.show() | |
} | |
.subscribe(this) | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
// request permission | |
cameraRequester.request() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment