MIUI权限检测之路

问题与需求

在业务线在使用过程中,发生了一些权限状态丢失的情况。而在我们的群控平台中,如果某些权限的设置状态丢失,则群控平台绝大多数功能不能执行。尤其是HRG业务人员,它们不仅需要群控自动执行功能指令,还不能影响手动操作。所以在权限状态丢失后,产品提出了一个权限检测的需求。这个需求就是在我们的平台中检测应用的权限列表,获取权限的当前状态。用于提示使用者,当前的群控在权限设置上是否已经被允许。

权限介绍

一个Android应用默认情况下是不拥有任何权限的, 在默认情况下, 一个应用是没有权利去进行一些可能会造成不好影响的操作的. 这些不好的影响可能是对其它应用,操作系统,或者是用户.
如果应用需要某些额外的能力,则需要在AndroidManifest.xml中静态地声明相应的权限.
也就是我们在AndroidManifest.xml注册了一系列uses-permission,这些permission就是我们需要申请的权限。

在Android 6.0发布之前, 所有的权限都在安装应用的时候显示给用户,用户选择安装则表示全部接受这些权限, 之后无法撤销对这些权限的授权.
但是从Android 6.0开始, 一部分比较危险的权限需要在程序运行请求用户授权.
至于什么时候需要授权,由应用程序自己决定.而这些权限,就是我们所说的运行时权限。
而对于其他权限,认为不是很危险,所以仍然保持原来的做法,在用户安装应用程序时就予以授权.
需要注意的是,在设置中,对于应用的危险权限,用户可以选择性地进行授权或者关闭.

Dangerous Permissions主要有:

Permission GroupPermissions
CALENDAR
CAMERA
CONTACTS
LOCATION
MICROPHONE
PHONE
SENSORS
SMS
STORAGE

而通过上述危险权限来看,是有了组的概念,只要我们授权了其中一个权限,那就授权当前组的所有全新啊。而我们可以通过下面的方法获取到所有的申请权限:

1
2
3
PackageManager packageManager = this.getPackageManager();
PackageInfo packageInfo =packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
String[] usesPermissionsArray = packageInfo.requestedPermissions;

实现历程

App Ops

Google在SDK19中引入了AppOps的权限管理方式,AppOpsManager是对外的管理接口,真正实现功能的是AppOpsService。AppOpsManager里面有两个比较重要的方法:

1
2
AppOpsManager::checkOp(int op ,int uid ,String packageName) (hide方法)
AppOpsManager::checkOp(String op,int uid ,String packageName)(public方法)

所以我们可以用第二个方式检测权限的状态,uid和packageName我们可以拿到,但是op是什么呢,通过查找我们发现AppOpsManager提供了一个函数permissionToOp,通过这个函数我们可以,我们可以把标题1中获取到的uses-permission转换成op,然后我们就可以利用checkOp获取到权限的状态结果了。
但是在实际的操作中我们发现。发生了一个SecurityException异常。这个是为什么呢,我们通过查看源码发现。permissionToOp可能为空,这个是因为,我们标题1中获取到的permission,不一定有对应的op值。部分展示如下所示:我们发现其中有好多null值。

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
/**
* This maps each operation to the public string constant for it.
* If it doesn't have a public string constant, it maps to null.
*/
private static String[] sOpToString = new String[] {
OPSTR_COARSE_LOCATION,
OPSTR_FINE_LOCATION,
null,
null,
OPSTR_READ_CONTACTS,
OPSTR_WRITE_CONTACTS,
OPSTR_READ_CALL_LOG,
......,
......,
......,
......,
......,
null,
OPSTR_USE_FINGERPRINT,
OPSTR_BODY_SENSORS,
OPSTR_READ_CELL_BROADCASTS,
OPSTR_MOCK_LOCATION,
OPSTR_READ_EXTERNAL_STORAGE,
OPSTR_WRITE_EXTERNAL_STORAGE,
null,
OPSTR_GET_ACCOUNTS,
null,
};

结论:

1、通过permissionToOp和checkOp将不能满足我们的需求。

2、我们发现AndroidManifest.xml中的权限要比小米手机权限列表中的要多。

SecurityCenter

