Clean Runtime Permissions in Android

Runtime permissions are very common feature implemented in Android applications. However, for some reason, they always catch me off-guard and I routinely plant bugs into my projects when using them. Maybe it’s just me, but I doubt it.

Therefore, I decided to summarize what I know about runtime permissions in Android in this post and use it as a reference in the future.

Runtime Permissions

First of all, I want to provide a general context for runtime permissions as a user-facing feature. If you’re already familiar with this mechanism, you can skip right to the more technical discussion below.

Before Android M (API 23), the system prompted users to approve all the permissions that apps required before the installation. Therefore, the moment your app had made it into users’ devices, you could be sure that it had all the permissions declared in its manifest file:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

Our lives, as developers, were simple back then because all we had to do was adding the above lines into app’s manifest, and that’s about it.

However, this “static” permissions model was a problem for user’s privacy. Since users had to approve all the permissions at once (otherwise they couldn’t install the app), this mechanism was abused and applications asked for way too many permissions. In addition, most users didn’t review apps’ permissions thoroughly enough (or didn’t review them at all), so malicious players could easily gain access to users’ private data.

That’s why Google introduced so-called “runtime permissions” mechanism in Android M. Following this change, Android apps wouldn’t get all the permissions upon installation, but, instead, would need to ask for users’ approval of so-called “dangerous” permissions at runtime. Furthermore, unlike with legacy static permissions, users can deny usage of individual runtime permissions and the applications are expected to handle this situation gracefully (e.g. disable parts of the functionality).

In my opinion, introduction of runtime permissions was the biggest change in Android applications’ architecture ever. Undoubtedly, it was a positive change for the users, but it added a considerable amount of work for Android developers.

Requesting a Runtime Permission

It’s important to remember that even if you request a specific permission at runtime, you must still declare it in the manifest file. If you forget doing that, the system will automatically deny your runtime request for that permission. That’s really annoying and I routinely waste time debugging this problem when I ask for additional runtime permissions.

Once you added the permission declaration to the manifest, the next step is to request that permission in code. For example, that’s how you’d do that inside Activity:

private static final int REQUEST_CODE_LOCATION = 1;
...
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_LOCATION);

Notice that the method is called ActivityCompat.requestPermissions and accepts an array of permission names. Therefore, you can ask for more than one permission at a time (discussed later). The last argument is just a number associated with a specific request. That’s so-called “async completion token” pattern and you can use this number to map from the result to a specific request at a later stage.

The above approach will work in any component where you can get a reference to Activity object. The result of this request will be delivered to the specific Activity that you pass into ActivityCompat.requestPermissions as the first argument (discussed later).

However, when you request runtime permissions from inside Fragment, usually you’d like the result to be delivered to that same Fragment. In such a case, don’t use ActivityCompat, but just call Fragment’s requestPermissions method:

private static final int REQUEST_CODE_LOCATION = 1;
...
requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_LOCATION);

Following the aforementioned API calls, the system will show a dialog to the user which will prompt them to either grant or deny a specific permission:

After the user will choose either of the options, the system will route the result of this request back to your application.

Deprecation of requestPermissions Method

Unfortunately, if you read the documentation of Fragment.requestPermissions method, you’ll notice that it’s already deprecated. The docs recommend using Fragment’s registerForActivityResult method, combined with ActivityResultContracts.RequestMultiplePermissions object instead. However, if you try to use this approach with “stable” versions of fragmentx and activityx today (as of this writing, these are fragmentx:1.2.5 and activityx:1.1.0), your code won’t compile. What’s going on here?

Turns out that the above APIs will be introduced only in the next versions of fragmentx and activityx. Therefore, today, they will work only with non-stable artifacts (as of this writing, these are fragmentx:1.3.0-rc01 and activityx:1.2.0-rc01). The official docs simply describe non-stable APIs. That’s appaling, of course, but, given what’s going on in Android ecosystem, isn’t too surprising.

I will probably write another post about this new approach once the APIs reach stable milestones, but I can already say that they look more complex and dirtier to me. So, if you have masochistic tendencies, use them. Otherwise, read on to discover how to avoid all this mess.

Handling the Result of a Runtime Permission Request

The result of runtime permission request will be delivered to onRequestPermissionsResult method inside either target Activity (if you used ActivityCompat.requestPermissions method), or source fragment (if you used Fragment’s requestPermissions method). Therefore, to handle this result, you’ll need to override this method:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    // handle the result
}

