Android M's name was just announced officially days ago. The final version is almost there and would be released not so long.
Although Android is being keep developed but the latest update to Android M is totally different since there is some major change that would change everything like new Runtime Permission. Surprisingly it is not much talked about in Android Developer community even though it is extremely important and may cause some big trouble in the near future.
That's the reason why I decide to blog about this topic today. Everything you need to know about this new Runtime Permission including how to implement it in your code. Let's do it before it's too late.
The New Runtime Permission
Android's permission system is one of the biggest security concern all along since those permissions are asked for at install time. Once installed, the application will be able to access all of things granted without any user's acknowledgement what exactly application does with the permission.
No surprise why there are so many bad guys trying to collect user's personal data through this security weakness and use it in the bad way.
Android team also know this concern. 7 year passed, finally permission system is redesigned. In Android 6.0 Marshmallow, application will not be granted any permission at installation time. Instead, application has to ask user for a permission one-by-one at runtime.
Please note that permission request dialog shown above will not launch automatically. Developer has to call for it manually. In the case that developer try to call some function that requires a permission which user has not granted yet, the function will suddenly throw an Exception which will lead to the application crashing.
Besides, user is also able to revoke the granted permission anytime through phone's Settings application.
You might already feel like there is some cold wind blowing through your arms ... If you are an Android Developer, you will suddenly know that programming logic is totally changed. You cannot just call a function to do the job like previous but you have to check for the permission for every single feature or your application will just simply crash !
Correct. I would not spoil you that it is easy. Although it is a great thing for user but it is truly nightmare for us developer. We have to take coding to the next level or it will surely have a problem in both short-term and long-term.
Anyway this new Runtime Permission will work like described only when we set the application's targetSdkVersion to 23 which mean it is declared that application has already been tested on API Level 23. And this feature will work only on Android 6.0 Marshmallow. The same app will run with same old behavior on pre-Marshmallow device.
What happened to the application that has already been launched?
This new permission system may cause you some panic right now. "Hey ! What's about my application that launched 3 years ago. If it is installed on Android 6.0 device, does this behavior also applied? Will my application also crash?!?"
Don't worry. Android team has already thought about it. If the application's targetSdkVersion is set to less than 23. It will be assumed that application is not tested with new permission system yet and will switch to the same old behavior: user has to accept every single permission at install time and they will be all granted once installed !
As a result, application will run perfectly like previous. Anyway please note that user still can revoke a permission after that ! Although Android 6.0 warn the user when they try to do that but they can revoke anyway.
Next question in your head right now. So will my application crash?
Such a kindness sent from god delivered through the Android team. When we call a function that requires a permission user revoked on application with targetSdkVersion less than 23, no any Exception will be thrown. Instead it will just simply do nothing. For the function that return value, it will return either null or 0 depends on the case.
But don't be too happy. Although application would not be crashed from calling a function. It may still can crash from what that application does next with those returned value.
Good news (at least for now) is these cases may rarely occur since this permission revoking feature is quite new and I believe that just few user will do it. In case they do, they have to accept the result.
But in the long run, I believe that there will be millions of users who turn some permission off. Letting our application not to work perfectly on new device is not acceptable.
To make it work perfectly, you better modify your application to support this new permission system and I suggest you to start doing it right now !
For that application which source code is not successfully modified to support Runtime Permission, DO NOT release it with targetSdkVersion 23 or it will cause you a trouble. Move the targetSdkVersion to 23 only when you pass all the test.
Warning: Right now when you create a new project in Android Studio. targetSdkVersion will be automatically set to the latest version, 23. If you are not ready to make your application fully support the Runtime Permission, I suggest you to step down the targetSdkVersion to 22 first.
Automatically granted permissions
There is some permission that will be automatically granted at install time and will not be able to revoke. We call it Normal Permission (PROTECTION_NORMAL). Here is the full list of them:
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT
Just simply declare those permissions in AndroidManifest.xml
and it will work just fine. No need to check for the permission listed above since it couldn't be revoked.
Make your application support new Runtime Permission
Now it's time to make our application support new Runtime Permission perfectly. Start with setting compileSdkVersion
and targetSdkVersion
to 23.
android {
compileSdkVersion 23
...
defaultConfig {
...
targetSdkVersion 23
...
}
In this example, we try to add a contact with a function below.
private static final String TAG = "Contacts";
private void insertDummyContact() {
// Two operations are needed to insert a new contact.
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2);
// First, set up a new raw contact.
ContentProviderOperation.Builder op =
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
operations.add(op.build());
// Next, set the name for the contact.
op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
"__DUMMY CONTACT from runtime permissions sample");
operations.add(op.build());
// Apply the operations.
ContentResolver resolver = getContentResolver();
try {
resolver.applyBatch(ContactsContract.AUTHORITY, operations);
} catch (RemoteException e) {
Log.d(TAG, "Could not add a new contact: " + e.getMessage());
} catch (OperationApplicationException e) {
Log.d(TAG, "Could not add a new contact: " + e.getMessage());
}
}
The above code requires WRITE_CONTACTS
permission. If it is called without this permission granted, application will suddenly crash.
Next step is to add a permission into AndroidManifest.xml
with same old method.
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
Next step is we have to create another function to check that permission is granted or not. If it isn't then call a dialog to ask user for a permission. Otherwise, you can go on the next step, creating a new contact.
Permissions are grouped into Permission Group like table below.
If any permission in a Permission Group is granted. Another permission in the same group will be automatically granted as well. In this case, once WRITE_CONTACTS
is granted, application will also grant READ_CONTACTS
and GET_ACCOUNTS
.
Source code used to check and ask for permission is Activity's checkSelfPermission
and requestPermissions
respectively. These methods are added in API Level 23.
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
private void insertDummyContactWrapper() {
int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
}
If permission has already been granted, insertDummyContact()
will be suddenly called. Otherwise, requestPermissions
will be called to launch a permission request dialog like below.
No matter Allow or Deny is chosen, Activity's onRequestPermissionsResult
will always be called to inform a result which we can check from the 3rd parameter, grantResults
, like this:
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_PERMISSIONS:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission Granted
insertDummyContact();
} else {
// Permission Denied
Toast.makeText(MainActivity.this, "WRITE_CONTACTS Denied", Toast.LENGTH_SHORT)
.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
This is how Runtime Permission works. Code is quite complicated but be used to it ... To make you application works perfectly with Runtime Permission, you have to handle all the case with the same method shown above.
If you want to punch some wall, it is a good time now ...
Handle "Never Ask Again"
If user denied a permission. In the second launch, user will get a "Never ask again" option to prevent application from asking this permission in the future.
If this option is checked before denying. Next time we call requestPermissions
, this dialog will not be appeared for this kind of permission anymore. Instead, it just does nothing.
However it is quite bad in term of UX if user does something but there is nothing interact back. This case has to be handled as well. Before calling requestPermissions
, we need to check that should we show a rationale about why application needs the being-requested permission through Activity's shouldShowRequestPermissionRationale
method. Source code will now look like this:
final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
private void insertDummyContactWrapper() {
int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel("You need to allow access to Contacts",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return;
}
requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(MainActivity.this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
The result are rational dialog will be shown when this permission is requested for the first time and also be shown if user has ever marked that permission as Never ask again. For the latter case, onRequestPermissionsResult
will be called with PERMISSION_DENIED
without any permission grant dialog.
Done !
Asking for multiple permissions at a time
There is definitely some feature that requires more than one permission. You could request for multiple permissions at a time with same method as above. Anyway don't forget to check the 'Never ask again' case for every single permission as well.
Here is the revised code.
final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
private void insertDummyContactWrapper() {
List<String> permissionsNeeded = new ArrayList<String>();
final List<String> permissionsList = new ArrayList<String>();
if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
permissionsNeeded.add("GPS");
if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
permissionsNeeded.add("Read Contacts");
if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
permissionsNeeded.add("Write Contacts");
if (permissionsList.size() > 0) {
if (permissionsNeeded.size() > 0) {
// Need Rationale
String message = "You need to grant access to " + permissionsNeeded.get(0);
for (int i = 1; i < permissionsNeeded.size(); i++)
message = message + ", " + permissionsNeeded.get(i);
showMessageOKCancel(message,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
});
return;
}
requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
return;
}
insertDummyContact();
}
private boolean addPermission(List<String> permissionsList, String permission) {
if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
permissionsList.add(permission);
// Check for Rationale Option
if (!shouldShowRequestPermissionRationale(permission))
return false;
}
return true;
}
When every single permission got its grant result, the result will be sent to the same callback method, onRequestPermissionsResult
. I use HashMap to make source code looks cleaner and more readable.
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
{
Map<String, Integer> perms = new HashMap<String, Integer>();
// Initial
perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
// Fill with results
for (int i = 0; i < permissions.length; i++)
perms.put(permissions[i], grantResults[i]);
// Check for ACCESS_FINE_LOCATION
if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
// All Permissions Granted
insertDummyContact();
} else {
// Permission Denied
Toast.makeText(MainActivity.this, "Some Permission is Denied", Toast.LENGTH_SHORT)
.show();
}
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
The condition is flexible. You have to set it by your own. In some case, if even one permission is not granted, that feature will be just simply disabled. But in some case, it will still work but with limited feature. There is no suggestion from me. It is all by your design.
Use Support Library to make code forward-compatible
Although the code above works perfectly on Android 6.0 Marshmallow. Unfortunate that it will crash on Android pre-Marshmallow since those functions called are added in API Level 23.
The straight way is you can check Build Version with code below.
if (Build.VERSION.SDK_INT >= 23) {
// Marshmallow+
} else {
// Pre-Marshmallow
}
But code will be even more complicated. So I suggest you to use some help from Support Library v4 which is already prepared for this thing. Replace those functions with these:
- ContextCompat.checkSelfPermission()
No matter application is run on M or not. This function will correctly return PERMISSION_GRANTED
if the permission is granted. Otherwise PERMISSION_DENIED
will be returned.
- ActivityCompat.requestPermissions()
If this function is called on pre-M, OnRequestPermissionsResultCallback will be suddenly called with correct PERMISSION_GRANTED
or PERMISSION_DENIED
result.
- ActivityCompat.shouldShowRequestPermissionRationale()
If this function is called on pre-M, it will always return false
.
ALWAYS replace Activity's checkSelfPermission
, requestPermissions
and shouldShowRequestPermissionRationale
with these functions from Support Library v4. And your application will work perfectly find on any Android version with same code logic. Please note that these functions require some additional parameter: Context or Activity. Nothing special to do, just pass what it wants correctly. Here is what source code will look like.
private void insertDummyContactWrapper() {
int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
Manifest.permission.WRITE_CONTACTS);
if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel("You need to allow access to Contacts",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return;
}
ActivityCompat.requestPermissions(MainActivity.this,
new String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return;
}
insertDummyContact();
}
These methods are also available in Fragment from Android Support Library v4. So please feel free to move these logics into the Fragment.
Shorten source code with 3rd Party Library
You will notice that code is quite complicated. No surprise, there are quite many of 3rd party libraries out there trying to solve this big thing. I gave a try with quite a lot of them and finally found one that satisfy me. It is hotchemi's PermissionsDispatcher.
What is does it exactly the same as I described above but just with shorter and cleaner code. Surely with some trade-off with flexibility. Please give it a try and see if it could be applied in your application. If it couldn't, you can go on the direct way which is also my choice right now.
What will happen if permission is revoked while application is opened?
As mentioned above, a permission can be revoked anytime through phone's Settings.
So what will happen if permission is revoked when application is opened? I have already given it a try and found that application's process is suddenly terminated. Everything inside application just simply stopped (since it is already terminated ...). It sounds make sense to me anyway since if OS allows the application to go on its process, it may summon Freddy to my nightmare. I mean even worse nightmare than currently is ...
Conclusion and Suggestion
I believe that you see the big picture of this new permission system quite clear right now. And I believe that you also see how big issue it is.
However you have no choice. Runtime Permission is already used in Android Marshmallow. We are at the point of no return. Only thing we could do right now is to make our application fully support this new permission system.
Good news is there are only few permission that requires Runtime Permission flow. Most of the frequently-used permissions, for example, INTERNET, are in Normal Permission are automatically granted and you have no need to do anything with them. In conclusion, there are just few part of code that you need to modify.
There are two suggestions to you all:
1) Make Runtime Permission support an urgent issue
2) Don't set application's targetSdkVersion to 23 if your code is not yet supported Runtime Permission. Especially when you create a new project from Android Studio, don't forget to take a look at build.gradle everytime for targetSdkVersion !
Talk about source code modification, I must admit that it is quite a big thing. If code structure is not designed good enough, you may need some serious reconstruction which will surely take some time. Or at least I believe that source code need to be refactored for every single application. Anyway like I said above, we have no choice ...
In the man time, since permission concept is turned upside down. Right now if some permission is not granted, your application need to still be able to work with limited feature. So I suggest you to list all the feature that related to permission you requested. And write down all the case possible, if permission A is granted but permission B is denied, what will happen. Blah blah blah.
Good luck with your code refactoring. Mark it as urgent in your to-do list and start do it today so it will contains no problem on the day Android M is publicly launched.
Hope you find this article helpful and Happy Coding !
More details are available here.
Author: nuuneoi (Android GDE, CTO & CEO at The Cheese Factory) A full-stack developer with more than 6 years experience on Android Application Development and more than 12 years in Mobile Application Development industry. Also has skill in Infrastucture, Service Side, Design, UI&UX, Hardware, Optimization, Cooking, Photographing, Blogging, Training, Public Speaking and do love to share things to people in the world!
|