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中的改动也不小,暂时先把这两项权限默认允许把。

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