您的位置:首页 > 移动开发 > Android开发

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。

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 权限管理