Android 6.0 Runtime Permissions

(a code lab)

Code Lab Objective

Experiment with Android 6.0 Runtime Permissions

  • You do the experimenting!
  • Or, let the presenter do the experimenting, while you sit back and relax, as why should you do all the work?

Runtime Permissions

Legacy Apps

(targetSdkVersion < 23)

  • No code changes
  • Behavior akin to "app ops"
    • User can revoke dangerous permissions at runtime
    • Affected APIs return bogus results

Runtime Permissions

Modern Apps

(targetSdkVersion >= 23)

  • Same uses-permission elements
  • Must request dangerous permissions
    • Modal dialog-style UI
    • User can accept, deny, or deny with extreme prejudice

Runtime Permission Mechanics

checkSelfPermission()

  • Context and ContextCompat
  • Given name of permission, tells you if you have it

Runtime Permission Mechanics

requestPermissions()

  • Activity and ActivityCompat
  • Given array of permission names, prompts user to accept/deny them
    • One "pane" to dialog per permission group
    • Get result in onRequestPermissionsResult()

Runtime Permission Mechanics

shouldShowRequestPermissionRationale()

  • Activity and ActivityCompat
  • Given permission name, returns true if...
    • ...you have never asked for this permission, or...
    • ...you asked, the user denied it, but the user has not blocked further requests
  • Use: educate user about upcoming permission request

Possible Runtime Permission States

  • We have never asked for the permission
  • We asked for the permission, and it was granted
  • We asked for the permission, and it was denied
  • We asked for the permission, and it was denied, and the user took out a restraining order against us

Code Lab Time!

Code Lab Resources

  • Starter project
  • PDF with instructions
  • Finished project ...if you just want to see the results

Task #0: Install the Android 6.0 SDK

If you have done this already, great!
If you have not done this already... just sit back and watch!

Task #1: Import the Starter Project

  • RuntimePermTutorial.zip file
  • Unzip in some likely spot
  • File > New... > Import Project from Android Studio

Reviewing the Sample App

  • Landscape and portrait layouts, two big buttons
    • Take a picture
    • Record a video
  • Dependencies
    • Icon button library
    • CWAC-Cam2 for camera stuff

Task #2: Upgrade Gradle for Android 6.0

  • compileSdkVersion 23
  • buildToolsVersion "23.0.0"
  • targetSdkVersion 23

So, What Are We Gonna Do About Permissions?

  • Ask for CAMERA and WRITE_EXTERNAL_STORAGE on first run, as the app is totally useless without them
  • Ask for RECORD_AUDIO when they click the "Record Video" button, as we will not need it before then
  • Ask for whatever permissions we do not hold when they click a button that needs them
  • If they deny permissions, then click a button, explain why we are going to ask for the permissions again

Task #3: Add Fields for First-Run Detection

private static final String PREF_IS_FIRST_RUN="firstRun";
private SharedPreferences prefs;

Task #4: Initialize the Preferences

Add the following to onCreate():
prefs=PreferenceManager.getDefaultSharedPreferences(this);

Task #5: Use the Preferences to Track the First Run

private boolean isFirstRun() {
  boolean result=prefs.getBoolean(PREF_IS_FIRST_RUN, true);
  
  if (result) {
    prefs.edit().putBoolean(PREF_IS_FIRST_RUN, false).apply();
  }
  
  return(result);
}

Task #6: Check for First Run

Add the following to the bottom of onCreate():
if (isFirstRun()) {
  // TODO
}

Task #7: Add Some Static Imports

import static android.Manifest.permission.CAMERA;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;

Task #8: List our Take-Picture Permissions

private static final String[] PERMS_TAKE_PICTURE={
  CAMERA,
  WRITE_EXTERNAL_STORAGE
};

Task #9: Add Our Take-Picture Permission Result Code

private static final int RESULT_PERMS_INITIAL=1339;

Task #10: Add the Support Library for Permission Compatibility Code

dependencies {
  compile 'com.commonsware.cwac:cam2:0.2.+'
  compile 'com.githang:com-phillipcalvin-iconbutton:1.0.1@aar'
  compile 'com.android.support:support-v4:23.0.1'
}

Task #11: Ask for Permission

if (isFirstRun()) {
  ActivityCompat.requestPermissions(this, PERMS_TAKE_PICTURE,
    RESULT_PERMS_INITIAL);
}

Task #12: Add Permission Callback Stub