SecurityCenter是什么,它是小米手机权限管理的apk,通过反编译,我们可以一步步的发现小米手机获取权限列表状态的实现方式。粘一下小米的实现核心代码:

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
37
38
39
public PermissionList loadInBackground() {
PermissionList permissionList = new PermissionList();
permissionList.isEnabled = isEnabled();
HashMap cB = packagePermisson(this.mContext, packageName);

ArrayList<PermissionBean> arrayList = new ArrayList();
permissionList.hashMap = cB;
permissionList.PermissionBeanList = arrayList;
if (cB != null) {
Iterable<PermissionGroupInfo> boo = mo5980boo(0);
Iterable<PermissionInfo> bop = mo5981bop(0);
Set keySet = cB.keySet();
HashMap hashMap = new HashMap();
for (PermissionGroupInfo permissionGroupInfo : boo) {
PermissionBean permissionBean = new PermissionBean();
permissionBean.permissionGroupInfo = permissionGroupInfo;
hashMap.put(Integer.valueOf(permissionGroupInfo.getId()), permissionBean);
arrayList.add(permissionBean);
}
for (PermissionInfo permissionInfo : bop) {
if (keySet.contains(Long.valueOf(permissionInfo.getId()))) {
PermissionBean permissionBean2 = (PermissionBean) hashMap.get(Integer.valueOf(permissionInfo.getGroup()));
if (permissionBean2 != null) {
permissionBean2.permissionInfoArrayList.add(permissionInfo);
}
}
}
ArrayList<PermissionBean> arrayList2 = new ArrayList();
for (PermissionBean permissionBean3 : arrayList) {
if (permissionBean3.permissionInfoArrayList.size() == 0) {
arrayList2.add(permissionBean3);
}
}
for (PermissionBean permissionBean32 : arrayList2) {
arrayList.remove(permissionBean32);
}
}
return permissionList;
}

其中,packagePermisson,mo5980boo,mo5981bop,都是通过ContentResolver跨进程获取到的,然后把获取到的权限分组拼装即可。所以,通过这种方式,我们获取到的权限状态和小米手机的权限列表是一致的。

结论:

因为上述实现是基于note 4x手机的安全中心获取的,所以没有问题。但是我们的手机型号不止是4x手机,还有其他小米手机,但我在其他手机上运行的时候,发现报错,出现没有miui.permission.READ_AND_WIRTE_PERMISSION_MANAGER的异常。所以,这种实现方式,不能支撑所有手机。

AppOpsX

是一个第三方权限管理的实现,通过调用appops的权限管理器,控制权限管理。
[Github:https://github.com/8enet/AppOpsX] (https://github.com/8enet/AppOpsX),接入AppOpsX后,可以支撑所有小米手机,但是获取到的权限列表不能和小米手机的权限中心一致,这个会导致产品和运营有一定的误解,并且接入比较困难,可能对今天版本的兼容也会有问题。

checkSelfPermission

ContextCompat

Android 6.0 变更:
此版本引入了一种新的权限模式,如今,用户可直接在运行时管理应用权限。这种模式让用户能够更好地了解和控制权限,同时为应用开发者精简了安装和自动更新过程。用户可为所安装的各个应用分别授予或撤销权限。

对于以 Android 6.0(API 级别 23)或更高版本为目标平台的应用,请务必在运行时检查和请求权限。要确定您的应用是否已被授予权限,请调用新增的 checkSelfPermission() 方法。要请求权限,请调用新增的 requestPermissions() 方法。即使您的应用并不以 Android 6.0(API 级别 23)为目标平台,您也应该在新权限模式下测试您的应用。

如需了解有关在您的应用中支持新权限模式的详情,请参阅使用系统权限。如需了解有关如何评估新模式对应用的影响的提示,请参阅权限最佳做法。

checkSelfPermission
从上图可知,最终的check会到PackageManagerService. checkComponentPermission,而checkComponentPermission最终调用了

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
37
@Override
public int checkUidPermission(String permName, int uid) {
final int userId = UserHandle.getUserId(uid);

if (!sUserManager.exists(userId)) {
return PackageManager.PERMISSION_DENIED;
}

synchronized (mPackages) {
Object obj = mSettings.getUserIdLPr(UserHandle.getAppId(uid));
if (obj != null) {
final SettingBase ps = (SettingBase) obj;
final PermissionsState permissionsState = ps.getPermissionsState();
if (permissionsState.hasPermission(permName, userId)) {
return PackageManager.PERMISSION_GRANTED;
}
// Special case: ACCESS_FINE_LOCATION permission includes ACCESS_COARSE_LOCATION
if (Manifest.permission.ACCESS_COARSE_LOCATION.equals(permName) && permissionsState
.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION, userId)) {
return PackageManager.PERMISSION_GRANTED;
}
} else {
ArraySet<String> perms = mSystemPermissions.get(uid);
if (perms != null) {
if (perms.contains(permName)) {
return PackageManager.PERMISSION_GRANTED;
}
if (Manifest.permission.ACCESS_COARSE_LOCATION.equals(permName) && perms
.contains(Manifest.permission.ACCESS_FINE_LOCATION)) {
return PackageManager.PERMISSION_GRANTED;
}
}
}
}

return PackageManager.PERMISSION_DENIED;
}

