Android 6.0中完善对 api <= 22 的应用的权限控制
2017-03-17 17:06
531 查看
Android 6.0中用了新的运行时权限,运行在6.0以上的设备,需要动态的申请权限,当然这只针对 targetSdk > 22的应用;targetSdk <= 22 的应用扔沿用旧版本的AppOps的权限管理机制,也就是安装时权限。
需要特别指出的是在 Android6.0 中,安装时权限必须都是默认允许的。因为在 Android 6.0 中移除了AppOps中通过弹窗获取权限的机制,如果我们将targetSdk <= 22的应用默认关闭安装权限,会导致这类应用因为权限问题无法正常运行,而且毫无提示。
为了添加对这些低版本应用的控制,我们有2项工作:
将三方应用的默认安装时权限设为拒绝;
恢复AppOps中的弹窗获取权限机制。
第一步比较简单,我们注意到AppOpsManager中有一个数组sOpDefaultMode,从名字上我们就猜到它是控制默认安装权限的,但如果你直接将其值全部改为拒绝,我相信你的手机肯定无法开机了。因为所有的系统应用也是依赖于这个数组来设置权限的。我的做法是复制了一个一样的数组sThirdOpDefaultMode,将其值改为拒绝,然后在调用的时候判断为三方应用则应用sThirdOpDefaultMode。
简单说来只要有AppOpsManager.opToDefaultMode,就会调用isSystemApp来区分系统应用与三方应用。
在众多的调用isSystemApp的地方,有一处我直接写的true,在AppOpsService中的readUid函数。这个函数是AppOpsService启动时必走的流程,因此这里和“应用安装”这个词扯不上关系,而且这里也仅是一个默认值,会被该函数中后续流程读取到的值覆盖(如果有的话)。这里只贴上改动部分:
改完这些,三方应用的默认安装权限应该都是默认拒绝了。
下面我们进行第二步,这里才是大坑。本以为将Android 4.4 中相关的类和方法粘贴过来稍加改动就OK,没想到遇到不少阻碍。
首先二话不说把4.4中相关的类粘过来吧:
BasePermissionDialog.java
PermissionDialog.java
PermissionDialogResult.java
若不对弹窗样式定制的话,这三个类是都不需要改动的。主要需要改动的是AppOpsService.java这个类。我相信能找到这里,你肯定对AppOps的机制已经很熟悉了,在AppOps中赋予权限最核心的就是2个方法:noteOperation和startOperation。这两个方法分别用于“非持续功能权限”和“持续功能权限”,startOperation会和finishOperation配合使用。以startOperation为例:
通过isSystemApp来区分系统应用与三方应用,这么做是为了确保系统流程不受影响。(通过targetSdk来做区分,只新增 api <= 22 的应用逻辑貌似也是不错的选择)
在三方应用的逻辑中,只要权限不是允许,就需要弹窗提醒,返回结果当然必须是要根据用户对弹窗的操作来决定。
最后的return userDialogResult.get();一定要写在同步代码块的外侧。避免死锁。调用get后,通过wait()方法,等待用户响应。注意做好超时处理,避免ANR。
PermissionDialog中处理点击事件交由一个Handler来完成:
到这里,用户操作已经有了结果,返回给AppOpsService来进行权限操作,然后通知应用:
这个方法比较长,但是细看发现其主要是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中的改动也不小,暂时先把这两项权限默认允许把。
需要特别指出的是在 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中的改动也不小,暂时先把这两项权限默认允许把。
相关文章推荐
- 控制Android应用权限的五种方法
- 保护你的隐私,五种控制Android应用的权限的方法
- Android 6.0应用权限请求
- 请教一个问题,Android应用运行时在调用某个API(比如相机)时,系统如何判断这个应用是否具备相应的权限?
- android 6.0运行时权限应用之图库图片选取
- 五种控制Android应用的权限的方法
- 五种控制Android应用的权限的方法
- Android 6.0权限控制
- Android 6.0 应用权限(二) -- 与系统权限一起工作(Working with System Permissions)
- 请教一个问题,Android应用运行时在调用某个API(比如相机)时,系统如何判断这个应用是否具备相应的权限?
- 五种控制Android应用的权限的方法
- 【android】五种控制Android应用的权限的方法
- 【Android 应用开发】Android 网络编程 API笔记 - java.net 包 权限 地址 套接字 相关类 简介
- android 6.0 权限时代的来临 摄像头的应用
- 保护你的隐私,五种控制Android应用的权限的方法
- 五种控制Android应用的权限的方法
- 五种控制Android应用的权限的方法
- Android4.3\4.4 应用权限控制隐藏功能AppOps
- 请教一个问题,Android应用运行时在调用某个API(比如相机)时,系统如何判断这个应用是否具备相应的权限?
- 【译】Android开发者应该知道的API6.0以上运行时权限