Android M 权限最佳实践

详述Android 6.0+ 权限申请相关的处理策略

Posted by Hander on 2016-11-10

万物基于MIUI.

前言

Google在Android 6.0 上开始原生支持应用权限管理,再不是安装应用时的一刀切。权限管理虽然很大程度上增加了用户的可操作性,但是却苦了广大Android开发者。由于权限管理涉及到应用的各个方面,为了避免背锅,很多大厂App的targetSdkVersion仍然停留在22。

现在Android 7.0 已经发布,是时候收拾这个烂摊子了😐😐😐

权限分类

Android的权限分为三类:

  • 普通权限(Normal Permissions)
  • 危险权限(Dangerous Permissions)
  • 特殊权限(Special Permissions)

普通权限(Normal Permissions)

普通权限不会对用户的隐私和安全产生太大的风险,所以只需要在AndroidManifest.xml中声明即可.

普通权限对照表

危险权限(Dangerous Permissions)

Permission Group Permissions
CALENDAR READ_CALENDAR
WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
MICROPHONE RECORD_AUDIO
PHONE READ_PHONE_STATE
CALL_PHONE
READ_CALL_LOG
WRITE_CALL_LOG
ADD_VOICEMAIL
USE_SIP
PROCESS_OUTGOING_CALLS
SENSORS BODY_SENSORS
SMS SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS
STORAGE READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE

危险权限基本都涉及到用户的隐私,诸如拍照、读取短信、写存储、录音等。

便于记忆:涉及隐私的就是危险权限

Android系统将这些危险权限分为9组,获取分组中某个权限的同时也就获取了同组中的其他权限。

例如,在应用中申请READ_EXTERNAL_STORAGE权限,用户同意授权后,则应用同时具有READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限。

危险权限不仅需要在AndroidManifest.xml中注册,还需要动态的申请权限。

下图为某信申请的权限( 九组权限,申请了八组,除了日历…😨😨😨 )

特殊权限(Special Permissions)

Special Permissions
SYSTEM_ALERT_WINDOW 设置悬浮窗
WRITE_SETTINGS 修改系统设置

看权限名就知道特殊权限危险权限更危险,特殊权限需要在manifest中申请并且通过发送Intent让用户在设置界面进行勾选.

申请SYSTEM_ALERT_WINDOW权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final int REQUEST_CODE = 1;
private void requestAlertWindowPermission() {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE) {
if (Settings.canDrawOverlays(this)) {
Log.i(LOGTAG, "onActivityResult granted");
}
}
}

申请WRITE_SETTINGS权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final int REQUEST_CODE_WRITE_SETTINGS = 2;
private void requestWriteSettings() {
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_CODE_WRITE_SETTINGS );
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_WRITE_SETTINGS) {
if (Settings.System.canWrite(this)) {
Log.i(LOGTAG, "onActivityResult write settings granted" );
}
}
}

何时需要动态申请权限?

  1. 危险权限
  2. Android 版本 >= 6.0
  3. targetSdkVersion >= 23

三个条件缺一不可

如果项目的targetSdkVersion < 23, 在Android 6.0+的手机上,会默认给予所有在AndroidManifest.xml中申请的权限。

是不是觉得这样就万事大吉了?

如果用户在应用的权限页面手动收回权限,将会导致应用Crash.💔

虽然系统再次提醒用户不要收回权限,但是点不点“仍然拒绝”还是看用户心情

稳妥的处理当然是遵循Google的权限申请机制。

权限申请的一般流程

API

为方便开发者实现权限管理,Google提供了4个API:

API 作用
checkSelfPermission( ) 判断权限是否具有某项权限
requestPermissions( ) 申请权限
onRequestPermissionsResult( ) 申请权限回调方法
shouldShowRequestPermissionRationale( ) 是否要提示用户申请该权限的缘由

申请权限

以发送短信为例

  1. 在AndroidManifest.xml中声明权限
1
2
3
4
5
6
7
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.SEND_SMS"/>
<application ... >
...
</application>
</manifest>
  1. 判断是否已经获取该权限,若未获取权限,则申请权限
