Wednesday, 23 March 2016

Marshmellow Run-time Permission Model


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


That's how Android's permissions are categorized int two groups. The full list is HERE

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:

  1. onPermissionGranted() -permission requested, and successfully granted by user.
  2. onPermissionDeclined() -permission requested, but declined by user.
  3. onPermissionPreGranted() -requested permission is already granted(allowed via app permissions screen)
  4. onPermissionNeedExplanantion() - requested permission was denied, tell user  why you need it.
  5. onPermissionReallyDeclined() - requested permission was denied,and future requests were denied too. (Can only allow from settings now).
  6. onNoPermissionNeeded() - fallback method for the pre Marshmellow devices. (older permission model).

Firstly, Initialize your PermissionHelper: 

 permissionHelper = PermissionHelper.getInstance(this);  

Then request your permission, say in button's OnClickListener:

 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:

  1. Permission grant
  2. Permission denial



  1. 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: 

  1. Never ask again
  2. 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.




No comments:

Post a Comment