@Override
public void onRequestPermissionsResult(int requestCode,
  String[] permissions, int[] grantResults) {
    // TODO
}

Task #13: Try It Out!

  • Run the app ...and it should prompt you for permissions
  • Press BACK
  • Run the app again ...and it should not prompt you for permissions
  • Uninstall the app ...so we start from scratch with permissions on the next run

Task #14: Create a Permission-Check Helper Method

private boolean hasPermission(String perm) {
  return(ContextCompat.checkSelfPermission(this, perm)==
    PackageManager.PERMISSION_GRANTED);
}

Task #15: See If We Can Take a Picture

private boolean canTakePicture() {
  return(hasPermission(CAMERA) &&
    hasPermission(WRITE_EXTERNAL_STORAGE));
}

Task #16: No, I Mean See If We Can Take a Picture

public void takePicture(View v) {
  if (canTakePicture()) {
    takePictureForRealz();
  }
}

Task #17: See If We Should Show Some Rationale

private boolean shouldShowTakePictureRationale() {
  return(ActivityCompat.shouldShowRequestPermissionRationale(this,
            CAMERA) ||
         ActivityCompat.shouldShowRequestPermissionRationale(this,
            WRITE_EXTERNAL_STORAGE));
}

Task #18: Use That New Method, As It Is Lonely

public void takePicture(View v) {
  if (canTakePicture()) {
    takePictureForRealz();
  }
  else if (shouldShowTakePictureRationale()) {
    // TODO
  }
}

Task #19: Add a TextView As Our "Breadcrust"

  • @+id/breadcrust
  • visibility set to gone
  • Add to both layout and layout-land

Task #20: Find Our Breadcrust

  • private TextView breadcrust; as field
  • breadcrust=(TextView)findViewById(R.id.breadcrust); in onCreate()

Task #21: Define a Picture Rationale Message

You need to grant us
permission! Tap the Take Picture button again, and we will ask
for permission.

Task #22: Define Another Result Code

private static final int RESULT_PERMS_TAKE_PICTURE=1340;

Task #23: Net the Permissions

  • requestPermissions() prompts user for everything we ask for ...even if they granted the permission to us before
  • This is an icky method, too big for this slide

Task #24: Show Rationale When Needed

"What is it that you want?"
"I want the code!"
"You can't handle the code!"
(...or at least this slide can't)

Task #25: Deal with the Results

  • If we requested permissions, and we can take a picture, go ahead
  • If we requested permissions, cannot take a picture, but should show rationale, do that
  • Otherwise, we're stuck
  • (and, yes, the code is too long for the slide here too)

Task #26: Try It Out!

  • Run the app, reject one of the permissions
  • Tap the picture button, get rationale
  • Tap the picture button again, reject the permission again
  • Uninstall the app

Task #27: Once More, From the Top, with Video

private boolean canRecordVideo() {
  return(canTakePicture() && hasPermission(RECORD_AUDIO));
}

Task #28: Only Record If We Can

public void recordVideo(View v) {
  if (canRecordVideo()) {
    recordVideoForRealz();
  }
}

Task #29: U Can Needz Video Rationale?

private boolean shouldShowRecordVideoRationale() {
  return(shouldShowTakePictureRationale() ||
    ActivityCompat.shouldShowRequestPermissionRationale(this,
      RECORD_AUDIO));
}

Task #30: Ask All the Permissions! And, Um, Results Too!

private static final String[] PERMS_ALL={
  CAMERA,
  WRITE_EXTERNAL_STORAGE,
  RECORD_AUDIO
};
private static final int RESULT_PERMS_RECORD_VIDEO=1341;

Task #31: Really Record the Video. Really.

(pretend that there is some code here)

Task #32: Handle the Results

(did I mention that runtime permissions are tedious?)

Task #33: Configuration Changes. Ugh.

private static final String STATE_BREADCRUST=
  "com.commonsware.android.perm.tutorial.breadcrust";

@Override
protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);

  if (breadcrust.getVisibility()==View.VISIBLE) {
    outState.putCharSequence(STATE_BREADCRUST,
      breadcrust.getText());
  }
}

Task #33½: Configuration Changes. Ugh.

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);

  CharSequence cs=savedInstanceState.getCharSequence(STATE_BREADCRUST);
  
  if (cs!=null) {
    breadcrust.setVisibility(View.VISIBLE);
    breadcrust.setText(cs);
  }
}