1
2
3
4
5
6
7
8
9
10
11
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.SEND_SMS);
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
// 发送短信
... ...
} else {
// 申请权限
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.SEND_SMS},
PERMISSIONS_REQUEST_SEND_SMS);
}
  1. 接收授权回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSIONS_REQUEST_SEND_SMS: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 已授予权限
doSomething();
} else {
// 申请权限被拒
Toast.show("...");
}
return;
}
}
}

流程图

所谓权限申请就这么简单???EXO ME???

进度条暴露了一切,事情并没有这么简单。

如果用户任性的勾选了“不再询问”,那么在执行requestPermissions( )后,onRequestPermissionsResult( )会永远返回PERMISSION_DENIED,这样应用原本的操作将永远无法执行。

权限申请的正确姿势

上文有提到Google提供了4个新的API,还有一个shouldShowRequestPermissionRationale( )方法没有用到。

shouldShowRequestPermissionRationale( )

Returns Explain
boolean 是否应该提示用户申请该权限的缘由

如果返回为true,一般情况下,应用应该弹出Dialog说明申请该权限的缘由

当第一次申请权限时,shouldShowRequestPermissionRationale( )会返回false,意味着第一次不需要告知用户申请该权限的理由。

如果第一次申请权限被拒,再次申请时,shouldShowRequestPermissionRationale( )会返回true,也就是说用户之前拒绝了该权限的授予,此时应该告知用户应用为什么需要该权限。

注意,此时系统弹出的Dialog会有一个checkbox选项,提示是否不再询问!!!

如果此时,用户勾选了“不再询问”,再次调用“shouldShowRequestPermissionRationale( )”会返回false

综上,shouldShowRequestPermissionRationale( )会在两种情况下返回false,两次的含义并不相同。

  1. 第一次申请权限
  2. 用户拒绝申请权限,且勾选了“不再询问”

shouldShowRequestPermissionRationale( )只会在一种情况下返回true

用户上一次拒绝申请权限,但是并未勾选“不再询问”

下表举例说明了shouldShowRequestPermissionRationale( )的返回

序号 用户是否授予权限 shouldShowRationale( ) 返回 是否勾选“不再询问”
1 false -
2 true
3 true
i true
i + 1 - false -

shouldShowRequestPermissionRationale( )方法名太长,在表格中简写

第i次用户勾选了“不再询问”,同时也没有给予应用权限,则第i + 1次应用将无法唤起请求权限的Dialog,只能引导用户进入设置界面,手动勾选所需权限。

如何判断用户勾选了“不再询问”?

从上面的表格可以看出,如果上次shouldShowRequestPermissionRationale( )返回了true,而这次调用该方法返回了false,则说明用户在上次勾选了“不再询问”。此时,我们需要引导用户进入设置界面进行权限授予。

由于涉及到上一次调用shouldShowRequestPermissionRationale( )的结果,所以需要将其持久化保存,SharedPreferences或者数据库均可。

正确姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void requestPermission(Activity activity, final String permission) {
boolean flag = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
if (getLastRequestState() && !flag) {
//当用户勾选`不再询问`时, 进入设置界面
Uri uri = Uri.fromParts("package", this.getPackageName(), null);
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri);
startActivityForResult(intent, COME_CODE);
} else if (flag) {
//之前有过`拒绝`授权时,提醒用户需要某权限
showRationaleDialog();
//同时保存返回值
SharedPrefsUtils.setBooleanPreference(getApplicationContext(), KEY_RESUEST_SOME_PERMISSION, flag);
} else {
//第一次申请权限时,直接申请权限
ActivityCompat.requestPermissions(activity, new String[]{permission}, REQUEST_PERMISSION_CODE);
}
}

流程图

最佳实践

上面的解决方案是可行的,但是每次申请权限需要依赖于上一次调用shouldShowRequestPermissionRationale( )方法的返回值,如果SharedPreferences被修改或者被删除,会影响正常的申请流程。

Google提供了一个非常好的思路,详见EasyPermissions .

EasyPermissions并没有存储上一次shouldShowRequestPermissionRationale( )的返回值,而是在申请权限被拒后调用shouldShowRequestPermissionRationale( )方法,如果此时返回false则说明用户勾选了“不再询问”。

