Android 6.0中用了新的運行時權限,運行在6.0以上的設備,需要動態的申請權限,當然這隻針對 targetSdk > 22的應用;targetSdk <= 22 的應用扔沿用舊版本的AppOps的權限管理機制,也就是安裝時權限。
需要特別指出的是在 Android6.0 中,安裝時權限必須都是默認允許的。因爲在 Android 6.0 中移除了AppOps中通過彈窗獲取權限的機制,如果我們將targetSdk <= 22的應用默認關閉安裝權限,會導致這類應用因爲權限問題無法正常運行,而且毫無提示。
爲了添加對這些低版本應用的控制,我們有2項工作:
將三方應用的默認安裝時權限設爲拒絕;
恢復AppOps中的彈窗獲取權限機制。
第一步比較簡單,我們注意到AppOpsManager中有一個數組sOpDefaultMode,從名字上我們就猜到它是控制默認安裝權限的,但如果你直接將其值全部改爲拒絕,我相信你的手機肯定無法開機了。因爲所有的系統應用也是依賴於這個數組來設置權限的。我的做法是複製了一個一樣的數組sThirdOpDefaultMode,將其值改爲拒絕,然後在調用的時候判斷爲三方應用則應用sThirdOpDefaultMode。
private boolean isSystemApp(String name) {
if ("media".equals(name)) return true;
if ("root".equals(name)) return true;
try {
ApplicationInfo info = mContext.getPackageManager().getApplicationInfo(name,
PackageManager.GET_PERMISSIONS);
//api大於22的應用,必須要使用運行時權限,我們也就沒必要將其安裝權限關閉了。交給google管理就好了。
if(info.targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1)
return true;
return info.isSystemApp();
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG,"isSystemApp1:"+name);
return false;
} catch (NullPointerException e) {
Log.e(TAG,"isSystemApp2:"+name);
return false;
}
}
簡單說來只要有AppOpsManager.opToDefaultMode,就會調用isSystemApp來區分系統應用與三方應用。
在衆多的調用isSystemApp的地方,有一處我直接寫的true,在AppOpsService中的readUid函數。這個函數是AppOpsService啓動時必走的流程,因此這裏和“應用安裝”這個詞扯不上關係,而且這裏也僅是一個默認值,會被該函數中後續流程讀取到的值覆蓋(如果有的話)。這裏只貼上改動部分:
String tagName = parser.getName();
if (tagName.equals("op")) {
// 這裏只有AppOpsService初始化纔會走到,而且Op對象只是新建一個默認值,如果有歷史保存的op mode,會覆蓋這個默認值
//因此,直接按照系統應用來給他權限(三方應用在安裝時候會通過getOpLocked方法設置爲“拒絕”的權限)
Op op = new Op(uid, pkgName, true, Integer.parseInt(parser.getAttributeValue(null, "n")));
String mode = parser.getAttributeValue(null, "m");
if (mode != null) {
op.mode = Integer.parseInt(mode);
}
改完這些,三方應用的默認安裝權限應該都是默認拒絕了。
下面我們進行第二步,這裏纔是大坑。本以爲將Android 4.4 中相關的類和方法粘貼過來稍加改動就OK,沒想到遇到不少阻礙。
首先二話不說把4.4中相關的類粘過來吧:
BasePermissionDialog.java
PermissionDialog.java
PermissionDialogResult.java
若不對彈窗樣式定製的話,這三個類是都不需要改動的。主要需要改動的是AppOpsService.java這個類。我相信能找到這裏,你肯定對AppOps的機制已經很熟悉了,在AppOps中賦予權限最核心的就是2個方法:noteOperation和startOperation。這兩個方法分別用於“非持續功能權限”和“持續功能權限”,startOperation會和finishOperation配合使用。以startOperation爲例:
@Override
public int startOperation(IBinder token, int code, int uid, String packageName) {
verifyIncomingUid(uid);
verifyIncomingOp(code);
ClientState client = (ClientState)token;
final Result userDialogResult;
synchronized (this) {
Ops ops = getOpsLocked(uid, packageName, true);
if (ops == null) {
if (DEBUG) Log.d(TAG, "startOperation: no op for code " + code + " uid " + uid
+ " package " + packageName);
return AppOpsManager.MODE_ERRORED;
}
Op op = getOpLocked(ops, code, true);
if (isOpRestricted(uid, code, packageName)) {
return AppOpsManager.MODE_IGNORED;
}
final int switchCode = AppOpsManager.opToSwitch(code);
UidState uidState = ops.uidState;
if (uidState.opModes != null) {
final int uidMode = uidState.opModes.get(switchCode);
if (uidMode != AppOpsManager.MODE_ALLOWED) {
if (DEBUG) Log.d(TAG, "3 noteOperation: reject #" + op.mode + " for code "
+ switchCode + " (" + code + ") uid " + uid + " package "
+ packageName);
op.rejectTime = System.currentTimeMillis();
return uidMode;
}
}
final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op;
if (isSystemApp(packageName)) {
if (switchOp.mode != AppOpsManager.MODE_ALLOWED) {
if (DEBUG) Log.d(TAG, "4 startOperation: reject #" + op.mode + " for code "
+ switchCode + " (" + code + ") uid " + uid + " package " + packageName);
op.rejectTime = System.currentTimeMillis();
return switchOp.mode;
}
if (DEBUG) Log.d(TAG, "startOperation: allowing code " + code + " uid " + uid
+ " package " + packageName);
if (op.nesting == 0) {
op.time = System.currentTimeMillis();
op.rejectTime = 0;
op.duration = -1;
}
op.nesting++;
if (client.mStartedOps != null) {
client.mStartedOps.add(op);
}
return AppOpsManager.MODE_ALLOWED;
} else { //新增的三方應用的處理
if (switchOp.mode == AppOpsManager.MODE_ALLOWED) {
if (DEBUG) Log.d(TAG, "startOperation: allowing code " + code + " uid " + uid
+ " package " + packageName);
if (op.nesting == 0) {
op.time = System.currentTimeMillis();
op.rejectTime = 0;
op.duration = -1;
}
op.nesting++;
if (client.mStartedOps != null) {
client.mStartedOps.add(op);
}
return AppOpsManager.MODE_ALLOWED;
} else {
/**
* add by zjzhu 2017.3.9
* 對於api小於等於22的應用,我們要對其appOp進行控制。若權限是拒絕的,則詢問用戶。
*/
IBinder clientToken = client.mAppToken;
op.mClientTokens.add(clientToken);
op.startOpCount++;
userDialogResult = askOperationLocked(code, uid, packageName, switchOp);
}
}
}
return userDialogResult.get();
}
通過isSystemApp來區分系統應用與三方應用,這麼做是爲了確保系統流程不受影響。(通過targetSdk來做區分,只新增 api <= 22 的應用邏輯貌似也是不錯的選擇)
在三方應用的邏輯中,只要權限不是允許,就需要彈窗提醒,返回結果當然必須是要根據用戶對彈窗的操作來決定。
最後的return userDialogResult.get();一定要寫在同步代碼塊的外側。避免死鎖。調用get後,通過wait()方法,等待用戶響應。注意做好超時處理,避免ANR。
public int get() {
synchronized (this) {
while (!mHasResult) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mResult;
}
PermissionDialog中處理點擊事件交由一個Handler來完成:
private final Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
int mode;
boolean remember = mChoice.isChecked();
switch(msg.what) {
case ACTION_ALLOWED:
mode = AppOpsManager.MODE_ALLOWED;
break;
case ACTION_IGNORED:
mode = AppOpsManager.MODE_IGNORED;
break;
default:
mode = AppOpsManager.MODE_IGNORED;
remember = false;
}
mService.notifyOperation(mCode, mUid, mPackageName, mode,
remember);
dismiss();
}
};
到這裏,用戶操作已經有了結果,返回給AppOpsService來進行權限操作,然後通知應用:
public void notifyOperation(int code, int uid, String packageName, int mode,
boolean remember) {
verifyIncomingUid(uid);
verifyIncomingOp(code);
ArrayList<Callback> repCbs = null;
int switchCode = AppOpsManager.opToSwitch(code);
Log.d(TAG,"notify:"+code+","+switchCode);
synchronized (this) {
recordOperationLocked(code, uid, packageName, mode);
Op op = getOpLocked(switchCode, uid, packageName, true);
if (op != null) {
// Send result to all waiting client
if( op.dialogResult.mDialog != null) {
op.dialogResult.notifyAll(mode);
op.dialogResult.mDialog = null;
}
if (remember && op.mode != mode) {
/**
* 此部分爲setMode部分提取
*/
op.mode = mode;
ArrayList<Callback> cbs = mOpModeWatchers.get(switchCode);
if (cbs != null) {
if (repCbs == null) {
repCbs = new ArrayList<Callback>();
}
repCbs.addAll(cbs);
}
cbs = mPackageModeWatchers.get(packageName);
if (cbs != null) {
if (repCbs == null) {
repCbs = new ArrayList<Callback>();
}
repCbs.addAll(cbs);
}
if (mode == AppOpsManager.opToDefaultMode(op.op, isSystemApp(packageName))) {
// If going into the default mode, prune this op
// if there is nothing else interesting in it.
pruneOp(op, uid, packageName);
}
scheduleFastWriteLocked();
/**
* 此部分爲setUidMode部分提取
*/
if (Binder.getCallingPid() != Process.myPid()) {
mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
Binder.getCallingPid(), Binder.getCallingUid(), null);
}
code = AppOpsManager.opToSwitch(code);
final int defaultMode = AppOpsManager.opToDefaultMode(code, isSystemApp(getPackagesForUid(uid)[0]));
UidState uidState = getUidStateLocked(uid, false);
if (uidState == null) {
if (mode == defaultMode) {
//return; nothing to do
}
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);
}
scheduleWriteLocked();
}
}
}
}
/**
* 此處參考4.4的來通知各服務opChanged, Android6.0中的UidMode需要不同的通知方式,可參考setUidMode中的內容實現。
*/
if (repCbs != null) {
for (int i=0; i<repCbs.size(); i++) {
try {
repCbs.get(i).mCallback.opChanged(switchCode, packageName);
} catch (RemoteException e) {
}
}
}
}
這個方法比較長,但是細看發現其主要是4點:
- op.dialogResult.notifyAll(mode),釋放Result中的鎖;
- setMode;
- setUidMode;
- repCbs.get(i).mCallback.opChanged通知各服務狀態改變;
釋放Result鎖一定要在通知服務狀態之前,不然會死鎖:因爲這一系列的起點是服務中的requestXXX,而opChanged和requestXXX在服務中是公用一把相同的鎖。
這裏把setMode和setUidMode中的代碼“基本複製”過來,而不直接調用就是爲了保證在最後才調用 repCbs.get(i).mCallback.opChanged,這一部分在setMode和setUidMode中都存在,分別調用會通知兩次,而且結果還會異常。
OP_READ_EXTERNAL_STORAGE
OP_WRITE_EXTERNAL_STORAGE 這兩個權限是在6.0中新增的需要控制的權限,按照上面的改法,會死鎖於ActivityManagerService。暫時沒有想到解決辦法,MountService在6.0中的改動也不小,暫時先把這兩項權限默認允許把。