这里最重要的就是mSettings,所有的权限信息都通过mSettings来获取,但是mSettings是都做了什么操作呢?看下边的图:
setting
Android6.0之前会吧所有的权限都放置在data/system/packages.xml文件中。Android6.0之后,分为运行时权限跟普通权限,普通权限还是放在data/system/packages.xml中,运行时权限防止在data/system/users/0/runtime-permissions.xml文件中。根据运行时是否动态申请去更新权限。而对于普通权限,一直都为true。
具体的实例信息如下:

packages.xml

1
2
3
4
5
6
7
8
9
10
11
<package name="com.permisison.naga" codePath="/data/app/com.wuba.tinyhand-2" nativeLibraryPath="/data/app/com.wuba.tinyhand-2/lib" primaryCpuAbi="armeabi" publicFlags="944291398" privateFlags="0" ft="166817e48c8" it="16615425c49" ut="166817e51fc" version="2540" userId="10141">
<sigs count="1">
<cert index="11" />
</sigs>
<perms>
<item name="android.permission.WRITE_SETTINGS" granted="true" flags="0" />
<item name="android.permission.RESTART_PACKAGES" granted="true" flags="0" />
<item name="android.permission.SYSTEM_ALERT_WINDOW" granted="true" flags="0" />
</perms>
<proper-signing-keyset identifier="28" />
</package>

runtime-permissions.xml

1
2
3
4
5
6
7
8
<pkg name="com.permisison.naga">
<item name="android.permission.ACCESS_COARSE_LOCATION" granted="true" flags="2" />
<item name="android.permission.READ_PHONE_STATE" granted="true" flags="2" />
<item name="android.permission.SEND_SMS" granted="false" flags="2" />
<item name="android.permission.PROCESS_OUTGOING_CALLS" granted="true" flags="2" />
<item name="android.permission.GET_ACCOUNTS" granted="true" flags="2" />
<item name="android.permission.WRITE_EXTERNAL_STORAGE" granted="true" flags="2" />
</pkg>

结论:

1、checkSelfPermission会分别判断两个xml文件,packages.xml是普通权限,并且一直返回true,runtime-permissions.xml返回的是动态权限的状态,会根据当前设置返回0和-1,0是授予,-1是未被授予权限。

2、国内手机厂商的rom都是修改过了,并且增加了不少自定义的权限,所以这个方法获取的结果是不全的。

PermissionChecker

直接看下关键代码:

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
public static int checkPermission(@NonNull Context context, @NonNull String permission,
int pid, int uid, String packageName) {
if (context.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_DENIED) {
return PERMISSION_DENIED;
}

String op = AppOpsManagerCompat.permissionToOp(permission);
if (op == null) {
return PERMISSION_GRANTED;
}

if (packageName == null) {
String[] packageNames = context.getPackageManager().getPackagesForUid(uid);
if (packageNames == null || packageNames.length <= 0) {
return PERMISSION_DENIED;
}
packageName = packageNames[0];
}

if (AppOpsManagerCompat.noteProxyOp(context, op, packageName)
!= AppOpsManagerCompat.MODE_ALLOWED) {
return PERMISSION_DENIED_APP_OP;
}

return PERMISSION_GRANTED;
}

里面有这么一句判断

1
2
3
4
String op = AppOpsManagerCompat.permissionToOp(permission);
if (op == null) {
return PERMISSION_GRANTED;
}