The first argument, requestCode, is the “async completion token” that you supplied when asking for a permission. If you need to differentiate between multiple flows that request permissions, you can check the value of this argument here. The second argument, permissions, is an array containing the names of the requested permissions and the third argument, grantResults, is an array containing values that indicate whether the user granted the respective permissions.

The full implementation of onRequestPermissionsResult for a single permission will usually look similar to this:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    
    if (permissions.length == 0 || grantResults.length == 0) {
        // permissions request cancelled
    }

    switch (requestCode) {
        case REQUEST_CODE_LOCATION:
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // permission granted
            } else if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) {
                // permission denied
            } else {
                // permission denied and don't ask for it again
            }
            break;
        default:
            throw new RuntimeException("unhandled permissions request code: " + requestCode);
    }
}

Let’s go over what happens here.

If either permissions or grantResults arrays are empty, it means that the request was cancelled by the system. That’s relatively rare corner case which I often forget about, but it still must be accounted for. In my opinion, in this case, in most cases, you can just request the same permissions again.

Then, by inspecting the values in grantResults, you can find out whether user granted the requested permission. If they did, great! If not, then there are two different cases you’ll need to account for.

When ActivityCompat.shouldShowRequestPermissionRationale returns true, it means that you’re allowed to ask for the same permission again. However, before you do, think carefully whether the requested permission is important enough to bother the user after they’ve already declined it once. If that’s the case, then it’s also recommended to show “permission rationale” before you ask for that permission again. This can be a simple dialog explaining to the user why your app needs that specific permission. Hopefully, after reading this message, the user will approve your request.

However, if ActivityCompat.shouldShowRequestPermissionRationale returns false, it means that, in addition to denying that permission request, the user also checked “Don’t ask again” checkbox. In this case, you can’t ask for this permission anymore and if you do, your request will be denied automatically by the system (without even prompting the user). Your app should basically keep working without that permission going forward. If your app simply can’t work without the permission in question, too bad for the user. In this case, you should probably show a message on the screen and explain to the user that the app won’t function properly. If they’ll want to grant you that permission after all, they’ll need to do it manually in settings app.

Requesting Multiple Runtime Permissions at Once

As I already mentioned above, you can request multiple permissions at once:

private static final int REQUEST_CODE_PERMISSIONS = 1;
...
String[] permissions = new String[] {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.GET_PHONE_STATE};
ActivityCompat.requestPermissions(this, permissions, REQUEST_CODE_PERMISSIONS);

The handling logic inside onRequestPermissionsResult becomes a bit more involved if it needs to handle multiple permissions (added for loop):

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    if (permissions.length == 0 || grantResults.length == 0) {
        // permissions request cancelled
    }

    for (int i = 0; i < permissions.length; i++) {
        switch (requestCode) {
            case REQUEST_CODE_LOCATION:
                if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                    // permission i granted
                } else if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[i])) {
                    // permission i denied
                } else {
                    // permission i denied and don't ask for it again
                }
                break;
            default:
                throw new RuntimeException("unhandled permissions request code: " + requestCode);
        }
    }
}

Requesting multiple permissions at once can be useful if any of your app’s features requires more than one permission, or if you want to request some permissions in advance (e.g. during app’s first startup).

Clean Design for Runtime Permissions

The approaches described above work and will do the trick. However, in my experience, runtime permissions management logic can get out of hand very quickly. When this happens, it becomes a source of bugs and maintainability overhead. The main reason for such an unfortunate outcome is code duplication.

If you look at the previous code snippet, it doesn’t look that bad on its own. However, imagine that you have 10 screens in your app that request various permissions. In this case, most of that “boilerplate” logic will be duplicated in 10 Activities or Fragments (or whatever component corresponds to “a screen” in your app). If you add the facts that there might be more than one permission request code on each “screen” and there will be additional app-specific logic there, you can imagine how ugly the permissions management logic can get. Or, maybe, you’ve already seen this ugliness in a real Android project, so you don’t even need to engage your imagination.

Therefore, in my projects, to avoid all the above problems, I always introduce PermissionsHelper abstraction and MyPermission enum.

The main goal of PermissionsHelper is to encapsulate the common “boilerplate” logic and expose a more convenient API.

The main goal of the additional enum is to replace “dirty” Android strings with type-safe enum values. Since these values will be specific to your app, it will allow you to see, at a glance, which runtime permissions the app requires and also search for all the places where you ask for specific permissions.

Once you add these classes to your codebase, you can request permissions in this manner:

