With Marshmallow a new permissions model was added to Android which requires developers to take a somewhat different approach to permissions on Android. In this series we’ll take a look at ways to handle requesting permissions both from a technical perspective, and in term of how to provide a smooth user experience.
Previously we looked at how we can incorporate the logic for checking that we have the required permissions, but next we’ll look at how we can actually request the permissions that haven’t been granted.
The happy flow (for us as developers, at least) for this is that we ask the user to explicitly grant us a required permission, the user grantes it and everyone is happy. But, as we discussed previously, we need to cover the cases where the user denies us the permission we’ve requested.
Let’s first look at the logic for the happy path as this is relatively straightforward:
public class PermissionsActivity extends AppCompatActivity { private static final int PERMISSION_REQUEST_CODE = 0; private static final String EXTRA_PERMISSIONS = "com.stylingandroid.permissions.EXTRA_PERMISSIONS"; private static final String EXTRA_FINISH = "com.stylingandroid.permissions.EXTRA_FINISH"; private static final String PACKAGE_URL_SCHEME = "package:"; private PermissionsChecker checker; private boolean requiresCheck; public static void startActivityForResult(Activity activity, int requestCode, String... permissions) { Intent intent = new Intent(activity, PermissionsActivity.class); intent.putExtra(EXTRA_PERMISSIONS, permissions); ActivityCompat.startActivityForResult(activity, intent, requestCode, null); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent() == null || !getIntent().hasExtra(EXTRA_PERMISSIONS)) { throw new RuntimeException("This Activity needs to be launched using the static startActivityForResult() method."); } setContentView(R.layout.activity_permissions); checker = new PermissionsChecker(this); requiresCheck = true; } @Override protected void onResume() { super.onResume(); if (requiresCheck) { String[] permissions = getPermissions(); if (checker.lacksPermissions(permissions)) { requestPermissions(permissions); } else { allPermissionsGranted(); } } else { requiresCheck = true; } } private String[] getPermissions() { return getIntent().getStringArrayExtra(EXTRA_PERMISSIONS); } private void allPermissionsGranted() { setResult(PERMISSIONS_GRANTED); finish(); } . . . }
We have a startActivityForResult()
method which is a simple utility method which other Activities must use to start the PermissionsActivity with a given set of required permissions. So the flow is pretty straightforward once we have the required permissions. However, you may be wondering why we need this – surely we launched the Activity because we already determined that we don’t have the required permissions? The reason that we need this is for some of the following edge cases – if the user leaves this Activity temporarily, and grants the permission in Settings, then returns then this flow will be followed.
But what about when we don’t have the required permissions – we’ll need to request them:
public class PermissionsActivity extends AppCompatActivity { . . . private void requestPermissions(String... permissions) { ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_CODE && hasAllPermissionsGranted(grantResults)) { requiresCheck = true; allPermissionsGranted(); } else { requiresCheck = false; showMissingPermissionDialog(); } } private boolean hasAllPermissionsGranted(@NonNull int[] grantResults) { for (int grantResult : grantResults) { if (grantResult == PackageManager.PERMISSION_DENIED) { return false; } } return true; } . . .
This is the core of the process. We call requestPermissions()
which passes control to the OS to request the permissions that we require. I always make a point of passing both normal and dangerous level permissions here even though we get automatically granted normal permissions. The reasoning behind this is that if what is now considered a normal permission was changed to a dangerous permission in the future then everything would still work.
requestPermissions()
operates in a similar manner to startActivityForResult()
– we pass control to another Activity and then get a callback once that Activity completes. In this case the callback method is onRequestPermissionResult()
. This checks that all of the requested permissions have been granted and either passes control back (either through finish()
or invoking MainActivity, as before), or things become a little more complex: We have requested the required permission, and the user has denied it.
The first time we ask the user for a particular permission following installation the user will be given with a simple choice: Allow or deny. If they deny us permission and we request it again they will also have a checkbox labelled “Never ask again”. If the user checks this and taps “Deny” then any subsequent requests that we make for the same permission will automatically be denied. Unfortunately we have no way of knowing if this has happened we will just get a PERMISSION_DENIED
response. Bearing in mind that this particular permission is critical to the operation of this app we need to further inform the user of why this permission is required and, if they still refuse to grant it, exit the app:
public class PermissionsActivity extends AppCompatActivity { . . . private void showMissingPermissionDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(PermissionsActivity.this); dialogBuilder.setTitle(R.string.help); dialogBuilder.setMessage(R.string.string_help_text); dialogBuilder.setNegativeButton(R.string.quit, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { setResult(PERMISSIONS_DENIED); finish(); } }); dialogBuilder.setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { startAppSettings(); } }); dialogBuilder.show(); } private void startAppSettings() { Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse(PACKAGE_URL_SCHEME + getPackageName())); startActivity(intent); } }
I have deliberately kept the actual text used here somewhat vague because every app will need to explain its precise reasons for needing the permission in question. Also, for the sake of clarity, I have no way of providing different information for different permissions as only one dangerous permission is being requested. However is a real app you will probably need to determine which permission is missing and provide appropriate information for each.
So here we can see the flow when the user initially denies the permission, exits and subsequently grants the permission:
If the user permanently denies permission (by checking “Never ask again”) the flow becomes a little more complex for the user because we cannot provide a link directly to the Permissions Settings page for our app so we need to provide some instructions to help the user:
It would be nice if we could alter the flows between the two kinds of denial. The first video shows that we have to tell the user to alter things through Settings even though we subsequently see that exit and re-launch prompts them once again to allow or deny. Unfortunately we have no way of knowing if the user has selected “Never ask again” so we have to work against the worst-case scenario.
That said we now have a relatively simple, reusable method of requesting the critical permissions for our app. In the final article in this series we’ll look at non-critical permissions and consider some best practise.
The source code for this article is available here.
© 2016, Mark Allison. All rights reserved.
Copyright © 2016 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.