Share
New Permissions model on a "Nice" way
In this presentation I will show you how the new permissions model introduced in Android 6.0 works, and how we can educate our users, so they can understand why are we asking for certain permission/s.
To make it even more complete and since educating our users adds more complexity to the new permissions model implementation, I will show you how we can follow Clean architecture and architectural patterns to make your implementation better, decoupled and easy to test, in just a word, "nice".
I've created a sample app to show you what I mentioned above, you can find it here: https://goo.gl/JI15TU feel free to check, clone, modify use, etc. is under Apache License v2.
A video with a demo of the sample app can be found here: https://goo.gl/svx3Vl
Cesar Valiente
Transcript
66 slides
-
Who is this guy? Image Placeholder César Valiente Android Engineer
@Wunderlist (@Microsoft) Android Google Developer Expert (GDE) +CesarValiente @CesarValiente -
Problems • Installing apps based in trust. • Using permissions
wildly. • Accepted permissions that maybe the user will never use (the related functionality won’t be ever used). First and crucial barrier, many users won’t download the app. -
What’s new here? • Permission groups. • “Normal” permissions. •
Permissions are accepted or denied in runtime. • Permissions can be accepted or denied in settings. • Apps have to deal with denied permissions. • Maybe you don’t have to ask for permissions. • User education. -
“Normal” permissions Are designated as PROTECTION_NORMAL, no great risk to
the user’s privacy or security. We have 37 permissions here. Widely used permissions, as INTERNET, VIBRATE and RECEIVE_BOOT_COMPLETED are in this group, so no need to ask for them. -
Apps have to deal with denied permissions • App with
targetSDK <23 on an Android 6.0 device won’t crash. Functions which need permissions will return an empty state value. • App with targetSDK >= 23 not ready to work with permissions, it will crash. Be careful to set your app to targetSDK 23. Implements the new permissions model before to release it. If we deny a permission in settings… -
Do I have to ask for permission? • We can
still use Intents without the need to request a permission for certain functions. • ACTION_INSERT, ACTION_IMAGE_CAPTURE, ACTION_VIDEO_CAPTURE, ACTION_PICK, ACTION_VIEW, ACTION_EDIT, ACTION_DIAL, ACTION_CALL, ACTION_SENDTO, ACTION_SEND, ACTION_SEND_MULTIPLE. -
User education Educate up-front Ask up-front Educate in context Ask
in context Critical Clear Secondary Unclear -
User education (3) Educate in context Provide an immediate benefit
Only ask for relevant permissions -
How can we ask for permission? if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED)
{ // Should we show an explanation? if (shouldShowRequestPermissionRationale( Manifest.permission.READ_CONTACTS)) { // Explain to the user why we need to read the contacts } requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS); // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an // app-defined int constant return; } -
How can we manage the result? @Override public void onRequestPermissionsResult(int
requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case MY_PERMISSIONS_REQUEST_READ_CONTACTS: { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted, yay! do the // calendar task you need to do. } else { // permission denied, boo! Disable the // functionality that depends on this permission. } return; } // other 'switch' lines to check for other // permissions this app might request } } -
Two ways to ask for the same • Using the
framework (targetSdkVersion 23). • Check first for the correct sdk version we are using. • Using the support library v4 or v13 revision 23. • Methods (v4) check internally for the correct sdk version. • Adds support for fragments (v13) and apps that use IPC (v4). -
User wants to do an action that involves a permission
Does the app have the required permission? -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) yes Handle permission result -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result Educate your users up to you (permission denied) -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result Educate your users up to you (permission denied) Has the user already been asked for this permission? no -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) shouldShowRequestPermission Rationale(permission) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result Educate your users up to you (permission denied) Has the user already been asked for this permission? no -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) shouldShowRequestPermission Rationale(permission) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result Educate your users up to you (permission denied) Has the user already been asked for this permission? no Ask for permission no -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) shouldShowRequestPermission Rationale(permission) requestPermissions(new String[] { permission1.. permissionN}, requestCode) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result Educate your users up to you (permission denied) Has the user already been asked for this permission? no Ask for permission no -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) shouldShowRequestPermission Rationale(permission) requestPermissions(new String[] { permission1.. permissionN}, requestCode) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result yes/no Educate your users up to you (permission denied) Has the user already been asked for this permission? no Ask for permission no -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) shouldShowRequestPermission Rationale(permission) requestPermissions(new String[] { permission1.. permissionN}, requestCode) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result yes/no Educate your users up to you (permission denied) Has the user already been asked for this permission? no Has “Don’t ask again” been checked? yes Ask for permission no -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) shouldShowRequestPermission Rationale(permission) requestPermissions(new String[] { permission1.. permissionN}, requestCode) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result yes/no Educate your users up to you (permission denied) Has the user already been asked for this permission? no Has “Don’t ask again” been checked? yes Ask for permission no Show rationale info no -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) shouldShowRequestPermission Rationale(permission) requestPermissions(new String[] { permission1.. permissionN}, requestCode) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result yes/no Educate your users up to you (permission denied) Has the user already been asked for this permission? no Has “Don’t ask again” been checked? yes Ask for permission no Show rationale info no up to you (you can add a “try it” option) -
User wants to do an action that involves a permission
Does the app have the required permission? checkSelfPermission (permission) shouldShowRequestPermission Rationale(permission) requestPermissions(new String[] { permission1.. permissionN}, requestCode) onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults) yes Handle permission result yes/no Educate your users up to you (permission denied) Has the user already been asked for this permission? no Has “Don’t ask again” been checked? yes Ask for permission no Educate your users yes (but is up to you) Show rationale info no up to you (you can add a “try it” option) -
Is it easy? • Yes, the implementation is simple, but
the flow is a mess. • Educate to your users adds more difficult to it. • Reusing permissions in several parts of your app makes this even more complicated. • Then? -
We can do it on a “nice” way • Follow
Clean architecture, but we have to feel free to adapt it following our requirements, tastes, preferences, etc. • Architectural patterns. • Activity manages permissions. Fragments just do stuff. • The permissions functionality is easy to test. • Sample app: PermissionsSample (under Apache License v2). -
What does the sample app do? • Shows how the
new permission model works. • Asks for three permissions. • Educate users. • Applies clean architecture and architectural patterns. • Shows how you can apply this approach to your app/ architecture. -
Concern separation App (Android project) Domain (Java project) Actions (Android
project) Use cases Action -
Action PermissionAction (Interface) ActivityPermission ActionImpl SupportPermission ActionImpl public interface PermissionAction
{ boolean hasSelfPermission(String permission); void requestPermission(String permission, int requestCode); boolean shouldShowRequestPermissionRationale(String permission); } -
Action PermissionAction (Interface) ActivityPermission ActionImpl SupportPermission ActionImpl public interface PermissionAction
{ boolean hasSelfPermission(String permission); void requestPermission(String permission, int requestCode); boolean shouldShowRequestPermissionRationale(String permission); } public class ActivityPermissionActionImpl implements PermissionAction { private Activity activity; public ActivityPermissionActionImpl(Activity activity) { this.activity = activity; } @Override public boolean hasSelfPermission(String permission) { if (!CommonUtils.isMarshmallowOrHigher()) { return true; } return activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; } @Override public void requestPermission(String permission, int requestCode) { activity.requestPermissions(new String[] { permission }, requestCode); } @Override public boolean shoulShowRequestPermissionRationale(String permission) { return activity.shouldShowRequestPermissionRationale(permission); } } -
Action PermissionAction (Interface) ActivityPermission ActionImpl SupportPermission ActionImpl public interface PermissionAction
{ boolean hasSelfPermission(String permission); void requestPermission(String permission, int requestCode); boolean shouldShowRequestPermissionRationale(String permission); } public class ActivityPermissionActionImpl implements PermissionAction { private Activity activity; public ActivityPermissionActionImpl(Activity activity) { this.activity = activity; } @Override public boolean hasSelfPermission(String permission) { if (!CommonUtils.isMarshmallowOrHigher()) { return true; } return activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; } @Override public void requestPermission(String permission, int requestCode) { activity.requestPermissions(new String[] { permission }, requestCode); } @Override public boolean shoulShowRequestPermissionRationale(String permission) { return activity.shouldShowRequestPermissionRationale(permission); } } public class SupportPermissionActionImpl implements PermissionAction { private Activity activity; public SupportPermissionActionImpl(Activity activity) { this.activity = activity; } @Override public boolean hasSelfPermission(String permission) { return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED; } @Override public void requestPermission(String permission, int requestCode) { ActivityCompat.requestPermissions(activity, new String[] { permission }, requestCode); } @Override public boolean shouldShowRequestPermissionRationale(String permission) { return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission); } } -
App (presentation) Interaction BaseActivity MainActivity ContactList Fragment Contact Fragment PermissionPresenter
PermissionCallbacks ContactPresenter ContactListPresenter MainView -
Interaction App (presentation) BaseActivity MainActivity ContactList Fragment Contact Fragment Action
XXXPermissionAction Impl PermissionPresenter PermissionCallbacks ContactPresenter ContactListPresenter MainView -
public interface MainView { //Rest of the methods have been
removed here void requestReadContacts(); void requestSaveImage(); void requestSendSMS(); } -
public interface PermissionCallbacks { void permissionAccepted(int action); void permissionDenied(int
action); void showRationale(int action); } public interface MainView { //Rest of the methods have been removed here void requestReadContacts(); void requestSaveImage(); void requestSendSMS(); } -
public interface PermissionCallbacks { void permissionAccepted(int action); void permissionDenied(int
action); void showRationale(int action); } public class MainActivity extends BaseActivity implements MainView, PermissionCallbacks { private PermissionPresenter permissionPresenter; @Override protected void onCreate(Bundle savedInstanceState) { //Removed content. Here it would be better to use a dependency injector or a service locator //to inject the dependencies. PermissionActionFactory permissionActionFactory = new PermissionActionFactory(this); PermissionAction permissionAction = permissionActionFactory.getPermissionAction(SUPPORT_IMPL); permissionPresenter = new PermissionPresenter(permissionAction, this); //Removed content (check code in the repo) } public interface MainView { //Rest of the methods have been removed here void requestReadContacts(); void requestSaveImage(); void requestSendSMS(); } -
public class MainActivity extends BaseActivity implements MainView, PermissionCallbacks { private
PermissionPresenter permissionPresenter; @Override protected void onCreate(Bundle savedInstanceState) { //Removed content. Here it would be better to use a dependency injector or a service locator //to inject the dependencies. PermissionActionFactory permissionActionFactory = new PermissionActionFactory(this); PermissionAction permissionAction = permissionActionFactory.getPermissionAction(SUPPORT_IMPL); permissionPresenter = new PermissionPresenter(permissionAction, this); //Removed content (check code in the repo) } @Override public void requestReadContacts() { permissionPresenter.requestReadContactsPermission(ACTION_READ_CONTACTS); } //The body of the next functions has been deleted here @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { } public interface PermissionCallbacks { void permissionAccepted(int action); void permissionDenied(int action); void showRationale(int action); } public interface MainView { //Rest of the methods have been removed here void requestReadContacts(); void requestSaveImage(); void requestSendSMS(); } -
public class MainActivity extends BaseActivity implements MainView, PermissionCallbacks { private
PermissionPresenter permissionPresenter; @Override protected void onCreate(Bundle savedInstanceState) { //Removed content. Here it would be better to use a dependency injector or a service locator //to inject the dependencies. PermissionActionFactory permissionActionFactory = new PermissionActionFactory(this); PermissionAction permissionAction = permissionActionFactory.getPermissionAction(SUPPORT_IMPL); permissionPresenter = new PermissionPresenter(permissionAction, this); //Removed content (check code in the repo) } @Override public void requestReadContacts() { permissionPresenter.requestReadContactsPermission(ACTION_READ_CONTACTS); } //The body of the next functions has been deleted here @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { } @Override public void permissionAccepted(int action) { } @Override public void permissionDenied(int action) { } @Override public void showRationale(int action) { } } public interface PermissionCallbacks { void permissionAccepted(int action); void permissionDenied(int action); void showRationale(int action); } public interface MainView { //Rest of the methods have been removed here void requestReadContacts(); void requestSaveImage(); void requestSendSMS(); } -
public class PermissionPresenter { public static final int ACTION_READ_CONTACTS
= 1; public static final int ACTION_SAVE_IMAGE = 2; public static final int ACTION_SEND_SMS = 3; private PermissionAction permissionAction; private PermissionCallbacks permissionCallbacks; public PermissionPresenter(PermissionAction permissionAction, PermissionCallbacks permissionCallbacks) { this.permissionAction = permissionAction; this.permissionCallbacks = permissionCallbacks; } -
public class PermissionPresenter { public static final int ACTION_READ_CONTACTS
= 1; public static final int ACTION_SAVE_IMAGE = 2; public static final int ACTION_SEND_SMS = 3; private PermissionAction permissionAction; private PermissionCallbacks permissionCallbacks; public PermissionPresenter(PermissionAction permissionAction, PermissionCallbacks permissionCallbacks) { this.permissionAction = permissionAction; this.permissionCallbacks = permissionCallbacks; } public void requestReadContactsPermission(int action) { checkAndRequestPermission(action, Manifest.permission.READ_CONTACTS); } public void requestReadContactsPermissionAfterRationale(int action) { permissionAction.requestPermission(Manifest.permission.READ_CONTACTS, action); } -
public class PermissionPresenter { public static final int ACTION_READ_CONTACTS
= 1; public static final int ACTION_SAVE_IMAGE = 2; public static final int ACTION_SEND_SMS = 3; private PermissionAction permissionAction; private PermissionCallbacks permissionCallbacks; public PermissionPresenter(PermissionAction permissionAction, PermissionCallbacks permissionCallbacks) { this.permissionAction = permissionAction; this.permissionCallbacks = permissionCallbacks; } public void requestReadContactsPermission(int action) { checkAndRequestPermission(action, Manifest.permission.READ_CONTACTS); } public void requestReadContactsPermissionAfterRationale(int action) { permissionAction.requestPermission(Manifest.permission.READ_CONTACTS, action); } private void checkAndRequestPermission(int action, String permission) { if (permissionAction.hasSelfPermission(permission)) { permissionCallbacks.permissionAccepted(action); } else { if (permissionAction.shoulShowRequestPermissionRationale(permission)) { permissionCallbacks.showRationale(action); } else { permissionAction.requestPermission(permission, action); } } } -
public class PermissionPresenter { public static final int ACTION_READ_CONTACTS
= 1; public static final int ACTION_SAVE_IMAGE = 2; public static final int ACTION_SEND_SMS = 3; private PermissionAction permissionAction; private PermissionCallbacks permissionCallbacks; public PermissionPresenter(PermissionAction permissionAction, PermissionCallbacks permissionCallbacks) { this.permissionAction = permissionAction; this.permissionCallbacks = permissionCallbacks; } public void requestReadContactsPermission(int action) { checkAndRequestPermission(action, Manifest.permission.READ_CONTACTS); } public void requestReadContactsPermissionAfterRationale(int action) { permissionAction.requestPermission(Manifest.permission.READ_CONTACTS, action); } private void checkAndRequestPermission(int action, String permission) { if (permissionAction.hasSelfPermission(permission)) { permissionCallbacks.permissionAccepted(action); } else { if (permissionAction.shoulShowRequestPermissionRationale(permission)) { permissionCallbacks.showRationale(action); } else { permissionAction.requestPermission(permission, action); } } } public boolean verifyGrantedPermission(int[] grantResults) { for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { return false; } } return true; } } -
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="match_parent" android:layout_width="match_parent" android:orientation="vertical">
<include android:id="@+id/toolbar" layout="@layout/toolbar"/> <FrameLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent"/> <android.support.design.widget.CoordinatorLayout android:id="@+id/main_coordinator_layout" android:layout_width="match_parent" android:layout_height="match_parent"/> <ViewStub android:id="@+id/permission_rationale_stub" android:inflatedId="@+id/permission_rationale" style="@style/PermissionRationale"/> </RelativeLayout> • The components to educate the user are in the layout of the Activity, fragments don’t have to redefine them. • CoordinatorLayout to allow the snackbar being swiped out. • ViewStub for being inflated in runtime. Is used to show the rationale info. Education *CoordinatorLayout bug: https://code.google.com/p/android/issues/detail?id=179600 -
@RunWith(JUnit4.class) public class PermissionPresenterTest { PermissionPresenter permissionPresenter; @Mock PermissionAction permissionAction;
@Mock PermissionCallbacks permissionCallbacks; @Before public void setup() { MockitoAnnotations.initMocks(this); permissionPresenter = new PermissionPresenter(permissionAction, permissionCallbacks); } -
@RunWith(JUnit4.class) public class PermissionPresenterTest { PermissionPresenter permissionPresenter; @Mock PermissionAction permissionAction;
@Mock PermissionCallbacks permissionCallbacks; @Before public void setup() { MockitoAnnotations.initMocks(this); permissionPresenter = new PermissionPresenter(permissionAction, permissionCallbacks); } @Test public void shouldCallAcceptedPermission() { when(permissionAction.hasSelfPermission(READ_CONTACTS)).thenReturn(true); permissionPresenter.requestReadContactsPermission(ACTION_READ_CONTACTS); verify(permissionCallbacks).permissionAccepted(ACTION_READ_CONTACTS); } -
@RunWith(JUnit4.class) public class PermissionPresenterTest { PermissionPresenter permissionPresenter; @Mock PermissionAction permissionAction;
@Mock PermissionCallbacks permissionCallbacks; @Before public void setup() { MockitoAnnotations.initMocks(this); permissionPresenter = new PermissionPresenter(permissionAction, permissionCallbacks); } @Test public void shouldCallAcceptedPermission() { when(permissionAction.hasSelfPermission(READ_CONTACTS)).thenReturn(true); permissionPresenter.requestReadContactsPermission(ACTION_READ_CONTACTS); verify(permissionCallbacks).permissionAccepted(ACTION_READ_CONTACTS); } @Test public void shouldCallShowRationale() { when(permissionAction.hasSelfPermission(READ_CONTACTS)).thenReturn(false); when(permissionAction.shoulShowRequestPermissionRationale(READ_CONTACTS)).thenReturn(true); permissionPresenter.requestReadContactsPermission(ACTION_READ_CONTACTS); verify(permissionCallbacks).showRationale(ACTION_READ_CONTACTS); } -
@RunWith(JUnit4.class) public class PermissionPresenterTest { PermissionPresenter permissionPresenter; @Mock PermissionAction permissionAction;
@Mock PermissionCallbacks permissionCallbacks; @Before public void setup() { MockitoAnnotations.initMocks(this); permissionPresenter = new PermissionPresenter(permissionAction, permissionCallbacks); } @Test public void shouldCallAcceptedPermission() { when(permissionAction.hasSelfPermission(READ_CONTACTS)).thenReturn(true); permissionPresenter.requestReadContactsPermission(ACTION_READ_CONTACTS); verify(permissionCallbacks).permissionAccepted(ACTION_READ_CONTACTS); } @Test public void shouldCallShowRationale() { when(permissionAction.hasSelfPermission(READ_CONTACTS)).thenReturn(false); when(permissionAction.shoulShowRequestPermissionRationale(READ_CONTACTS)).thenReturn(true); permissionPresenter.requestReadContactsPermission(ACTION_READ_CONTACTS); verify(permissionCallbacks).showRationale(ACTION_READ_CONTACTS); } @Test public void shouldRequestPermission() { when(permissionAction.hasSelfPermission(READ_CONTACTS)).thenReturn(false); when(permissionAction.shoulShowRequestPermissionRationale(READ_CONTACTS)).thenReturn(false); permissionPresenter.requestReadContactsPermission(ACTION_READ_CONTACTS); verify(permissionAction).requestPermission(READ_CONTACTS, ACTION_READ_CONTACTS); } } -
Useful docs and acknowledgements • New permissions model. • Android
Patterns Permissions. • Exploring the new android permissions model (Joe Birch). • Everything every Android de must know about new Android’s runtime permission (nuuneoi). • The Clean Architecture (Uncle Bob). • Architecting Android… The clean way? (Fernando Cejas). • MVP for Android: how to organise the presentation layer (Antonio Leiva). • Effective Android UI (Pedro V. Gómez Sánchez). • Engineering Wunderlist for Android (César Valiente & Wunderlist Android team ). And many thanks as well to Fine Cinnamon (OJTHandler) for all the knowledge and good people that can be found there (and funny times too). -
License (cc) 2015 César Valiente. Some rights reserved. This document
is distributed under the Creative Commons Attribution-ShareAlike 3.0 license, available in http:// creativecommons.org/licenses/by-sa/3.0/ All device screenshots used in the “education” section, belong to Google Inc. and can be found here.