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問題 僅供參考,歡迎指正