MyPermission[] permissions = new MyPermission[] {MyPermission.FINE_LOCATION, MyPermission.READ_PHONE_STATE};
if (!mPermissionsHelper.hasAllPermissions(permissions)) {
    mPermissionsHelper.requestAllPermissions(permissions, 1);
}

In order to get the results in either Activity or Fragment, you’ll need to register them as listeners first:

@Override
protected void onStart() {
    super.onStart();
    mPermissionsHelper.registerListener(this);
}

@Override
protected void onStop() {
    super.onStop();
    mPermissionsHelper.unregisterListener(this);
}

Then, once the user will provide the required inputs, either of these two callbacks will be invoked:

@Override
public void onRequestPermissionsResult(int requestCode, PermissionsHelper.PermissionsResult result) {
    if (!result.deniedDoNotAskAgain.isEmpty()) {
        // handle permission(s) that were denied and shouldn't ask again
    } 
    
    if (!result.denied.isEmpty()) {
        // handle permission(s) that were denied
    }

    if (!result.granted.isEmpty()) {
        // handle permission(s) that were granted
    }

}

@Override
public void onPermissionsRequestCancelled(int requestCode) {
    // handle permissions request cancellation
}

As you hopefully see, this approach is simpler, more readable and leaves much less space for misunderstandings and bugs.

However, there are no free lunches in software design and architecture, so here is the catch: in order for PermissionsHelper to function properly, you’ll need to make sure that all clients within the scope of a single Activity share the same instance of this object. That’s something you can achieve with Dagger using Activity-scoped Component, or using Hilt’s ActivityComponent. If you don’t use proper DI in your app, you can theoretically hack this up with Activity-scoped ViewModel.

Speaking of ViewModel, note that PermissionsHelper keeps a reference to Activity object. Therefore, it requires a bit more caution if you also use ViewModels in your project (I never use them).

Lastly, you’ll also need to delegate the call to onRequestPermissionsResult from the enclosing Activity to PermissionsHelper:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    // must delegate to PermissionsHelper because this object functions as a central "hub"
    // for permissions management in the scope of this Activity
    mPermissionsHelper.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

Additional benefits of using PermissionsHelper and MyPermission are:

  • Use exactly the same approach in Activities, Fragments and other types of “controllers”.
  • Make your unit tests simpler and more expressive (if you unit test your code).
  • Encapsulate usage of Android permissions APIs in a single class.

Note that last benefit. Given our previous discussion of the deprecation of the current APIs for permissions management and introduction of new ones, the value of PermissionsHelper increases ten-fold. In projects where I use this abstraction, the aforementioned deprecation will affect just the internal implementation of PermissionsHelper itself (and several lines of code outside of it). The clients of this class won’t even know about all this mess. In projects that lack this level of abstraction, the refactoring can affect tens of classes and hundreds of lines of code, which will increase the effort and the risk of introducing bugs many-fold.

Therefore, in my experience, the above slight complications when usign PermissionsHelper are ridiculously low price to pay for the cleanup of runtime permissions management logic in Android apps.

Permission Requests Using Kotlin Coroutines

Here and there on the internet I saw examples that demonstrate how to use Kotlin Coroutines to replace standard callback method that delivers permissions result with suspension. If I’m not mistaken, I even saw something along these lines from Google.

I don’t think it’s a good idea. The code related to runtime permissions is usually complex enough on its own, so throwing in additional complexity to spare several lines of code doesn’t make sense to me.

Conclusion

That’s it about runtime permissions in Android. Thanks for reading and I hope you found this article interesting and useful.

As usual, if you have any comments or questions, leave them below.

Check Out My Courses on Udemy

6 comments on "Clean Runtime Permissions in Android"

  1. Cool article! I was always ended up in projects where all permission logic is derived from base classes but I’m more in favor of composition. 🙂

    How about Activity/Fragment Result Listener API? Seems to be a nice solution for handling even runtime permissions?

    Reply
    • Glad you found the article useful.
      Result listener API is exactly that new official API that I described in the article as very unfortunate. It feels complex, unreadable and brittle to me.

      Reply
  2. Yeah, permissions are painful nowadays 🙁 We used something similar to abstract away permission request logic in my previous company, but without enum. That’s a good idea ?

    Reply
  3. As always very good and scoped article. I have a doubt about one thing that you mentioned. You don’t use ViewModels, don’t you think that the lifecycle aware is a good advantage of this class? Do you use MVVM architecture?

    Reply
    • I try to avoid ViewModel as much as I can. In my opinion, it’s the worst addition to Android toolbox ever, which led to completely unnecessary increase in complexity.

      Reply

Leave a Comment