Android权限适配

动态权限

背景

从Android6.0版本开始google将权限分为普通权限和特殊权限,app必须在AndroidManifest.xml添加引用权限的语句。 在安装apk时安卓会将普通权限授予该app,但特殊权限需要运行时申请。

安卓按照权限类别分为权限组和权限, 每个权限都隶属于一个权限组。 当安卓系统授权一个权限时, 那么该权限所属权限组的权限都会自动被授权。

目前如果app的targetSdkVersion等于21,即按照Android5.0版本特性运行。 技术层面与市场上主流app差距较大, 功能层面也有一些功能可能失效(例如在一些机型上无法打电话、读写SD卡), 根本原因是没适配动态权限。

如何申请动态权限

判断当前手机系统是Android6.0及以上版本, 在Activity/Fragment里申请权限并处理权限结果回调。 这里要说明一下:Fragment是通过Activity申请权限的, 且权限回调onRequestPermissionResult也是Activity调用的Fragment该方法. image 上图是权限申请流程图, 我们看到的权限弹窗对应/packages/apps/PackageInstaller/src/com/android/packageinstaller/permission/ui/GrantPermisssionsActivity.java, 点击“同意”或“不同意”通过PackageManager、AppOpsManager将权限操作更新到PackageManagerService和AppOpsService中。

调用Activity的申请权限方法其实是打开一个系统的Activity,操作结果通过setResult返回过来。

能不能直接调用PackageManager/AppOpsManagerd的方法授权给自己? 显然是不行的, PackageManagerService只允许在AndroidManifest.xml配置coreApp=”true”的应用修改权限,而普通app无法设置coreApp属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int getPermissionFlags(String name, String packageName, int userId) {
if (!sUserManager.exists(userId)) {
return 0;
}
//普通app调用该方法会抛异常
enforceGrantRevokeRuntimePermissionPermissions("getPermissionFlags");
enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false,
"getPermissionFlags");
...
}
private void enforceGrantRevokeRuntimePermissionPermissions(String message) {
if (mContext.checkCallingOrSelfPermission(Manifest.permission.GRANT_RUNTIME_PERMISSIONS)
!= PackageManager.PERMISSION_GRANTED
&& mContext.checkCallingOrSelfPermission(Manifest.permission.REVOKE_RUNTIME_PERMISSIONS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(message + " requires "
+ Manifest.permission.GRANT_RUNTIME_PERMISSIONS + " or "
+ Manifest.permission.REVOKE_RUNTIME_PERMISSIONS);
}
}

如何判断权限

image 如上图所示,判断是否有权限最终会执行到PackagerManagerService的checkUidPermission函数中。

适配动态权限的方式

  1. 基本用法:在Activity、Fragment派生类中添加权限申请和结果回调。 坑:1、在插件中调用View的getContext返回值是PluginContext, 无法通过类型强转调用其附着Activity/Fragment的方法。2、如果界面层级很深,需要逐层添加回调参数。
  2. AOP方式,推荐https://github.com/permissions-dispatcher/PermissionsDispatcher, 在需要权限的函数上添加注解并在构建阶段注入代码。缺点是app插件中有多View控件如BaseCard无法使用。
  3. 第三方库https://github.com/yanzhenjie/AndPermission, 原理:新启动个透明Activity申请权限并保存回调函数到静态变量里,用户操作权限提示框结束后通过回调执行成功、失败逻辑。

示例代码: 为了避免适配动态权限逻辑产生风险, 可以新增if代码块做动态权限逻辑, else分支仍然是现有逻辑。 各业务线可能无法在同一个版本上搞定, 可以按照这种写法先后完成动态权限适配工作,待所有业务线都完成后调整宿主targetSdkVersion到26。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (getBaseContext().getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.M
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AndPermission.with(this)
.runtime()
.permission(permissions)
// .rationale(new RuntimeRationale())
.onGranted(new Action<List<String>>() {
@Override
public void onAction(List<String> permissions) {
toast(R.string.successfully);
}
})
.onDenied(new Action<List<String>>() {
@Override
public void onAction(@NonNull List<String> permissions) {
toast(R.string.failure);
if (AndPermission.hasAlwaysDeniedPermission(MainActivity.this,
permissions)) {
showSettingDialog(MainActivity.this, permissions);
}
}
})
.start();
} else {
//默认有权限, 用现在的业务逻辑
}

注意事项

  1. 适配小米机型动态权限;
  2. Android7.0版本开始使用FileProvider, 需要适配拍照功能;
  3. 适配DownloadManager安装apk;
  4. 用户禁用权限且不再提醒, 需要有个提示框提示用户去应用详情界面里放开权限, 弹窗建议使用CustomDialog(各业务UI样式统一)。
  5. 适配WindowManager悬浮窗;

附录

官方文档 AndPermission

AndPermission库解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 权限申请其实是startActivityForResult的过程, 弹出的权限提示框是安卓系统应用的GrantPermisssionsActivity.java
* 即:申请权限是阻塞的, 在申请权限时当前界面(activity)被GrantPermisssionsActivity盖住了。
* 为了实现回调有2种方式:
* 1、类似于Glide的做法,在当前activity里添加个空fragment申请权限; 优点是无资源文件,可编译成jar。
* 2、新启动个无界面activity申请权限。优点是不限制context类型,缺点是要编译成aar,占位编译只能引用jar,
* 打开activity时要依赖链家routerbus。
*
* 参考新房陈少的实现方式修改,即添加fragment申请权限,返回结果后移除fragment
* 没处理8.0的2个新增权限,貌似贝壳没用到
*
* 备注:
* 只是为了集中管理申请权限逻辑,代码要尽量简单好维护。
* 1、没有判断入参权限是否在AndroidManifest.xml中声明,感觉没啥必要
* 2、申请权限时没有判断是否已经有这个权限了,考虑实际业务场景没添加。
* 3、申请"禁用且不再提醒"的权限时会执行返回deny, 不会弹权限申请框。这时需要引导用户去系统设置放开权限。
*
* 用法:
* LjPermissionUtil.with(MainActivity.this)
* .requestPermissions(Manifest.permission.CAMERA)
* .onCallBack(new LjPermissionUtil.PermissionCallBack() {
* @Override public void onPermissionResult(List<String> granted, List<String> denied) {
* Log.d("brycegao", "onPermissionResult");
* if (denied != null && denied.size() > 0) {
* boolean isAlwayDeny = LjPermissionUtil.isAlwaysDeniedPermission(MainActivity.this,
* denied.get(0));
* //这里要弹出个自定义提示框,引用用户去系统设置里放开权限
* Log.d("brycegao", "");
* }
* }
* }).begin();
* 集成方式
* compileOnly("com.lianjia.common.android:lib_permission:1.0.0-SNAPSHOT") {
* changing = true
* }
*/
坚持原创技术分享,您的支持将鼓励我继续创作!