SureshJoshi.com ▼

Living Dangerously with Android Permissions


2017-01-31

When Android M (aka. Android 6, aka. API Level 23) came out, it changed how we do business with regards to application permissions. Back in the day, permissions were granted at install time, however now permissions are granted at run-time - and depending on the specific permission, it’s on a case-by-case basis. While this is great for users, this sucks for developers - even more so because of a few API design decisions Google made.

Permissions are those things you declare in the AndroidManifest.xml when you want to use a specific set of APIs to control part of the phone, or get access to user data. For example, if you want to use the GPS - you need location permissions. If you want to interact with the internet, you need the Internet permission.

Ever since SDK 23, permissions have been split into 2 classes (Normal and Dangerous). From Google:

Normal permissions do not directly risk the user’s privacy. If your app lists a normal permission in its manifest, the system grants the permission automatically.

Dangerous permissions can give the app access to the user’s confidential data. If your app lists a normal permission in its manifest, the system grants the permission automatically. If you list a dangerous permission, the user has to explicitly give approval to your app.

What’s the Problem?

As a user: None! I really like this system. Actually, one step further

  • I’d probably like some level of control over Normal permissions as well.

As a developer: This entire system makes me want to throw my computer out of a window.

In theory, this system still sounds fine. You’re allowed certain permissions by default, and when you want access to more sensitive information, you ask the user and they accept/decline. Perfect… However, the implementation is… less perfect.

Bluetooth Discrimination

This gripe is more specific to the type of work I do. Scanning for Bluetooth Low Energy devices requires asking for a ‘dangerous’ (coarse or fine) Location run-time permission. This effectively means that you CANNOT use BLE in your app without requesting a permission that makes a lot of people squeamish.

I kind of understand the logic, as a lot of iBeacon-type devices can be used to locate a user within range and send that information back up to a server… But, I’d much rather all Bluetooth Low Energy be under a dangerous run-time permission called “Bluetooth” - which can caveat accordingly.

Or, maybe, do something a bit more iOS-like and limit API access based on how you’re using it… iBeacon-specific APIs require a sensitive permission, but BLE-specific APIs just require the standard Bluetooth permission.

In Android, for example, maybe you should be allowed to scan for devices, BUT, not be able to get the advertising packet’s raw data. This would handcuff a lot of applications, but basic connection could still work without a Location permission. As it is today, I can scan for a device, connect to that device, TURN OFF the location permission, and still re-connect to that BLE device - so for those of you who hate permissions, keep an eye on this option (although I’ve never tried programmatically revoking a permission I was previously granted).

And, while I’m on the subject, what the hell!? Wifi networks and gateways can ALSO be used to track your location, but we don’t need any dangerous run-time Location permissions there… Damn Bluetoothists.

Poor API Execution

This one is a biggie to me. Overall, I can deal with the new permissions, needing to request at run-time, and yes, even the Bluetooth Location BS… BUT, what I cannot handle is Google coming out with an API that returns a boolean, but is inherently a tri-state (or quad-state?)… For shame Google, for shame.

If you don’t know what I’m talking about, then you’ve probably never given permissions much thought.

I can’t put it much better than this (from Android’s issue tracker):

The current implementation of the shouldShowRequestPermissionRationale() method in the v4 support library is a downright catastrophe

Essentially, this API returns different results in 3 or 4 situations. In fact, in the API docs, they state that the pre-requisite for this call is that you do not currently have a granted permission…

So, okay, fine - there is a caveat to remove one of the 4 use cases (the method returns true if the permission was already granted), however, the method returns false in one of two incredibly different use cases:

  1. The user selected “Never Ask Again” for this permission.
  2. The permission has not yet been requested.

What this means, is that as a developer, I have to track (across app reboots possibly) whether this permission has been requested before. The easiest way to do this is a shared preference, but needing to keep state of whether a permission was requested? Come on Google.

This is doubly bad, because Android keeps an internal state itself (to populate the result of shouldShowRequestPermissionRationale). Two ways this could be solved are: Have shouldShowRequestPermissionRationale return an int/enum for each state, or have an API along the lines of hasPermissionBeenRequested(context, permission) or allowedToRequestPermission(context, permission).

Do You Need to Show Rationales?

Short answer: Yes Long answer: Kinda.

Using Google’s request patterns, based on the clarity and importance of the request, you can play around with whether you need to show a rationale or not. They refer to this as asking and educating up-front or in context.

Explanation of how to ask for Android permissions

For example, back to Bluetooth… If you have a Bluetooth scanner app, you can’t operate the app without the Location permission (as ranted above). This would be a good permission to ask for (and frankly, verify) when the app opens. However, this is a pretty easy one to dismiss, because why would a Bluetooth scanner app need to know your location??? So, to hedge against this, you can educate the user up-front about why you’re asking for Location, and if they deny it, you should probably remind them again why the app needs it. This would fit into the Unclear and Critical segment.

If that same app had a button that would let you save your app logs to the Downloads directory, you’d need to have permission to write to the external storage. So, when a user clicks on the button, check if they have granted permission to write to the external storage, and if not - request it. This would fit into the “Secondary” and “Clear” segment, since asking for storage access while you’re trying to save logs there is pretty obvious. Also, because it’s a log saver, it’s not required to make the app function - so no need to ask for it up-front.