AppOpsManager返回为null的permission,都返回PERMISSION_GRANTED=0;这个判断就不准确了,如果一个运行时权限,没有在这个里面,那就永远为o了。即使设置了拒绝,也获取不到PERMISSION_DENIED=1。并且,小米手机设置好多10000以上都op自定义权限,肯定是获取不到,并且有些权限还没有permission。

结论:完全不可用。继续尝试

AuthManager

通过上述一些列的验证,都是不可以采用的结论。最终我们再次回到note 4x的实现方案上来。由于再标题2的时候,我们只是调研了获取权限状态的方式,但是没有深入的调研底层实现。所以我们通过一些列跟踪,最终找到具体的实现地方在AuthManager这个app下。
authmanager
结论:

1、由上图可以看出,小米手机完全自己封装了一层权限管理,它的权限状态都存储在了miui_ngen.db数据库里,并且通过PermissionManager管理并定义了权限的op和android.permission值。

2、既然update时完全操作了db和xml三个文件。那我们是不是可以用反射的方法实现呢?经过实现,利用反射checkOpNoThrow虽然可以获取到具体状态值,但是我们只能获取到当前的权限,不能获取到第三方app的权限,报
java.lang.SecurityException: uid 10141 does not have android.permission.UPDATE_APP_OPS_STATS异常。

3、开发过程中,发现某些运行权限op=59的权限不能改变,一致获取到的是0.即授权的状态。
所以利用反射我们也不能实现我们的需求,继续。

runtime-permissions.xml与appops.xml

经过调研后发现,当我们在修改权限的状态值时,不仅appops.xml里面的状态值在该比那,而且运行权限runtime-permissions.xml的值也在改变,那我们是不是可以比较两个文件的内容进行获取状态值呢。 首先,我们需要读取文件,但是我们没有读取文件权限,我们需要copy到sdcard进行解析。解析过程如下图:

verify

miui_ngen.db

miui_ngen.db是什么?我们都知道是一个数据库文件,而且这个是miui的权限状态管理文件。任何的App权限状态都一一对应的存在了这个文件里。从而由上述6种的操作,我们受到了一定的启发,既然能够拿到xml文件,那我们是不是也可以直接拿到这个db文件,然后直接读取db,获取状态信息呢?经过尝试,好吧,权限检测的最终方案决定下来了,我们完全可以直接读取这个db文件,获取到准确的,和miui权限管理中心一一对应,完全匹配的权限状态。

总结:通过以上步骤的实现过程,我们发现由于国内厂商对rom的定制,可能完全修改或者封装了权限的实现和检测机制,我们通过一步步的测试和添坑,最终还是回到了手机原本自身的检测机制上来,并且这种方式是最准确的方式。

对权限的处理场景

如果设备运行的是 Android 6.0(API 级别 23)或更高版本,并且应用的 targetSdkVersion 是 23 或更高版本,则应用在运行时向用户请求权限。用户可随时调用权限,因此应用在每次运行时均需检查自身是否具备所需的权限。

如果设备运行的是 Android 5.1(API 级别 22)或更低版本,并且应用的 targetSdkVersion 是 22 或更低版本,则系统会在用户安装应用时要求用户授予权限。如果将新权限添加到更新的应用版本,系统会在用户更新应用时要求授予该权限。用户一旦安装应用,他们撤销权限的唯一方式是卸载应用。

如果设备运行的是 Android 6.0(API 级别 23)或更高版本,并且应用的 targetSdkVersion 是 22 或更低版本, 此时Android系统会把你申请的全部权限都给你 。 用户依然可以进入App的设置界面把权限关闭 !

参考资料

https://developer.android.com/about/versions/marshmallow/android-6.0-changes?hl=zh-cn#behavior-runtime-permissions
https://blog.csdn.net/lewif/article/details/49124757
https://blog.csdn.net/happylishang/article/details/53813779
https://mp.weixin.qq.com/s/OQRHEufCUXBA3d3DMZXMKQ
https://developer.android.com/reference/android/support/v4/content/ContextCompat#checkSelfPermission(android.content.Context,%20java.lang.String)
https://developer.android.com/reference/android/content/pm/PackageManager#PERMISSION_DENIED
https://www.jianshu.com/p/0ddd129dd32b
https://blog.csdn.net/happylishang/article/details/78222788
https://www.cnblogs.com/neo-java/p/7117482.html