If you are an Android developer, you definitely know an important change beginning in Android 6.0 (API 23) is run time permission: users grant permissions to apps while the app is running, not when they install the app. This official training guide explains clearly how you can check for permission and request them at runtime.
Let me list the sample code in the training guide so that you can understand what improvements I would like to do with them:
Check for Permissions
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {
// Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
}
} else {
// permission was granted, yay! Do the task you need to do.
}
Request Permissions
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
}
}
The sample code looks nice but it does not work so well in real-world projects for several reasons:
- The sample structure encourages you to write duplicated code:
- There are two places your app have to perform the real task:
- while permission is already granted - line 17
- while permission is granted after user interaction - line 8
- There are two places your app have to request the permission:
- after user see the explanation and decide to request permission again - line 7
- while no explanation is needed - line 12
- There are two places your app have to perform the real task:
- When permission is finally granted from user, the original context has lost. For example, when my app tries to make a phone call, the context is the phone number. A typical solution is to store the context in class field, such as a
mPhoneNumberToCall
. It is bad and something I would like to avoid. - The sample code does not suggest how to show an explanation asynchronously.
More Elegant Solution
My basic idea is using Runnable
(with help of Java8 lambda) to capture the context and prevent duplication. Here is my way to :
- The original task is wrapped as a
Runnable
that can be stored inHashMap
and retrieved later for execution without repeating its logic. All necessary information can be captured when the lambda is created. - The runnable task is wrapped again by a permission checker which is also an
Runnable
. - Use
Snackbar
to show an explanation
Here is revised version of original sample code:
private HashMap<String, Runnable> mPendingPermissionRequests = new HashMap<>();
private static final int PENDING_PERMISSION_REQUESTS = 0;
private Runnable newPermissionRequester(Activity activity, String permission, String explanation, Runnable task) {
return () -> {
if (ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED) {
task.run();
return;
}
Runnable requestPermission = () -> {
ActivityCompat.requestPermissions(getActivity(), new String[] {permission}, PENDING_PERMISSION_REQUESTS);
mPendingPermissionRequests.put(permission, task);
};
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) {
// Show an explanation to the user *asynchronously* -- don't block this thread waiting for the user's
// response! After the user sees the explanation, try again to request the permission.
Snackbar.make(getView(), explanation, Snackbar.LENGTH_LONG)
.setAction(R.string.request, v -> {
requestPermission.run();
})
.show();
} else {
// No explanation needed, we can request the permission.
requestPermission.run();
}
};
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PENDING_PERMISSION_REQUESTS) {
for (int i = 0; i < permissions.length; i++) {
String permission = permissions[i];
if (grantResults[i] == PackageManager.PERMISSION_GRANTED && mPendingPermissionRequests.containsKey(permission)) {
// permission was granted, yay! Do the task you need to do.
mPendingPermissionRequests.remove(permission).run();
}
}
}
}
Here is an example to make a phone call:
newPermissionRequester(
getActivity(),
Manifest.permission.CALL_PHONE, // permission to request
getString(R.string.call_phone_permission), // explanation to user
() -> startActivity(new Intent(Intent.ACTION_CALL, Uri.fromParts("tel", phone.number, null))))
.run();
Of course you can try extracting a more reusable PermissionRequester
class but this is good enough for me.