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

How to ask permission, the clean way - Droidcon...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Ronaldo Pace Ronaldo Pace
October 22, 2021
360

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.

Avatar for Ronaldo Pace

Ronaldo Pace

October 22, 2021
Tweet

Transcript

  1. 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
  2. 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<String>, grantResults: IntArray ) { if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { useTheApi() } else { showPopUpToUser() } } } How to ask permission (according to the docs)
  3. 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<String>, grantResults: IntArray ) { if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { viewModel.useTheApi() } else { showPopUpToUser() } } }
  4. 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<String>, 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 }
  5. 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)
  6. interface PermissionService { val permissions: Flow<Set<Permission>> suspend fun requestPermissions(permissions: Set<Permission>)

    } 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)
  7. Dividing the problem: List<Permission> 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<String, PermissionState> { 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<String, PermissionState> // but DENIED can´t be obtained here =( map[name] = GRANTED / REQUEST_PERMISSION / SHOW_RATIONALE
  8. 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<String, PermissionState>, current: Map<String, PermissionState> ): Map<String, PermissionState> { // any permissions that current is DENIED, // must stay DENIED // the others, pick from the newData }
  9. class PermissionServiceImpl { private var pendingResult: CompletableDeferred<RequestResult>? = null override

    suspend fun requestPermissions(permissions: Set<Permission>) { val request = filter(permissions) ?: return pendingResult = CompletableDeferred().also { requestPermissionInternal(permissions, it) // magic ? } val result = pendingResult.await() val newData: Map<String, PermissionState> = parseResult(result) _permissions.value = updatePermissions(newData, _permissions.value) } } private data class RequestResult(val permissions: Array<String>, val grantResults: IntArray) Dividing the problem: Prepare the request
  10. class PermissionFragment : Fragment() { private var pendingResult: CompletableDeferred<RequestResult>? =

    null override fun onResume() { super.onResume() requestPermissions(requireArguments().getStringArray("permissions")!!, 42) } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { pendingResult?.let { it.complete("") pendingResult = null } } fun setPendingResult(pendingResult: CompletableDeferred<RequestResult>) { this.pendingResult = pendingResult } } Dividing the problem: Request permission
  11. Dividing the problem: Triggering the request class PermissionServiceImpl { private

    var pendingResult: CompletableDeferred<RequestResult>? = null private lateinit var currentActivity: WeakReferenced<FragmentActivity> 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<Permission>, pendingResult: CompletableDeferred<RequestResult>) { currentActivity.get()?.supportFragmentManager.beginTransaction() .add(PermissionFragment().apply { arguments = Bundle().apply { putStringArray("permissions", val) } setPendingResult(pendingResult)}, "permission-fragment").commit() } }
  12. 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