Android6.0以后,权限分为install时的权限跟运行时权限,若是咱们的targetSdkVersion>=23,install权限同runtime权限是分开的,app也要针对6.0已经作适配,没什么大问题,不管运行在旧版本仍是6.0以后的手机上都ok,这也是Google推荐的适配方案。可是若是targetSdkVersion < 23 ,在6.0以后的手机上就会遇到一些问题,由于在这种状况下默认权限是所有授予的,可是可能会被用户手动取消,而Context的checkSelfPermission权限检查接口也会失效,由于这个API接口6.0以后用的是runtime-permission的模型,而targetSdkVersion < 23 时候,app只有intalled的权限,其granted值一直是true,也能够看作是所有是受权了的,就算在设置里面取消受权也不会影响installed权限的granted,而Context的checkSelfPermission的接口倒是用granted这个值做为受权与否的参考,因此若是用这个接口,那获得的必定是受权了,是不许确的,以下:targetSdkVersion < 23的时候,package信息中的权限包含app申请的所有权限,android
<package name="com.snail.labaffinity" codePath="/data/app/com.snail.labaffinity-1" nativeLibraryPath="/data/app/com.snail.labaffinity-1/lib" publicFlags="944291398" privateFlags="0" ft="15f0f58e548" it="15f0f58e548" ut="15f0f58e548" version="1" userId="10084">
<perms>
<item name="android.permission.ACCESS_FINE_LOCATION" granted="true" flags="0" />
<item name="android.permission.INTERNET" granted="true" flags="0" />
<item name="android.permission.READ_EXTERNAL_STORAGE" granted="true" flags="0" />
<item name="android.permission.ACCESS_COARSE_LOCATION" granted="true" flags="0" />
<item name="android.permission.READ_PHONE_STATE" granted="true" flags="0" />
<item name="android.permission.CALL_PHONE" granted="true" flags="0" />
<item name="android.permission.CAMERA" granted="true" flags="0" />
<item name="android.permission.WRITE_EXTERNAL_STORAGE" granted="true" flags="0" />
<item name="android.permission.READ_CONTACTS" granted="true" flags="0" />
</perms>
<proper-signing-keyset identifier="18" />
</package>
复制代码
这种状况下,该作法就会引起问题,先从源码看一下为何targetSdkVersion < 23 Context 的 checkSelfPermission方法失效,以后再看下在targetSdkVersion < 23 的时候,如何判断6.0的手机是否被受权。markdown
跟踪一下源码发现Context 的 checkSelfPermission最终会调用ContextImp的checkPermission,最终调用app
@Override
public int checkPermission(String permission, int pid, int uid) {
if (permission == null) {
throw new IllegalArgumentException("permission is null");
}
try {
return ActivityManagerNative.getDefault().checkPermission(
permission, pid, uid);
} catch (RemoteException e) {
return PackageManager.PERMISSION_DENIED;
}
}
复制代码
最终请求ActivityManagerService的checkPermission,通过预处理跟中转最后会调用PackageManagerService的checkUidPermissionide
@Override
public int checkUidPermission(String permName, int uid) {
final int userId = UserHandle.getUserId(uid);
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;
}
if (Manifest.permission.ACCESS_COARSE_LOCATION.equals(permName) && permissionsState
.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION, userId)) {
return PackageManager.PERMISSION_GRANTED;
}
} ... }
return PackageManager.PERMISSION_DENIED;
}
复制代码
PackageManagerService会从mSettings全局变量中获取权限,而后进一步验证权限是否被授予函数
public boolean hasPermission(String name, int userId) {
enforceValidUserId(userId);
if (mPermissions == null) {
return false;
}
PermissionData permissionData = mPermissions.get(name);
return permissionData != null && permissionData.isGranted(userId);
}
复制代码
这里的检查点只有两点,第一个是是否有这个权限,第二是是不是Granted,对于targetSdkVersion<23的全部的权限都在packages.xml中,grante一直是true,没法被跟新,为何没法被更新呢?看一下6.0以后的受权与取消受权的函数,首先看一个变量mAppSupportsRuntimePermissionsoop
mAppSupportsRuntimePermissions = packageInfo.applicationInfo
.targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1;
mAppOps = context.getSystemService(AppOpsManager.class);
复制代码
mAppSupportsRuntimePermissions定义在AppPermissionGroup中,6.0以后权限都是分组的,对于targetSdkVersion<23的APP来讲,很明显是不支持动态权限管理的,那么受权跟取消受权函数就很不同以下: 受权函数ui
public boolean grantRuntimePermissions(boolean fixedByTheUser, String[] filterPermissions) {
final int uid = mPackageInfo.applicationInfo.uid;
for (Permission permission : mPermissions.values()) {
if (filterPermissions != null
&& !ArrayUtils.contains(filterPermissions, permission.getName())) {
continue;
}
<!--关键点1 若是支持,也便是targetSdkVersion>23那走6.0动态权限管理那一套-->
if (mAppSupportsRuntimePermissions) {
// Do not touch permissions fixed by the system.
if (permission.isSystemFixed()) {
return false;
}
// Ensure the permission app op enabled before the permission grant.
if (permission.hasAppOp() && !permission.isAppOpAllowed()) {
permission.setAppOpAllowed(true);
mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_ALLOWED);
}
// Grant the permission if needed.
if (!permission.isGranted()) {
permission.setGranted(true);
<!--关键点2更新其runtime-permission.xml 中granted值-->
mPackageManager.grantRuntimePermission(mPackageInfo.packageName,
permission.getName(), mUserHandle);
}
...
} else {
if (!permission.isGranted()) {
continue;
}
int killUid = -1;
int mask = 0;
if (permission.hasAppOp()) {
if (!permission.isAppOpAllowed()) {
permission.setAppOpAllowed(true);
<!--关键点3 设置为AppOpsManager.MODE_ALLOWED-->
mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_ALLOWED);
killUid = uid;
}
}
<!--关键点4 更新其PermissionFlags-->
if (mask != 0) {
mPackageManager.updatePermissionFlags(permission.getName(),
mPackageInfo.packageName, mask, 0, mUserHandle);
}
}
}
return true;
}
复制代码
能够看出6.0以后的手机,针对targetSdkVersion是否高于23作了不一样处理,若是targetSdkVersion>=23支持动态权限管理,那就更新动态权限,并将其持久化到runtime-permission.xml中,并更新其granted值,若是targetSdkVersion<23 ,也便是不知道6.0的动态管理,那就只更新AppOps,这是4.3引入的老的动态权限管理模型,不过这里主要是将权限持久化到appops.xml中,不过对于其granted的值是没有作任何更新的,仅仅是更新了packages.xml中的flag,这个flag能够配合appops.xml标识是否被受权(对于targetSdkVersion<23的适用),以上就是为何context checkSelfPermission会失效的缘由,涉及代码不少,不一一列举,对于取消受权revokeRuntimePermissions函数,模型同样,不在赘述,那下面看第二个问题,如何检查targetSdkVersion<23 app 在6.0以上手机的权限呢? Google给了一个兼容类PermissionChecker,这个类能够间接使用AppOpsService那一套逻辑,获取到权限是否被授予。this
targetSdkVersion < 23的时候,6.0权限检查API失效了,不过经过上面的分析指导,在设置中权限的操做仍然会被存储内存及持久化到appops.xml文件中,这里就是走的AppOpsService那一套,AppOpsService能够看作6.0为了兼容老APP而保留的一个附加的权限管理模型,在6.0以后的系统中,能够看作runtime权限管理的补充,其实AppOpsService这套在4.3就推出了,不过不太灵活,基本没啥做用,以前只用到了通知管理。看一下Google提供的一个兼容类PermissionChecker如何作的:spa
public static int checkPermission(@NonNull Context context, @NonNull String permission,
int pid, int uid, String packageName) {
<!--对于targetSdkVersion < 23 必定是true-->
if (context.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_DENIED) {
return PERMISSION_DENIED;
}
String op = AppOpsManagerCompat.permissionToOp(permission);
<!--看看这个权限是否是可以操做,动态受权与取消受权 若是不能,说明权限一直有-->
if (op == null) {
return PERMISSION_GRANTED;
}
<!--若是可以取消受权,就看如今是否是处于权限被容许的状态,若是不是,那就是用户主动关闭了权限-->
if (AppOpsManagerCompat.noteProxyOp(context, op, packageName)
!= AppOpsManagerCompat.MODE_ALLOWED) {
return PERMISSION_DENIED_APP_OP;
}
return PERMISSION_GRANTED;
}
复制代码
对于6.0以后的手机AppOpsManagerCompat.noteProxyOp会调用AppOpsManager23的noteProxyOp,code
private static class AppOpsManagerImpl {
public String permissionToOp(String permission) {
return null;
}
public int noteOp(Context context, String op, int uid, String packageName) {
return MODE_IGNORED;
}
public int noteProxyOp(Context context, String op, String proxiedPackageName) {
return MODE_IGNORED;
}
}
private static class AppOpsManager23 extends AppOpsManagerImpl {
@Override
public String permissionToOp(String permission) {
return AppOpsManagerCompat23.permissionToOp(permission);
}
@Override
public int noteOp(Context context, String op, int uid, String packageName) {
return AppOpsManagerCompat23.noteOp(context, op, uid, packageName);
}
@Override
public int noteProxyOp(Context context, String op, String proxiedPackageName) {
return AppOpsManagerCompat23.noteProxyOp(context, op, proxiedPackageName);
}
}
复制代码
上面的是6.0以前对应的API,下面的是6.0及其以后对应的接口,AppOpsManagerCompat23.noteProxyOp会进一步调用AppOpsManager的noteProxyOp向AppOpsService发送请求
public static int noteProxyOp(Context context, String op, String proxiedPackageName) {
AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
return appOpsManager.noteProxyOp(op, proxiedPackageName);
}
复制代码
最后看一下AppOpsService如何检查权限
private int noteOperationUnchecked(int code, int uid, String packageName,
int proxyUid, String proxyPackageName) {
synchronized (this) {
Ops ops = getOpsLocked(uid, packageName, true);
Op op = getOpLocked(ops, code, true);
if (isOpRestricted(uid, code, packageName)) {
return AppOpsManager.MODE_IGNORED;
}
op.duration = 0;
final int switchCode = AppOpsManager.opToSwitch(code);
UidState uidState = ops.uidState;
if (uidState.opModes != null) {
final int uidMode = uidState.opModes.get(switchCode);
op.rejectTime = System.currentTimeMillis();
return uidMode;
}
}
final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op;
if (switchOp.mode != AppOpsManager.MODE_ALLOWED) {
op.rejectTime = System.currentTimeMillis();
return switchOp.mode;
}
op.time = System.currentTimeMillis();
op.rejectTime = 0;
op.proxyUid = proxyUid;
op.proxyPackageName = proxyPackageName;
return AppOpsManager.MODE_ALLOWED;
}
}
复制代码
UidState能够看作每一个应用对应的权限模型,这里的数据是有一部分是从appops.xml恢复回来,也有部分是在更新权限时候加进去的,这部分变化最终都要持久化到appops.xml中去,不过持久化比较滞后,通常要等到手机更新权限后30分钟才会持久化到appops.xml中,这里的数据通常是在启动的时候被恢复重建,在启动ActivityManagerService服务的时候,会在其构造函数总启动AppOpsService服务:
public ActivityManagerService(Context systemContext) {
...
mAppOpsService = new AppOpsService(new File(systemDir, "appops.xml"), mHandler);
...}
复制代码
在AppOpsService的构造函数中会将持久化到appops.xml中的权限信息恢复出来,并存到内存中去,
public AppOpsService(File storagePath, Handler handler) {
mFile = new AtomicFile(storagePath);
mHandler = handler;
// 新建的时候就会读取
readState();
}
复制代码
readState就是将持久化的UidState数据给从新读取出来,以下mFile其实就是appops.xml的文件对象
void readState() {
synchronized (mFile) {
synchronized (this) {
FileInputStream stream;
try {
stream = mFile.openRead();
} catch (FileNotFoundException e) {
}
boolean success = false;
mUidStates.clear();
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(stream, StandardCharsets.UTF_8.name());
int type;
int outerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals("pkg")) {
readPackage(parser);
} else if (tagName.equals("uid")) {
readUidOps(parser);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
success = true;
...}
复制代码
读取以后,当用户操做权限的时候,也会随机的更新这里的标记,只看下targetSdkVersion<23的,
public boolean grantRuntimePermissions(boolean fixedByTheUser, String[] filterPermissions) {
final int uid = mPackageInfo.applicationInfo.uid;
for (Permission permission : mPermissions.values()) {
if (filterPermissions != null
&& !ArrayUtils.contains(filterPermissions, permission.getName())) {
continue;
}
<!--关键点1 若是支持,也便是targetSdkVersion>23那走6.0动态权限管理那一套-->
if (mAppSupportsRuntimePermissions) {
...
} else {
if (!permission.isGranted()) {
continue;
}
int killUid = -1;
int mask = 0;
if (permission.hasAppOp()) {
if (!permission.isAppOpAllowed()) {
permission.setAppOpAllowed(true);
<!--关键点3 设置为AppOpsManager.MODE_ALLOWED-->
mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_ALLOWED);
killUid = uid;
}
}
if (mask != 0) {
mPackageManager.updatePermissionFlags(permission.getName(),
mPackageInfo.packageName, mask, 0, mUserHandle);
}
}
}
return true;
}
复制代码
拿受权的场景来讲,其实关键就是 mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_ALLOWED)函数,这个函数会更新AppOpsService中对于权限的标记,并将权限是否授予的信息持久化到appops.xml及packages.xml,不一样版本可能有差异,有可能须要appops.xml跟packages.xml配合才能肯定是否授予权限,具体没深究,有兴趣能够自行分析。
@Override
public void setUidMode(int code, int uid, int mode) {
if (Binder.getCallingPid() != Process.myPid()) {
mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
Binder.getCallingPid(), Binder.getCallingUid(), null);
}
verifyIncomingOp(code);
code = AppOpsManager.opToSwitch(code);
synchronized (this) {
final int defaultMode = AppOpsManager.opToDefaultMode(code);
<!--更新操做权限-->
UidState uidState = getUidStateLocked(uid, false);
if (uidState == null) {
if (mode == defaultMode) {
return;
}
uidState = new UidState(uid);
uidState.opModes = new SparseIntArray();
uidState.opModes.put(code, mode);
mUidStates.put(uid, uidState);
scheduleWriteLocked();
} else if (uidState.opModes == null) {
if (mode != defaultMode) {
uidState.opModes = new SparseIntArray();
uidState.opModes.put(code, mode);
scheduleWriteLocked();
}
} else {
if (uidState.opModes.get(code) == mode) {
return;
}
if (mode == defaultMode) {
uidState.opModes.delete(code);
if (uidState.opModes.size() <= 0) {
uidState.opModes = null;
}
} else {
uidState.opModes.put(code, mode);
}
<!--持久化到appops.xml-->
scheduleWriteLocked();
}
}
...
}
复制代码
这里有一点注意:scheduleWriteLocked并非当即执行写操做,而是比更新内存滞后,通常滞后30分钟
static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000;
复制代码
30分钟才会去更新 ,不过内存中都是最新的 ,若是直接删除appops.xml,而后意外重启,好比adb reboot bootloader,那么你的全部AppOpsService权限标记将会被清空,通过验证,是符合预期的,也就说,targetSdkVersion<23的状况下,Android6.0以上的手机,它的权限操做是持久化在appops.xml中的,通常关机的时候,会持久化一次,若是还没来得及持久化,异常关机,就会丢失,这点同runtime-permission相似,异常关机也会丢失,不信能够试验一下 。
targetSdkVersion>=23系统已经提供了比较合理的检测手段,PermisionChecker的checkPermission就能够,不过,这里须要注意的是,AppOpsService对于targetSdkVersion>=23的时候就不能用了,这里多是Android的一个bug,当targetSdkVersion>=23而SDK_Version>=23的,对于AppOpsService,权限的授予跟撤销不是配对的,以下,先简单看下受权:
public boolean grantRuntimePermissions(boolean fixedByTheUser, String[] filterPermissions) {
final int uid = mPackageInfo.applicationInfo.uid;
for (Permission permission : mPermissions.values()) {
if (mAppSupportsRuntimePermissions) {
<!--关键点1 同时更新runtim-permission及Appops-->
if (permission.hasAppOp() && !permission.isAppOpAllowed()) {
permission.setAppOpAllowed(true);
mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_ALLOWED);
}
if (!permission.isGranted()) {
permission.setGranted(true);
mPackageManager.grantRuntimePermission(mPackageInfo.packageName,
permission.getName(), mUserHandle);
}
} else {
if (!permission.isGranted()) {
continue;
}
int killUid = -1;
int mask = 0;
<!--关键点2 更新Appops-->
if (permission.hasAppOp()) {
if (!permission.isAppOpAllowed()) {
permission.setAppOpAllowed(true);
// Enable the app op.
mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_ALLOWED);
killUid = uid;
}
...
}
}
return true;
}
复制代码
可见,对于6.0的系统,不管targetSdkVersion是否>=23,在受权的时候,都会更新appops.xml,那取消受权呢?
public boolean revokeRuntimePermissions(boolean fixedByTheUser, String[] filterPermissions) {
final int uid = mPackageInfo.applicationInfo.uid;
for (Permission permission : mPermissions.values()) {
...
if (mAppSupportsRuntimePermissions) {
if (permission.isSystemFixed()) {
return false;
}
// Revoke the permission if needed.
if (permission.isGranted()) {
permission.setGranted(false);
mPackageManager.revokeRuntimePermission(mPackageInfo.packageName,
permission.getName(), mUserHandle);
}
<!--关键点1 这里没有使用mAppOps.setUidMode更新appops.xml文件->
} else {
// Legacy apps cannot have a non-granted permission but just in case.
if (!permission.isGranted()) {
continue;
}
int mask = 0;
int flags = 0;
int killUid = -1;
if (permission.hasAppOp()) {
if (permission.isAppOpAllowed()) {
<!--关键点2 这里使用mAppOps.setUidMode更新appops.xml文件->
mAppOps.setUidMode(permission.getAppOp(), uid, AppOpsManager.MODE_IGNORED);
killUid = uid;
}
...
}
}
return true;
}
复制代码
看关键点1 ,若是targetSdkVersion>=23在取消受权的时候,是不会更新appops.xml的,只有在targetSdkVersion<23的时候,才会向关键点2,撤销受权。也就是说对于targetSdkVersion>=23的时候,不要用AppOpsManager了。
对于Android6.0如下的手机,不须要关心targetVersion。先说个本身验证的结果:基本无法检测,同时也不须要检测,就算检测出来也没有多大意义,由于,触发时机是在真正的调用服务时候。对于4.3到6.0以前的国产ROM,虽然采用AppopsManagerService,可是并未按照Google的模型对全部权限进行适配,在这个模型下,也就适配了两个权限,
Google发行版的APPOpsService,基本是把整个鉴权逻辑给屏蔽了,经过CM的源码,课对这部分代码窥探一斑,若是整个权限都采用4.3权限管理模型,在拒绝一项权限的时候,这个操做会被持久化到appops.xml中去,可是具体看下去,其实并非如此,这种机制只对以上两个权限生效:
<pkg n="com.xxx">
<uid n="10988">
<!--关键点1-->
<op n="11" m="1" t="1513145979969" r="1521550658067" />
<op n="12" t="1521550651593" />
<op n="29" t="1521550682769" />
<pkg n="com.wandoujia.phoenix2.usbproxy">
<uid n="10969">
<op n="4" t="1517279031173" />
<!--关键点2-->
<op n="11" m="1" t="1510889291834" r="1517279030708" />
<op n="14" t="1517293452801" />
<!--关键点3-->
<op n="24" m="1" />
<op n="40" t="1513599239364" d="600011" />
复制代码
国产rom中,假如你拒绝受权位置权限,按照AppOpsService模型,该操做应该被持久化到appops.xml中去,可是,结果并不是如此,也就是说,对于其余权限,国产ROM应该是本身糊弄了一套持久管理,持久化Android系统API没法访问的地方,仅仅为自身ROM可见。appops.xml真正被系统使用时从Android6.0开始,其实Android6.0是有两套权限管理的,这其实很混乱,不知道Google怎么想的,不过6.0彷佛也有漏洞:权限的授予跟回收权限好像并不配对。
那么这就带来了一个问题,在Android4.3到Android6.0之间的版本,并无同一个API来检测是否获取了某种权限,由于你动态更新的权限并未持久化到appops.xml中去。对于Android6.0以前的ROM,虽然不能检测,但彻底能够直接用服务,不会崩溃,由于若是真须要鉴权,它的鉴权时机实际上是在服务使用的时候。AppopsManager在6.0以前,只能用来检测通知,可能还有悬浮窗。
public boolean selfPermissionGranted(Context context, String permission) {
boolean ret = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (targetSdkVersion >= Build.VERSION_CODES.M) {
ret = context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
} else {
ret = PermissionChecker.checkSelfPermission(context, permission) == PermissionChecker.PERMISSION_GRANTED;
}
}else{
return true;
}
return ret;
}
复制代码
或者所有采用PermissionChecker的checkSelfPermission:
public boolean selfPermissionGranted(Context context, String permission) {
return PermissionChecker.checkSelfPermission(context, permission) == PermissionChecker.PERMISSION_GRANTED;
}
复制代码
Android6.0系统其实支持两种动态管理,runtime-permission及被阉割的AppOpsService,当targetSdkVersion>23的时候,采用rumtime-permission,当 targetSdkVersion<23的时候,二者兼有,其实targetSdkVersion<23的时候,仍然能够动态申请6.0的权限,前提是你要采用23以后的compileSdkVersion,只有这样才能用相应的API,不过仍是推荐升级targetSdkVersion,这才是正道。对于Android6.0如下的手机,除了通知(可能还有悬浮窗),其余权限基本都没有系统的检测手段,不管Context的checkPermission仍是AppopsManager的checkOp,基本都是对Android6.0以后才有效。
做者:看书的小蜗牛 原文连接:Android权限检查API checkSelfPermission问题 仅供参考,欢迎指正