Skip to content

Instantly share code, notes, and snippets.

@mkovalyk
Last active November 2, 2022 08:04
Show Gist options
  • Save mkovalyk/1e80a975a7f7fa9a895c7e6915041009 to your computer and use it in GitHub Desktop.
Save mkovalyk/1e80a975a7f7fa9a895c7e6915041009 to your computer and use it in GitHub Desktop.
Make Android permission easier
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
}
}
}
@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)
}
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