Google Play Services and Runtime Permissions

Along with new platform features, Android 6.0 Marshmallow has a new permissions model that streamlines the app install and auto-update process. Permissions are now requested at runtime instead of before app installation. Additionally, users can choose to deny specific permissions. To give users this flexibility, you need to make sure that your app behaves as expected when an Android Marshmallow user enables or disables a specific permission.

Google Play services 8.1 is the first release to target API level 23 and support runtime permissions in Android 6.0 and above. This means Google Play services itself has runtime permissions that users can choose to deny separately from those permissions specifically requested by your application. Google Play services automatically obtains all permissions it needs to support its APIs--your app won't normally need to request permissions to use them. However, your app should still check and request runtime permissions as necessary and appropriately handle errors in cases where a user has denied Google Play services a permission required for an API your app uses.

To update your apps using Google Play services to handle Android 6.0 permissions, it’s good practice to manage the user’s expectations in setting permissions that the runtime may require. The following best practices will help you avoid potential issues.

Prerequisites

Ensure that your API level and Target SDK are set to 23 or greater. Additionally, ensure that you are using the V4 support library to verify and request permissions. If you don’t have it already, add it to your gradle dependencies:

com.android.support:support-v4:23.0.0

You’ll also need to declare permissions in your AndroidManifest.xml file. There’s no change here--whatever permissions your app has always needed should be declared in your AndroidManifest.xml file with the uses-permission tag. For example:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

Guidelines

Verify permissions before calling APIs

Once you’ve declared the APIs that you want to use in your AndroidManifest.xml file, ensure that you have the required permission before calling an API. This can be done using the checkSelfPermission method of ActivityCompat or ContextCompat.

If the call returns false this means the permissions aren’t granted and you should use requestPermissions to request them. The response to this is returned in a callback which you will see in the next step.

For example:

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
  // Check Permissions Now
  ActivityCompat.requestPermissions(this,
        new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
        REQUEST_LOCATION);
} else {
  // permission has been granted, continue as usual
  Location myLocation =
LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
}

Implement the request permission callback

If the permission your app needs hasn't been granted by the user, the requestPermissions method should be called to ask the user to grant them. The response from the user is captured in the onRequestPermissionsResult callback. Your app should implement this and always check the return values because the request could be denied or canceled. You can also request and check for multiple permissions at once--the following sample only checks for a single permission.

public void onRequestPermissionsResult(int requestCode,
                                       String[] permissions,
                                       int[] grantResults) {
    if (requestCode == REQUEST_LOCATION) {
        if(grantResults.length == 1
           && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // We can now safely use the API we requested access to
            Location myLocation =
                LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
        } else {
            // Permission was denied or request was cancelled
        }
    }
}

Show the permission rationale

If the permissions your app requests are necessary for the core features of the app and the user has previously denied the permission request, your app should display an additional explanation before requesting the permission again. Users are more likely to grant permissions when they understand the why the permission is needed and the immediate benefit for them.

In this case, before the calling requestPermissions, you should call shouldShowRequestPermissionRationale. If it returns true, you should create some UI to display additional context for the permission.

For example, your code might look like this:

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
    // Check Permissions Now
    private static final int REQUEST_LOCATION = 2;

    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.ACCESS_FINE_LOCATION)) {
        // Display UI and wait for user interaction
    } else {
        ActivityCompat.requestPermissions(
              this, new String[]{Manifest.permission.LOCATION_FINE},
    ACCESS_FINE_LOCATION);
    }
} else {
    // permission has been granted, continue as usual
    Location myLocation =
        LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
}

Handle connection failures

If your app uses GoogleApiClient, when you call connect(), Google Play services validates that it has all the necessary permissions needed. In Google Play services 8.1 and later, connect() fails when any permission groups needed by Google Play services itself are missing.

If the call to connect() fails, ensure your app handles the connection failure correctly. If Google Play services itself is missing permissions, you can invoke startResolutionForResult() to initiate the user flow to fix them.

For example:

@Override
public void onConnectionFailed(ConnectionResult result) {
    if (mResolvingError) {
        // Already attempting to resolve an error.
        return;
    } else if (result.hasResolution()) {
        try {
            mResolvingError = true;
            result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
        } catch (SendIntentException e) {
            // There was an error with the resolution intent. Try again.
            mGoogleApiClient.connect();
        }
    } else {
        // Show dialog using GooglePlayServicesUtil.getErrorDialog()
        showErrorDialog(result.getErrorCode());
        mResolvingError = true;
    }
}