Showing posts with label Permissions. Show all posts
Showing posts with label Permissions. Show all posts

Wednesday, October 28, 2015

Tips & Tricks: Dangerous Permissions, Part 2

This entry is going to focus on what happens when a User revokes a permission while the app is backgrounded.  

The user can change a permission status at anytime by going to the settings app=>apps=>pick any app=>Permissions.

If the user grants a permission this way then nothing special happens.  Your app (if it is up) just keeps chugging along.

The real interesting stuff happens when the user revokes a permission for your app.  If the app isn’t running, then nothing happens.  If the app is running, then it will fully re-create.  This means that the old application is destroyed.  It will take down all of its Services and all static data is lost.  It will then start a new version of the app.   This will call the Application objects onCreate.  

The really fun problem comes in when the user foregrounds your app again.  The system will restore to its last known point using any saved bundles.

This can leave your app in a weird state: where it has restarted, but also restored to a state without any breadcrumbs.  As one of my coworkers put it: “It’s like a game where you dies and regenerate right in front of the Boss’s door without any of the goodies you built up along the way”.  Pretty apt way to describe it.

For most apps, this isn’t a big deal because they automatically create the database, etc. as part of app start.  However, it is important to understand to avoid painting yourself into a corner data wise.

I included an demonstration of this in my Example using counters.  I maintain a static counter and a local counter.

   1:  
   2:     /**
   3:      * The static counter.  This is here to show that static data resets when revoking a permission
   4:      * via the settings app.
   5:      */
   6:     private static int sStaticCounter;
   7:  
   8:  
   9:     /**
  10:      * a local counter.  This is here to show that activity restores when revoking a permission via
  11:      * the settings app.
  12:      */
  13:     private int mCounter;
  14:  

  Each time the Toolbar’s “Click Me” is clicked I increment both counters.



   1:  
   2:  
   3:     @Override
   4:     public boolean onOptionsItemSelected(MenuItem item) {
   5:         int id = item.getItemId();
   6:  
   7:         //noinspection SimplifiableIfStatement
   8:         if (id == R.id.action_settings) {
   9:             ++mCounter;
  10:             ++sStaticCounter;
  11:  
  12:             updateCounterUi();
  13:             requestPermissions();
  14:             return true;
  15:         }
  16:  
  17:         return super.onOptionsItemSelected(item);
  18:     }

The value of the counters is shown in the UI:


device-2015-10-10-211138


The local counter value is saved into save state bundle in onSaveInstance().  This value is then restored in the activity’s onCreate method.


So here is the recreation:



  • Start the app, notice that both counters are 0.

start



  • Click the “Click Me”, notice that both counters are 1.

device-2015-10-10-211138



  • Click the “Click Me” in the Snackbar. 
  • Grant the permission.
  • Background the app
  • Start the Settings=> App=> PermissionsExample=>Permissions.
  • Notice that the “Contacts” permission is granted
  • Turn off the “Contacts” permission.

    • At this point, the app recreates

  • Foreground the PermissionsExample app.  Notice that the static data is reset, but the data from the savedInstance bundle restored correctly.

device-2015-10-10-212749


A word of Advice


Regardless of whether a permission is not dangerous, I always check the permission status under the app settings.  This is because some normal permissions are grouped under an umbrella permission that can be revoked.   If any permissions shows up then you have to be prepared for the above scenario to occur. 


For instance in the example, I am using the GET_ACCOUNTS permissions which is technically normal*.  However, it is grouped under the “Contacts” group, which can be revoked. That means that I have to deal with the above situations even though the specific permission I am using cannot be revoked.


*There is an issue reported to Google to make GET_ACCOUNTS dangerous.  As of this writing the change is implemented, but not delivered.


Example


The example can be found in my GitHub PermissionsExample repository.

Wednesday, October 21, 2015

Tips & Tricks: Dangerous Permissions, Part 1

