Handling Android Permissions in Repository

Vlad Kolozian
5 min readMay 17, 2023

--

Nowadays, there are two preferable approaches to building Android architecture. The first one is Google’s Android Architecture, and the second one is Clean Architecture.

An essential part of the architecture is the data layer which includes repositories that provide data to the rest of the application. Repositories include the logic to retrieve data, such as obtaining the current location, retrieving data from another devices via bluetooth, etc. However, since Android 6, things got more complicated. A security enhancement mandates fine grained access control was introduced, requiring asking user permissions to access sensitive information or functions. The mechanism is inconvenient, since it encourages asking for permissions in the view layer, via Activities or Fragments and then retrieve data from the repositories, resulting in logic being spread across different layers.

I’ve implemented a possible solution that helps to work with Android permissions within the repository using Kotlin Coroutines.

The central idea is to employ a global object known as the Permission Checker, serving as a mediator between the Activity and Repository.

Here is the simplified diagram.

In code, the permission checker is represented by the PermissionChecker interface.

interface PermissionChecker {

suspend fun checkPermissions(vararg permissions: String): Result<Unit>

class PermissionsDeniedException(val permissions: Set<String>) : Exception()
}

It has a checkPermissions function which accepts a list of permissions and returns a Result<Unit> — either successful with the Unit type or with a PermissionsDeniedException that contains a set of denied permissions.

The current implementation is based on using the Result API — recommended solution by Google, so we need to use either an Activity or Fragment. I will utilize the activity because it’s the fundamental component for the majority of applications.

The snippet below includes the PermissionCheckerActivity, which serves as the base activity for all other activities. Its main purpose is to be attached to the PermissionChecker and facilitate the transfer of permission results to it.

abstract class PermissionCheckerActivity : AppCompatActivity() {

private val permissionChecker = get<ResultApiPermissionChecker>()

val resultLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
) { result: Map<String, Boolean> ->
permissionChecker.onPermissionResult(result)
onPermissionResult(result)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launchOnLifecycleStart {
permissionChecker.attach(this@PermissionCheckerActivity)
}
}

open fun onPermissionResult(result: Map<String, Boolean>) {
// None
}
}

Here, we receive the direct implementation of PermissionCheckerResultApiPermissionChecker, which we obtained from DI, and call the attach method to include the activity. To ensure that the actual activity is attached to the PermissionChecker as soon as it becomes visible, the attach method needs to be called when activity is in Started state. Also, to align with Google’s recommendation of not overriding lifecycle methods directly, an extension function is utilized in this context.

Additionally, in inherited activities, it is possible to override the onPermissionResult method and use resultLauncher property that allows for direct permission checking to organize navigation between activities, for example.

Here is the implementation of the attach and onPermissionResult functions. In the case of attach, the activity is wrapped in a WeakReference to prevent memory leaks, stored in activityWeakRef, and onNewActivityCallback is invoked to notify that a new activity is attached. In the case of onPermissionResult, the resultCallback is invoked with the permission result.

private var activityWeakRef: ActivityWeakRef? = null

private var resultCallback: ResultCallback? = null

private var onNewActivityCallback: ActivityCallback? = null

@MainThread
fun attach(activity: PermissionCheckerActivity) {
activityWeakRef = WeakReference(activity)
onNewActivityCallback?.invoke(activity)
}

@MainThread
fun onPermissionResult(result: PermissionResult) {
resultCallback?.invoke(result)
}

Both attach and onPermissionResult are annotated with the @MainThread since both are essentially invoked from the activity, within the main thread, and need to be called in this way for the purpose of synchronization.

The next snippet is the implementation of the checkPermissions function in ResultApiPermissionChecker. Function makes a permission request only if any of permissions are denied; otherwise, it simply returns a success result. Also, we should not worry about the situation when we make a request for a granted permission because the Result API handles that moment and does not show the permission dialog.

The key components in the logic for requesting permissions are the resultLauncher obtained from the attached activity that is used to ask permissions, and the suspendCancellableCoroutine function that suspends the function until the PermissionCheckerActivity invokes the resultCallback with a permission result.

override suspend fun checkPermissions(vararg permissions: String): Result<Unit> =
if (permissions.any(context::isPermissionDenied)) {

mutex.lock()
try {
val result: PermissionResult = withContext(mainDispatcher) {
var activity: PermissionCheckerActivity? = awaitForStartedActivity()

val result: PermissionResult = suspendCancellableCoroutine {
resultCallback = it::resume
it.invokeOnCancellation { resultCallback = null }
activity?.resultLauncher?.launch(arrayOf(*permissions))
activity = null // Preventing memory leak
}

resultCallback = null

result
}

if (result.all { it.value }) {
Result.success(Unit)
} else {
val deniedPermissions = result
.entries
.asSequence()
.filter { !it.value }
.map { it.key }
.toSet()

Result.failure(PermissionChecker.PermissionsDeniedException(deniedPermissions))
}
} catch (throwable: Throwable) {
Result.failure(throwable)
} finally {
mutex.unlock()
}
} else {
Result.success(Unit)
}

Here we observe that all operations involving the activity and its dependencies are executed on the main thread to ensure synchronization. This is because the activity is a shared resource that can be modified from the main thread. Accessing it from a different thread could result in inconsistent states and potential conflicts.

PermissionCheckerActivity is received from awaitForStartedActivity. The activity must be at least in the Started state; otherwise, there might be a situation¹ where the Result API won't return a permission result. Additionally, the activity only returns a result when it is started.

Here is the implementation of awaitForStartedActivity

private suspend fun awaitForStartedActivity(): PermissionCheckerActivity {
val activity = activityWeakRef?.get()

return if (activity?.isAtLeastStarted == true) {
activity
} else {
val newActivity = suspendCancellableCoroutine {
it.invokeOnCancellation { onNewActivityCallback = null }
onNewActivityCallback = it::resume
}
onNewActivityCallback = null

newActivity
}
}

Lastly, the provided code snippet showcases how the PermissionChecker is utilized within the location repository to obtain the current location. Here, we do not utilize the ResultApiPermissionChecker, which is an implementation of the PermissionChecker interface, because the repository must not have knowledge about whether the permission checker uses an activity or any other specific implementation details.

class LocationRepositoryImpl(
private val context: Context,
private val permissionChecker: PermissionChecker,
private val mainDispatcher: CoroutineContext = Dispatchers.Main.immediate,
) : LocationRepository {

override suspend fun getLocation(): Location {

val ex = permissionChecker.checkPermissions(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
).exceptionOrNull()

if ((ex is PermissionChecker.PermissionsDeniedException && ex.permissions.size > 1) ||
(ex != null && ex !is PermissionChecker.PermissionsDeniedException)
) {
throw ex
}

val locationService: LocationManager = context
.getSystemService(LocationManager::class.java)

......
}
}

If you’d like to check out the full code, you can access it via the link below.

[1]: When Activity2, which does not inherit PermissionCheckerActivity (such as an activity from a library), is launched from Activity1, and permissions are requested within the coroutine scope of Activity1’s ViewModel, returning back to Activity1 after a configuration change (such as rotation, theme change, etc.), the Result API won’t provide a result due to the state persistence within the Result API library.

Please let me know your suggestions and comments.

You can also find me on Linkedin!

Thank you for reading.

--

--

Vlad Kolozian

Proficient in building Android applications, committed to clean coding, dedicated to implementing SOLID principles and clean architecture.