万物基于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_STORAGE 和 WRITE_EXTERNAL_STORAGE 权限。
危险权限不仅需要在AndroidManifest.xml中注册,还需要动态的申请权限。
下图为某信申请的权限( 九组权限,申请了八组,除了日历…😨😨😨 )
¶特殊权限(Special Permissions)
| Special Permissions |
|---|
| SYSTEM_ALERT_WINDOW 设置悬浮窗 |
| WRITE_SETTINGS 修改系统设置 |
看权限名就知道特殊权限比危险权限更危险,特殊权限需要在manifest中申请并且通过发送Intent让用户在设置界面进行勾选.
¶申请SYSTEM_ALERT_WINDOW权限
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权限
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" );
}
}
}
¶何时需要动态申请权限?
- 危险权限
- Android 版本 >= 6.0
- targetSdkVersion >= 23
三个条件缺一不可
如果项目的targetSdkVersion < 23, 在Android 6.0+的手机上,会默认给予所有在AndroidManifest.xml中申请的权限。
是不是觉得这样就完事大吉了?
如果用户在应用的权限页面手动收回权限,将会导致应用Crash.💔
虽然系统再次提醒用户不要收回权限,但是点不点“仍然拒绝”还是看用户心情
稳妥的处理当然是遵循Google的权限申请机制。
¶权限申请的一般流程
¶API
为方便开发者实现权限管理,Google提供了4个API:
| API | 作用 |
|---|---|
| checkSelfPermission( ) | 判断权限是否具有某项权限 |
| requestPermissions( ) | 申请权限 |
| onRequestPermissionsResult( ) | 申请权限回调方法 |
| shouldShowRequestPermissionRationale( ) | 是否要提示用户申请该权限的缘由 |
¶申请权限
以发送短信为例
- 在AndroidManifest.xml中声明权限
...
- 判断是否已经获取该权限,若未获取权限,则申请权限
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);
}
- 接收授权回调
@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,两次的含义并不相同。
- 第一次申请权限
- 用户拒绝申请权限,且勾选了“不再询问”
而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或者数据库均可。
¶正确姿势
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 | - | - |
¶简化判断“不再询问”的条件
- 未获得授权
- shouldShowRequestPermissionRationale( )返回false
¶流程图
¶还能再优化吗?
拜读了EasyPermissions后,我做了一些微小的工作,简单的封装可以减少很多样板代码。
将通用的操作转移到
BaseActivity和BaseFragment中
每个Activity或者Fragment都需要覆写onRequestPermissionsResult( )方法,这部分可以统一放到BaseActivity和BaseFragment中
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionUtils.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
另外权限授权和拒绝也可以在基类里统一处理
@Override
public void onPermissionGranted(int requestCode, List perms) {
Log.d(TAG, perms.size() + " permissions granted.");
}
@Override
public void onPermissionDenied(int requestCode, List perms) {
Log.e(TAG, perms.size() + " permissions denied.");
if (PermissionUtils.somePermissionsPermanentlyDenied(this, perms)) {
// 勾选了“不再询问”,进入应用设置界面
magic code ...
}
}
这样,在Activity或者Fragment只需做很小的修改就可以实现6.0上的权限管理了
// 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. 定义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 |