Android 6.0中完善對 api

Android 6.0中用了新的運行時權限,運行在6.0以上的設備,需要動態的申請權限,當然這隻針對 targetSdk > 22的應用;targetSdk <= 22 的應用扔沿用舊版本的AppOps的權限管理機制,也就是安裝時權限。

需要特別指出的是在 Android6.0 中,安裝時權限必須都是默認允許的。因爲在 Android 6.0 中移除了AppOps中通過彈窗獲取權限的機制,如果我們將targetSdk <= 22的應用默認關閉安裝權限,會導致這類應用因爲權限問題無法正常運行,而且毫無提示。

爲了添加對這些低版本應用的控制,我們有2項工作:

  1. 將三方應用的默認安裝時權限設爲拒絕;

  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點:

  1. op.dialogResult.notifyAll(mode),釋放Result中的鎖;
  2. setMode;
  3. setUidMode;
  4. 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中的改動也不小,暫時先把這兩項權限默認允許把。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章