Hello
Friends,
Android
Marshmello 6.0 has introduced a new permission model,which checks and
allows permissions at run time rather then app installation time. Users
now will be able to grant the permissions while running an
application rather
than app installation time.
There
are almost 130
unique permissions available to
Android developers.
An
applications permissions can be controlled from the
App
Info > App permissions section. Earlier
we simply declared permissions in AndroidManifest.xml and we were on
our way. But now apart from that, we need to check every time for a
permission related task. Such as requesting the camera, calendar,
contacts and so on. In addition, if the user denies the request
permission, we need to handle that too.
Good for users,not for developers
In pre marshmellow, all permissions had to be granted during install time. The user has no choice but to accept them. But that has changed now. Runtime permissions give users control over the sensitive information they provide. They choose which apps can access what and when they cannot.
Earlier we simply declared permissions in AndroidManifest.xml file and we were on our way. But now apart from that, we need to check everytime for a permission related task. Such as requesting camera,calendars,contacts and so on. In addition, if user denies the request permission, we need to handle that too.
You might be wonder what about the pre Marshmellow versions devices?
Don't worry.
Android team has already thought about it. If 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 one installed!
In Android 6.0 Marshmellow ,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. Developers has to call it manually. In the case that developer try to call some function that requires permission ,which user has not granted yet, the function will suddenly throw an Exception which will lead to the application crashing.
Automatically Granted permissions (Simplified Permissions)
Traditionally, when developing an Android application, it was required to specify each and every permission needed when calling a specific API. There are some permission that will be automatically granted at install time and will not be able to revoke. We call it Normal Permission(PROTECTION_NORMAL).
Below is the full list of it:
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 this permissions in AndroidManifest.xml and it will work just fine. No need to check for the permissions listed above since it couldn't be revoked.
Getting Started
If there's one way to lessen our worries, its using a library. Like this one. The Permission Helper library will help us implement runtime permissions a whole lot easier !
Start by adding the library to your apps' build.gradle file:
compile 'com.github.k0shk0sh:PermissionHelper:1.0.7'
Basic Permssion workflow
It's important to determine which permissions are required and which have been upgraded to the new permission groups, In this instance, the Geolocator Plugin required both Fine and coarse location permissions, which have been grouped into android.permission-group.LOCATION. It's still necessary to add these two permissions in the AndroidManifest.xml, but the use of these permissions is only requested at runtime, not during install, on Android Marshmellow and above.
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
Normal and Dangerous Permissions
“Cover areas where your app needs to access data or resources outside the app’s sandbox, but where there’s very little risk to the user’s privacy –Normal Permissions”
“Dangerous permissions cover areas where the app wants data or resources that involve the user’s private information –Dangerous Permissions”
Android will automatically grant access for Normal Permissions. So runtime permissions really come into play for Dangerous Permissions.
Below permissions are grouped into Permission Group like table below:
If any permission in a Permission Group is granted. Another permission. 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.
Make your application support new Runtime Permission
Let's make our application support new Runtime Permission perfectly. Start with setting compileSdkVersion and targetSdkVersion to 23.
android {
compileSdkVersion 23
...
defaultConfig {
...
targetSdkVersion 23
...
}
Requesting Runtime permissions
I'm going to request the 'Dangerous' permission of reading the calendar. So for reference sake, I declare it like this.
final String PERMISSION = Manifest.permission.READ_CALENDAR;
Implement OnPermissionCallback in your activity. You'll get the following methods:
- onPermissionGranted() -permission requested, and successfully granted by user.
- onPermissionDeclined() -permission requested, but declined by user.
- onPermissionPreGranted() -requested permission is already granted(allowed via app permissions screen)
- onPermissionNeedExplanantion() - requested permission was denied, tell user why you need it.
- onPermissionReallyDeclined() - requested permission was denied,and future requests were denied too. (Can only allow from settings now).
- onNoPermissionNeeded() - fallback method for the pre Marshmellow devices. (older permission model).
Firstly, Initialize your PermissionHelper:
permissionHelper = PermissionHelper.getInstance(this);
permissionHelper.setForceAccepting(false).request(PERMISSION);
setForceAccepting() helps ensure that the request doesn't force the user to grant permission.
Next, override the Activity's onRequestPermissionResult() method.
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
permissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
Handling Requested Permission
After requesting permission, we need to handle it:
- Permission grant
- Permission denial
- Permission Grant
Override the Activity's OnRequestPermissionResult()
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
permissionHelper.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
If user granted the permission,the onPermissionGranted() is used,otherwise onPermissionDeclined() is called.
In the user flow diagram ,you can see that denied permission can be requested again. If requested again, this time system dialog doesn't appear. But the permission will be added to your app's Permission screen,from where you can manually enable disable requested permissions.
So if you want to request denied permission, use the onPermissionNeedExplaination() method. Use this to tell the user WHY you need that permission. I do it with simple dialog explaining why.
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Request", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
permissionHelper.requestAfterExplanation(permission);
}
})
.create();
dialog.show();
Finally, If you're requesting for a permission that is already granted,then the onPermissionPreGranted() method comes really handy. When i tried to get this new stuff work in practice, two things worth some elaborations:
- Never ask again
- Pre marshmellow handling.
Never asked again
The new permission workflow works like following. Suppose,there is a button that you could press to make a call.
private static final int PERMISSIONS_REQUEST_CALL_PHONE = 201;
private String mManifestPersmission;
private int mRequestCode;
mManifestPersmission = Manifest.permission.CALL_PHONE;
mRequestCode = PERMISSIONS_REQUEST_CALL_PHONE;
int permerssion = ActivityCompat.checkSelfPermission(mActivity, mManifestPersmission); boolean should = ActivityCompat.shouldShowRequestPermissionRationale(mActivity, mManifestPersmission);
if (permerssion != PackageManager.PERMISSION_GRANTED) {
requestPermission();
}
private void requestPermission() {
ActivityCompat.requestPermissions(mActivity, new String[]{mManifestPersmission}, mRequestCode);
}
And when you press the call button first time, it will show the system dialog first time which i mentioned above.If you choose ALLOW, it would just go and call like pre marshmellow;If you choose DENY, it means you have denied permission for user to access, then better we'd show an alert to guide user what's going like below screenshot:
The idea here is ,so user denied a permission, we need to provide some explanation(i.e. rationale) to tell user why we need it. Also we provide two actions RE-TRY and I'M SURE. Click Retry, it would prompt permission asking alert again; Click I'm sure, then it just dismiss silently because user has explicitly known what he is doing.
So if you click Retry or you press call button second time,the permission requesting window will have an option: Never ask again
The tricky part here what happens if user denied Never ask again checked? It turns out in onRequestPermissionResult() , you could query shouldShowRequestPermissionRationale to tell whether user has denied with never ask again.
The code onRequestPermissionResult:
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
if(requestCode == mRequestCode){
Logger.t(mManifestPersmission);
boolean hasSth = grantResults.length > 0;
if(hasSth){
if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//user accepted , make call
Logger.d("Permission granted");
if(this.mAffirmativeCallback != null){
this.mAffirmativeCallback.onPermissionConfirmed();
}
} else if(grantResults[0] == PackageManager.PERMISSION_DENIED) {
boolean should = ActivityCompat.shouldShowRequestPermissionRationale(mActivity, mManifestPersmission);
if(should){
//user denied without Never ask again, just show rationale explanation
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity, R.style.AppCompatAlertDialogStyle);
builder.setTitle("Permission Denied");
builder.setMessage("Without this permission the app is unable to make call.Are you sure you want to deny this permission?");
builder.setPositiveButton("I'M SURE", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
builder.setNegativeButton("RE-TRY", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
requestPermission();
}
});
builder.show();
}else{
//user has denied with `Never Ask Again`, go to settings
promptSettings();
}
}
}
}
}
private void promptSettings() {
AlertDialog.Builder builder = new AlertDialog.Builder(mActivity, R.style.AppCompatAlertDialogStyle);
builder.setTitle(mDeniedNeverAskTitle);
builder.setMessage(mDeniedNeverAskMsg);
builder.setPositiveButton("go to Settings", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
goToSettings();
}
});
builder.setNegativeButton("Cancel", null);
builder.show();
}
private void goToSettings() {
Intent myAppSettings = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + mActivity.getPackageName()));
myAppSettings.addCategory(Intent.CATEGORY_DEFAULT);
myAppSettings.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mActivity.startActivity(myAppSettings);
}
So if showShowRequestPermissionRationale returns false , we will display an alert for go to app settings to allow user manually toggle permissions.
Last point when requested permission, we need a way to distinguish two case when shouldShowRequestPermissionRationale returns false. According to android documentation: shouldShowRequestPermissionRationale
To help find the situations where you need to provide extra explanation, the systemprovides the Activity.showShowRequestPermissionRationale(String) method. This method returns true if the app has requested this permission previously and the user denied the request. That indicates that you should probably explain to the user why you need the permission.
If user turned down the permission request in the past and chose the Don't ask again option in the permission request system dialog, this method returns false. The method also returns false if the device policy prohibits the app from having that permission.
Hence we need to modify request code a littel bit:
private static final int PERMISSIONS_REQUEST_CALL_PHONE = 201;
...
boolean should = ActivityCompat.shouldShowRequestPermissionRationale(mActivity, mManifestPersmission);
if (permerssion != PackageManager.PERMISSION_GRANTED) {
if (should) {
// should show some explanation alert, but here now, just prompt ask again
requestPermission();
} else {
//TWO CASE:
//1. first time - system up - //request window
if(!PrefUtils.hasLocationPermissionBeenRequested(mActivity)){
PrefUtils.markLocationPermissionBeenRequested(mActivity, true); requestPermission();
}else{
//2. second time - user denied with never ask - go to settings promptSettings();
}
}
return;
}
Pre Marshmellow and code reuse
To make it work with per Marshmellow, we could encapsulate those logic in a helper. So in helper,we could specify a general callback for affirmative actions. On Pre Marshmellow , you could just call that callback; on marshmellow, do permission flow:
public class PermissionHelper {
public interface PermissionAffirmativeCallback
{
public void onPermissionConfirmed();
}
private PermissionAffirmativeCallback mAffirmativeCallback;
public static PermissionHelper permissionHelper(PermissionType type,
Activity activity,
PermissionAffirmativeCallback callback){
return new PermissionHelper(type, activity, callback);
}
public PermissionHelper(PermissionType type, Activity activity, PermissionAffirmativeCallback callback) {
if(type == PermissionType.LOCATION){
mManifestPersmission = Manifest.permission.ACCESS_FINE_LOCATION;
mRequestCode = PERMISSIONS_REQUEST_LOCATION;
mDeniedMsg = "Without this permission the app is unable to find your location.Are you sure you want to deny this permission?";
mDeniedNeverAskTitle = "Unable to locate your position";
mDeniedNeverAskMsg = "You have denied the permission for location access. Please go to app settings and allow permission";
}else if(type == PermissionType.CALL){
mManifestPersmission = Manifest.permission.CALL_PHONE;
mRequestCode = PERMISSIONS_REQUEST_CALL_PHONE;
mDeniedMsg = "Without this permission the app is unable to make call.Are you sure you want to deny this permission?";
mDeniedNeverAskTitle = "Unable to make call";
mDeniedNeverAskMsg = "You have denied the permission for calling.. Please go to app settings and allow permission";
}
this.mActivity = activity;
this.mAffirmativeCallback = callback;
checkPermission();
}
private void checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int permerssion = ActivityCompat.checkSelfPermission(mActivity, mManifestPersmission);
boolean should = ActivityCompat.shouldShowRequestPermissionRationale(mActivity, mManifestPersmission);
if (permerssion != PackageManager.PERMISSION_GRANTED) {
//...blablabla
return;
}
}
if(this.mAffirmativeCallback != null){
this.mAffirmativeCallback.onPermissionConfirmed();
}
}
//others
}
Then in activity, you could use like this way:
public class MainActivity extends AppCompatActivity {
protected List<PermissionHelper> mPermissionHelpers = new ArrayList<>();
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
for(PermissionHelper helper : mPermissionHelpers){
helper.onRequestPermissionsResult(requestCode,permissions, grantResults);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mToolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(mToolbar);
PermissionHelper permissionHelper = PermissionHelper.permissionHelper(PermissionType.LOCATION, this,
new PermissionHelper.PermissionAffirmativeCallback() {
@Override
public void onPermissionConfirmed() {
renderMap();
}
});
mPermissionHelpers.add(permissionHelper);
permissionHelper = PermissionHelper.permissionHelper(PermissionType.CALL, this,
new PermissionHelper.PermissionAffirmativeCallback() {
@Override
public void onPermissionConfirmed() {
makeCall();
}
});
mPermissionHelpers.add(permissionHelper);
}
}
Thank you.