What Can I Do About It

So, there is a way around using a shared preference, but it’s pretty crappy. You can keep some sort of static variable lying around that keeps track of whether a permission has been requested - and essentially, always ask at least once for the permission. This will allow the permissions system it to go through the motions, and if the user has selected “Never Ask Again” for all of your permissions, the call to requestPermissions is effectively ignored.

I say “effectively” because a side effect is that it will (seemingly) call and dismiss a permissions request dialog, which causes your app to go through the onPause/onResume lifecycle.

If you’re cool with that, then you can skip the shared preferences… However, a) I don’t like that, and b) if you put any substantial work in your onResume - you’re going to see very annoying visual artifacts.

Show Me Some Code

I’ve written some sample code on how to request multiple permissions at once, taking into account the various caveats mentioned above. The goals are:

  • Request multiple permissions up-front.
  • If the user denies any of them, immediately show them your rationale for having the permission (via a String resource).
  • If the user accepts, or ’Never Ask Again’s, them, then don’t request the permissions again - to avoid a lifecycle change.
  • Persist this capability across reboots.

The code roughly follows this flow-chart:

Flow chart of how to ask for Android permissions

And, without further ado, here is the PermissionUtils:

public class PermissionUtils {

    private static final int REQUEST_ALL_PERMISSIONS = 0;
    private static final Map<String, Integer> mPermissionsMap = new HashMap<>();

    static {
        mPermissionsMap.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, R.string.permissions_phone_required);
        mPermissionsMap.put(Manifest.permission.ACCESS_COARSE_LOCATION, R.string.permissions_location_required);
    }

    public static void checkForPermissions(Activity activity) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return;
        }

        Timber.d("Checking for Android M/N 'dangerous' permissions");
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
        final boolean hasPreviouslyRequestedPermissions = sharedPreferences.getBoolean(activity.getString(R.string.key_permissions_requested), false);
        requestAllPermissions(activity, hasPreviouslyRequestedPermissions);
        sharedPreferences.edit().putBoolean(activity.getString(R.string.key_permissions_requested), true).apply();
    }

    public static void onRequestPermissionsResult(Activity activity, int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case REQUEST_ALL_PERMISSIONS:
                // Don't really need the verify, just an optimization
                if (!verifyPermissions(grantResults)) {
                    requestAllPermissions(activity, true);
                }
                break;
        }
    }

    private static void requestAllPermissions(Activity activity, boolean hasPreviouslyRequestedPermissions) {
        Timber.d("Requesting all permissions for SDK %d", Build.VERSION.SDK_INT);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return;
        }

        final StringBuilder permissionsRationale = new StringBuilder();
        final List<String> permissionRequired = new ArrayList<>();
        for (Map.Entry<String, Integer> entry : mPermissionsMap.entrySet()) {

            // If permission already granted, skip it
            final String permission = entry.getKey();
            if (ActivityCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED) {
                Timber.d("Permission %s is already granted", permission);
                continue;
            }

            // If shouldShowRequestPermissionRationale is true, we should tell the user why we need the permissions
            // If it's false, then EITHER we've never requested this permission, OR the user clicked 'never ask again'
            // Pull from SharedPreferences to see if we've previously requested the permission or not
            if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
                Timber.d("Require permission rationale for %s", permission);
                permissionRequired.add(permission);
                final String rationale = activity.getString(entry.getValue());
                permissionsRationale.append(rationale).append("\n\n");
            } else if (!hasPreviouslyRequestedPermissions) {
                permissionRequired.add(permission);
            }
        }

        if (permissionRequired.isEmpty()) {
            Timber.d("All permissions are already granted or denied (never ask again)");
            return;
        }

        final String rationales = permissionsRationale.toString();
        if (Internals.isEmpty(rationales)) {
            Timber.d("No permission rationales required");
            ActivityCompat.requestPermissions(activity,
                    permissionRequired.toArray(new String[permissionRequired.size()]),
                    REQUEST_ALL_PERMISSIONS);
        } else {
            Timber.d("Permission rationales required");
            new AlertDialog.Builder(activity)
                    .setMessage(rationales)
                    .setPositiveButton(android.R.string.ok, null)
                    .setOnDismissListener(dialog -> ActivityCompat.requestPermissions(activity,
                            permissionRequired.toArray(new String[permissionRequired.size()]),
                            REQUEST_ALL_PERMISSIONS))
                    .show();
        }
    }

    /**
     * Check that all given permissions have been granted by verifying that each entry in the
     * given array is of the value {@link PackageManager#PERMISSION_GRANTED}.
     *
     * @see Activity#onRequestPermissionsResult(int, String[], int[])
     */
    private static boolean verifyPermissions(int[] grantResults) {
        // At least one result must be checked.
        if (grantResults.length < 1) {
            return false;
        }

        // Verify that each required permission has been granted, otherwise return false.
        for (int result : grantResults) {
            if (result != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }
}

The easiest way to use it is to do the following (make the request in onCreate or onStart):

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    PermissionUtils.checkForPermissions(this);
}

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

Featured photo credit: XKCD