Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to ask permission, the clean way - Droidcon 2021

Ronaldo Pace
October 22, 2021
210

How to ask permission, the clean way - Droidcon 2021

Presentation at Droidcon 2021 by Ronaldo Pace
How to ask permission, the clean way
Tells the approach to implement Android Runtime permissions using a "clean" approach of service/repository/viewModel by the means decoupling it from the Android UI.

Ronaldo Pace

October 22, 2021
Tweet

Transcript

  1. How to ask permission,
    @ronaldopace
    (the clean way)

    View Slide

  2. Slide was added after the conference
    I decided I'll refactor my previous library
    "permission-bitte" into a V2 that will be the full
    implementation on what's on this presentation.
    This will be a nights and weekends deal,
    so might take a lil bit to complete.
    So if you're interested,
    be sure to watch or star the repo on
    https://github.com/budius/permission-bitte

    View Slide

  3. data
    (services)
    domain / business
    (repository)
    presentations
    (view/viewModel)
    The "clean" architecture

    View Slide

  4. class MyFragment {
    fun onSomethingClick() = when {
    ContextCompat.checkSelfPermission(this, Manifest.permission.REQUESTED_PERMISSION) == PackageManager.PERMISSION_GRANTED ->
    useTheApi()
    shouldShowRequestPermissionRationale(permissions) -> showPopUpToUser()
    else -> requestPermissions(this, arrayOf(Manifest.permission.REQUESTED_PERMISSION), REQUEST_CODE)
    }
    override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array,
    grantResults: IntArray
    ) {
    if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
    useTheApi()
    } else {
    showPopUpToUser()
    }
    }
    }
    How to ask permission (according to the docs)

    View Slide

  5. How to "cleanly" ask permission (naively)
    class MyFragment {
    fun onSomethingClick() = when {
    ContextCompat.checkSelfPermission(this, Manifest.permission.REQUESTED_PERMISSION) == PackageManager.PERMISSION_GRANTED ->
    viewModel.useTheApi()
    shouldShowRequestPermissionRationale(permissions) -> showPopUpToUser()
    else -> requestPermissions(this, arrayOf(Manifest.permission.REQUESTED_PERMISSION), REQUEST_CODE)
    }
    override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array,
    grantResults: IntArray
    ) {
    if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
    viewModel.useTheApi()
    } else {
    showPopUpToUser()
    }
    }
    }

    View Slide

  6. How to ask permission (with libraries)
    // delegates
    private val permissionDelegate = PermissionDelegate(this)
    fun onSomethingClick() {
    if (permissionDelegate.needsPermission(Manifest.permission.REQUESTED_PERMISSION))
    permission.request(Manifest.permission.REQUESTED_PERMISSION)
    }
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array,
    grantResults: IntArray) = permissionDelegate.result(requestCode, permissions, grantResults)
    // annotation
    @NeedsPermission(Manifest.permission.REQUESTED_PERMISSION)
    fun onSomethingElseClick() {
    // permission was approved
    }
    @OnPermissionResult(Manifest.permission.REQUESTED_PERMISSION)
    fun onRequestPermissionResult(result: Code) {
    // handle result here
    }

    View Slide

  7. Are they really clean,
    if all that logic is on the presentation layer?
    data
    (services)
    domain / business
    (repository)
    presentations
    (view/viewModel)
    (don´t forget to mention DRY)

    View Slide

  8. interface PermissionService {
    val permissions: Flow>
    suspend fun requestPermissions(permissions: Set)
    }
    data class Permission(val name: String, state: PermissionState)
    enum class PermissionState {
    GRANTED,
    REQUEST_PERMISSION,
    DENIED,
    SHOW_RATIONALE
    }
    PermissionService.kt
    data
    (services)
    domain / business
    (repository)
    presentations
    (view/viewModel)

    View Slide

  9. Dividing the problem: List
    Just copy from: https://github.com/budius/permission-bitte
    class PermissionServiceImpl(app: Application) : PermissionService {
    private val _permissions = MutableStateFlow(extractPermissionsFromManifest(app))
    override val permissions = _permissions.map{ mapToSet(it) }.distinctUntilChanged()
    }
    private fun extractPermissionsFromManifest(context: Context): Map {
    val pm = context.packageManager
    val info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS)
    val names = info.requestedPermissions
    val groups = names.map { pm.getPermissionInfo(name, 0).group }
    val flags = info.requestedPermissionsFlags
    // parse name, group, flags into Map
    // but DENIED can´t be obtained here =(
    map[name] = GRANTED / REQUEST_PERMISSION / SHOW_RATIONALE

    View Slide

  10. Dividing the problem: Updating from outside the app
    class PermissionServiceImpl(app: Application) : PermissionService {
    private val activityCallback = object : ActivityLifecycleCallbacks {
    override fun onActivityResumed(activity: Activity) {
    val newData = extractPermissionsFromManifest(activity)
    _permissions.value = updatePermissions(newData, _permissions.value)
    }
    }
    }
    private fun updatePermissions(
    newData: Map,
    current: Map
    ): Map {
    // any permissions that current is DENIED,
    // must stay DENIED
    // the others, pick from the newData
    }

    View Slide

  11. class PermissionServiceImpl {
    private var pendingResult: CompletableDeferred? = null
    override suspend fun requestPermissions(permissions: Set) {
    val request = filter(permissions) ?: return
    pendingResult = CompletableDeferred().also {
    requestPermissionInternal(permissions, it) // magic ?
    }
    val result = pendingResult.await()
    val newData: Map = parseResult(result)
    _permissions.value = updatePermissions(newData, _permissions.value)
    }
    }
    private data class RequestResult(val permissions: Array, val grantResults: IntArray)
    Dividing the problem: Prepare the request

    View Slide

  12. class PermissionFragment : Fragment() {
    private var pendingResult: CompletableDeferred? = null
    override fun onResume() {
    super.onResume()
    requestPermissions(requireArguments().getStringArray("permissions")!!, 42)
    }
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    pendingResult?.let {
    it.complete("")
    pendingResult = null
    }
    }
    fun setPendingResult(pendingResult: CompletableDeferred) {
    this.pendingResult = pendingResult
    }
    }
    Dividing the problem: Request permission

    View Slide

  13. Dividing the problem: Triggering the request
    class PermissionServiceImpl {
    private var pendingResult: CompletableDeferred? = null
    private lateinit var currentActivity: WeakReferenced
    override fun onActivityCreated(activity: Activity) {
    pendingRequest?.let { addPermissionFragment(activity, pendingResult) }
    }
    override fun onActivityStarted(activity: Activity) {
    if (activity is FragmentActivity) currentActivity = WeakReferenced(activity)
    }
    private fun requestPermissionInternal(permissions: Set,
    pendingResult: CompletableDeferred) {
    currentActivity.get()?.supportFragmentManager.beginTransaction()
    .add(PermissionFragment().apply {
    arguments = Bundle().apply { putStringArray("permissions", val) }
    setPendingResult(pendingResult)}, "permission-fragment").commit()
    }
    }

    View Slide

  14. Is it clean now?
    data
    (services)
    domain / business
    (repository)
    presentations
    (view/viewModel)
    PermissionFragment
    PermissonServiceImpl
    PermissionService
    PermissionRepo LocationRepo
    Onboarding V-VM SomeMapThing V-VM
    Google Play Services
    - Location

    View Slide

  15. Questions?
    @ronaldopace

    View Slide