序号 用户是否授予权限 shouldShowRationale( ) 返回 是否勾选“不再询问” 再次调用shouldShowRationale( )返回
1 false - true
2 true true
3 true true
i true false
i + 1 - false - -

简化判断“不再询问”的条件

  1. 未获得授权
  2. shouldShowRequestPermissionRationale( )返回false

流程图

还能再优化吗?

拜读了EasyPermissions后,我做了一些微小的工作,简单的封装可以减少很多样板代码。

具体见PermissionBestPractice

将通用的操作转移到BaseActivityBaseFragment

每个Activity或者Fragment都需要覆写onRequestPermissionsResult( )方法,这部分可以统一放到BaseActivityBaseFragment

1
2
3
4
5
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionUtils.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}

另外权限授权和拒绝也可以在基类里统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onPermissionGranted(int requestCode, List<String> perms) {
Log.d(TAG, perms.size() + " permissions granted.");
}
@Override
public void onPermissionDenied(int requestCode, List<String> perms) {
Log.e(TAG, perms.size() + " permissions denied.");
if (PermissionUtils.somePermissionsPermanentlyDenied(this, perms)) {
// 勾选了“不再询问”,进入应用设置界面
magic code ...
}
}

这样,在Activity或者Fragment只需做很小的修改就可以实现6.0上的权限管理了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 定义Request Code
private static final int REQUEST_CAMERA_PERMISSION = 0x01;
// 某项操作需要Camera权限
public void doSomethingNeedCamera(View view) {
// 2. 判断是否具有该权限
if (PermissionUtils.hasPermisssions(this, Manifest.permission.CAMERA)) {
openCamera();
} else {
// 3. 如果没有权限,则申请权限
PermissionUtils.requestPermissions(this, getString(R.string.rationale_camera), REQUEST_CAMERA_PERMISSION, Manifest.permission.CAMERA);
}
}
// 4. 为执行操作添加注解
@AfterPermissionGranted(REQUEST_CAMERA_PERMISSION)
private void openCamera() {
// 唤起照相机代码...
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_OPEN_CAMERA);
}
}

如果某项操作需要多个权限?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 定义Request Code
private static final int REQUEST_CALENDAR_AND_CONTACTS = 0x02;
// 某项操作需要多个权限
public void needTwoPermissions(View view) {
String[] perms = new String[]{Manifest.permission.READ_CALENDAR, Manifest.permission.READ_CONTACTS};
// 2. 判断是否具有这些权限
if (PermissionUtils.hasPermisssions(this, perms)) {
twoPermissionsGranted();
} else {
// 3. 如果没有权限,则申请权限
PermissionUtils.requestPermissions(this, getString(R.string.rationale_calendar_and_contacts), REQUEST_CALENDAR_AND_CONTACTS, perms);
}
}
// 4. 为执行操作添加注解
@AfterPermissionGranted(REQUEST_CALENDAR_AND_CONTACTS)
private void twoPermissionsGranted() {
Toast.makeText(this, "授权成功", Toast.LENGTH_SHORT).show();
}

附表

普通权限

Normal Permissions
ACCESS_LOCATION_EXTRA_COMMANDS
ACCESS_NETWORK_STATE
ACCESS_NOTIFICATION_POLICY
ACCESS_WIFI_STATE
BLUETOOTH
BLUETOOTH_ADMIN
BROADCAST_STICKY
CHANGE_NETWORK_STATE
CHANGE_WIFI_MULTICAST_STATE
CHANGE_WIFI_STATE
DISABLE_KEYGUARD
EXPAND_STATUS_BAR
GET_PACKAGE_SIZE
INSTALL_SHORTCUT
INTERNET
KILL_BACKGROUND_PROCESSES
MODIFY_AUDIO_SETTINGS
NFC
READ_SYNC_SETTINGS
READ_SYNC_STATS
RECEIVE_BOOT_COMPLETED
REORDER_TASKS
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
REQUEST_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS

Demo地址

https://github.com/HanderWei/PermissionBestPractice

参考文档

Requesting Permissions at Run Time

EasyPermissions

Android M 运行时权限实践全解析

聊一聊 Android 6.0 的运行时权限