The biggest new feature of Marshmallow is the permissions model change.  A permission is now either dangerous or normal.  Normal permissions behave just like the old permissions.  They are automatically granted at app download and cannot revoked by the User.

Dangerous permissions are the really interesting ones because the user must explicitly grant the app that permission at run-time.   The user can also revoke the permission anytime via the device settings app.

Even if the permission is normal, it can be classed into a grouping that is revocable.  This doesn’t mean that the user can deny your specific permission, but it will cause your app to recreate if the user revokes it in the background.

Basics

If you don’t have permission for a feature, then attempting to use that feature will cause an exception.  To avoid that you need to do the following each time you want to use the feature protected by the permission:

Permission Model - New Page

Obviously it look a little different in code so here is my step-by-step implementation.

Checking Permissions:

This all done in a custom requestPermissions method of the MainActiviity:

   1: private void requestPermissions() {
   2:  
   3:         //use the static ActivityCompat methods if the minimum app level is less than 23.
   4:         if (ActivityCompat.checkSelfPermission(this, permission.GET_ACCOUNTS) == PackageManager.PERMISSION_DENIED) {
   5:  
   6:             //The permission is denied, so ask the OS if we should show a rationale that explains
   7:             //why the permission is needed.
   8:  
   9:             if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission.GET_ACCOUNTS)) {
  10:                 showRationale();
  11:             }
  12:             else {
  13:                 requestAccountsPermission();
  14:             }
  15:         }
  16:         else {
  17:             Toast.makeText(this, "Activity: Permission Already Granted!", Toast.LENGTH_LONG).show();
  18:         }
  19:     }

This method uses the ActivityCompat static methods to check the permissions.  Using those methods eliminates the pesky logic breaks for handling Marshmallow vs pre-Marshmallow devices. 


Basically,  it calls checkSelfPermission to determine if the desired permission is granted.  If it is then is just shows a toast.  If then permission is denied, then it needs to determine whether to show a rationale for the permission to the user.  This is done via the shouldShowRequestPermissionRationale method.  This will normally return true only if the user has denied the permission before.  You are not obligated to show the rationale, but it is a really good idea to give the user an explanation for why they should grant the permission.


How and what to show in the rationale is completely up to you.  In the example, I use a Snackbar.  When the user clicks on the Snackbar’s “Click Me” I request the OS to display the Permission prompt.  This is done in the requestAccountsPermission method.



   1: private void requestAccountsPermission() {
   2:      //use the static ActivityCompat methods if the minimum app level is less than 23.
   3:      ActivityCompat.requestPermissions(MainActivity.this, PERMISSIONS, PERMISSION_REQUEST_CODE);
   4:  }

This method calls the ActivityCompat’s requestPermissions method.  This method takes a string array of the permissions to request and a code to identify the request.



   1:  
   2:     private static final int PERMISSION_REQUEST_CODE = 1;
   3:  
   4:     private static final String PERMISSIONS[] = {permission.GET_ACCOUNTS};

Since the user prompt for the permissions is handled by the OS, the activity will get the response when its onRequestPermissionsResult method is called.



   1:  
   2:     @Override
   3:     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
   4:         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
   5:  
   6:         if (requestCode == PERMISSION_REQUEST_CODE) {
   7:             if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
   8:                 Toast.makeText(this, "Activity: Permission Granted!", Toast.LENGTH_LONG).show();
   9:             }
  10:             else {
  11:                 Toast.makeText(this, "Activity: Permission Denied!", Toast.LENGTH_LONG).show();
  12:             }
  13:         }
  14:  
  15:     }

From here you can proceed with the feature if the permission is granted or discontinue if the permission is still denied.


Fragment Quirk


Fragments can follow the same process for requesting permissions, but be careful cannot using ActivityCompat methods. If the ActivityCompat requestPermissions method is called, then the Fragments onRequestPermissionsResult will not be called. Ever.   Instead,  use the built-in requestPermissions method only.


Example


The example can be found in my GitHub PermissionsExample repository.