1.App-ops作用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
从AppOpsManager注解可以看出,App-Ops主要用于权限控制和追述一些重要事件。
App-Ops可以对每一个uid或者每一个应用程序进行操作权限控制,此接入控制可以被设置成一下四种模式:
MODE_DEFAULT:默认行为,用于区分不同app-op
MODE_ALLOWED: 运行操作
MODE_IGNORED: 不允许操作。不执行请求操作,或者返回空或者虚拟数据
MODE_ERRORED:此操作发生之后,抛SecurityException异常。
每个平台会给每一个运行时权限定义一个app-op,用于追述和运行异常操作。如果运行时权限被拒绝,系统将抛SecurityException异常,但是如果权限被授予并且app op是MODE_IGNORED,那么调用者将获得虚拟数据。
如果被平台定义的App-ops被覆盖,app-op权限将被设置成MODE_DEFAULT,如果希望将app-ops设置成MODE_ALLOWED或者MODE_IGNORED,都需要校验之后再赋值。
2.AppopsService分析 2.1 AppopsService启动和数据结构分析 AppOpsService的启动是再ActivityManagerService中启动的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 mAppOpsService = new AppOpsService(new File(systemDir, "appops.xml" ), mHandler); mAppOpsService.startWatchingMode(AppOpsManager.OP_RUN_IN_BACKGROUND, null , new IAppOpsCallback.Stub() { @Override public void opChanged (int op, int uid, String packageName) { if (op == AppOpsManager.OP_RUN_IN_BACKGROUND && packageName != null ) { if (mAppOpsService.checkOperation(op, uid, packageName) != AppOpsManager.MODE_ALLOWED) { runInBackgroundDisabled(uid); } } } }); public AppOpsService (File storagePath, Handler handler) { mFile = new AtomicFile(storagePath); mHandler = handler; readState(); }
在AppOpsService初始化的时候,通过readState()去读取data/system/appops.xml文件把他保存在内存中。
appops.xml文件大致如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <app-ops v ="1" > <pkg n ="com.ts.appservice.settings" > <uid n ="1000" p ="false" > <op n ="59" tp ="1622129003446" pu ="0" /> <op n ="60" tp ="1622129003446" pu ="0" /> </uid > </pkg > <pkg n ="com.ts.appservice.mediacenterservice" > <uid n ="1000" p ="false" > <op n ="59" tp ="1622128997408" pu ="0" /> <op n ="60" tp ="1622128997408" pu ="0" /> </uid > </pkg > <pkg n ="com.iflytek.autofly.systemserver" > <uid n ="1000" p ="false" > <op n ="0" /> <op n ="2" tp ="1577845829783" d ="182727" /> <op n ="41" tp ="1622129006431" d ="9182935" /> <op n ="42" tp ="1622129006431" d ="9182935" /> <op n ="59" tp ="1622129006246" pu ="0" /> <op n ="60" tp ="1622129006246" pu ="0" /> <op n ="76" tp ="1577837011354" d ="9001156" /> </uid > </pkg > ...... </app-ops >
最外层用app-ops标签,里面包裹了,再里面包裹了,最里面包裹各式各样不同的操作。每个uid可能存在于多个包中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 void readState () { synchronized (mFile) { synchronized (this ) { FileInputStream stream; try { stream = mFile.openRead(); } catch (FileNotFoundException e) { Slog.i(TAG, "No existing app ops " + mFile.getBaseFile() + "; starting empty" ); return ; } boolean success = false ; mUidStates.clear(); try { XmlPullParser parser = Xml.newPullParser(); parser.setInput(stream, StandardCharsets.UTF_8.name()); int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { ; } if (type != XmlPullParser.START_TAG) { throw new IllegalStateException("no start tag found" ); } int outerDepth = parser.getDepth(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue ; } String tagName = parser.getName(); if (tagName.equals("pkg" )) { readPackage(parser); } else if (tagName.equals("uid" )) { readUidOps(parser); } else { Slog.w(TAG, "Unknown element under <app-ops>: " + parser.getName()); XmlUtils.skipCurrentTag(parser); } } success = true ; } catch (IllegalStateException e) { Slog.w(TAG, "Failed parsing " + e); } catch (NullPointerException e) { Slog.w(TAG, "Failed parsing " + e); } catch (NumberFormatException e) { Slog.w(TAG, "Failed parsing " + e); } catch (XmlPullParserException e) { Slog.w(TAG, "Failed parsing " + e); } catch (IOException e) { Slog.w(TAG, "Failed parsing " + e); } catch (IndexOutOfBoundsException e) { Slog.w(TAG, "Failed parsing " + e); } finally { if (!success) { mUidStates.clear(); } try { stream.close(); } catch (IOException e) { } } } } }
该方法中主要关注readPackage(parser)
和readUidOps(parser)
函数,先看看readUidOps(parser)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 void readUidOps (XmlPullParser parser) throws NumberFormatException,XmlPullParserException, IOException { final int uid = Integer.parseInt(parser.getAttributeValue(null , "n" )); int outerDepth = parser.getDepth(); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue ; } String tagName = parser.getName(); if (tagName.equals("op" )) { final int code = Integer.parseInt(parser.getAttributeValue(null , "n" )); final int mode = Integer.parseInt(parser.getAttributeValue(null , "m" )); UidState uidState = getUidStateLocked(uid, true ); if (uidState.opModes == null ) { uidState.opModes = new SparseIntArray(); } uidState.opModes.put(code, mode); } else { Slog.w(TAG, "Unknown element under <uid-ops>: " + parser.getName()); XmlUtils.skipCurrentTag(parser); } } }
这里主要是创建一个UidState。然后创建opModes,之后将code和mode关系保存在opModes中。
然后再分析一下readPackage(parser)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void readPackage (XmlPullParser parser) throws NumberFormatException, XmlPullParserException, IOException { String pkgName = parser.getAttributeValue(null , "n" ); int outerDepth = parser.getDepth(); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue ; } String tagName = parser.getName(); if (tagName.equals("uid" )) { readUid(parser, pkgName); } else { Slog.w(TAG, "Unknown element under <pkg>: " + parser.getName()); XmlUtils.skipCurrentTag(parser); } } }
从上面看解释pkg只是解析package name,然后解析uid的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 void readUid (XmlPullParser parser, String pkgName) throws NumberFormatException,XmlPullParserException, IOException { int uid = Integer.parseInt(parser.getAttributeValue(null , "n" )); String isPrivilegedString = parser.getAttributeValue(null , "p" ); boolean isPrivileged = false ; if (isPrivilegedString == null ) { try { IPackageManager packageManager = ActivityThread.getPackageManager(); if (packageManager != null ) { ApplicationInfo appInfo = ActivityThread.getPackageManager() .getApplicationInfo(pkgName, 0 , UserHandle.getUserId(uid)); if (appInfo != null ) { isPrivileged = (appInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0 ; } } else { return ; } } catch (RemoteException e) { Slog.w(TAG, "Could not contact PackageManager" , e); } } else { isPrivileged = Boolean.parseBoolean(isPrivilegedString); } int outerDepth = parser.getDepth(); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue ; } String tagName = parser.getName(); if (tagName.equals("op" )) { Op op = new Op(uid, pkgName, Integer.parseInt(parser.getAttributeValue(null , "n" ))); String mode = parser.getAttributeValue(null , "m" ); if (mode != null ) { op.mode = Integer.parseInt(mode); } String time = parser.getAttributeValue(null , "t" ); if (time != null ) { op.time = Long.parseLong(time); } time = parser.getAttributeValue(null , "r" ); if (time != null ) { op.rejectTime = Long.parseLong(time); } String dur = parser.getAttributeValue(null , "d" ); if (dur != null ) { op.duration = Integer.parseInt(dur); } String proxyUid = parser.getAttributeValue(null , "pu" ); if (proxyUid != null ) { op.proxyUid = Integer.parseInt(proxyUid); } String proxyPackageName = parser.getAttributeValue(null , "pp" ); if (proxyPackageName != null ) { op.proxyPackageName = proxyPackageName; } UidState uidState = getUidStateLocked(uid, true ); if (uidState.pkgOps == null ) { uidState.pkgOps = new ArrayMap<>(); } Ops ops = uidState.pkgOps.get(pkgName); if (ops == null ) { ops = new Ops(pkgName, uidState, isPrivileged); uidState.pkgOps.put(pkgName, ops); } ops.put(op.op, op); } else { Slog.w(TAG, "Unknown element under <pkg>: " + parser.getName()); XmlUtils.skipCurrentTag(parser); } } }
这里我们可以看出Ops数据结构分为三层:
1 SparseArray mUidStates 用于存放uid和对应的uid下的状态,用UidState变量代表 2 UidState,一个uid对应一个UidState,然而一个uid可以对应多个package, 每个package下面都有一个Ops代表一组操作 3 每个Ops下又有多个op 另外每个UidState下还有一组opModes,分别保存code和mode的对应管理。
到这里就分析完成了解析,和数据结构的组织。 我们来线下AppOps如何检查权限。
2.2 Appops权限校验 我们以AppOpsManager类的checkOp和checkOpNoThrow为例子说明AppOps的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public int checkOp (String op, int uid, String packageName) { return checkOp(strOpToOp(op), uid, packageName); } public int checkOp (int op, int uid, String packageName) { try { int mode = mService.checkOperation(op, uid, packageName); if (mode == MODE_ERRORED) { throw new SecurityException(buildSecurityExceptionMsg(op, uid, packageName)); } return mode; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } public int checkOpNoThrow (String op, int uid, String packageName) { return checkOpNoThrow(strOpToOp(op), uid, packageName); } public int checkOpNoThrow (int op, int uid, String packageName) { try { return mService.checkOperation(op, uid, packageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
两个方法的区别在于,如果mode是MODE_ERRORED抛不抛异常。
当我们看到strOpToOp(op)似乎就可以猜到是把一个字符串转为从一个整形,就是转化成op code,似乎瞬间明白op标签的n是数字的原因了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static int strOpToOp (String op) { Integer val = sOpStrToOp.get(op); if (val == null ) { throw new IllegalArgumentException("Unknown operation string: " + op); } return val; } for (int i=0 ; i<_NUM_OP; i++) { if (sOpToString[i] != null ) { sOpStrToOp.put(sOpToString[i], i); } } private static String[] sOpToString = new String[] { OPSTR_COARSE_LOCATION, OPSTR_FINE_LOCATION, null , null , ...... }
在AppOpsManager中有op的名字对应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static final int OP_NONE = -1 ;public static final int OP_COARSE_LOCATION = 0 ;public static final int OP_FINE_LOCATION = 1 ;public static final int OP_GPS = 2 ;public static final int OP_VIBRATE = 3 ;public static final int OP_READ_CONTACTS = 4 ;public static final int OP_WRITE_CONTACTS = 5 ;public static final int OP_READ_CALL_LOG = 6 ;public static final int OP_WRITE_CALL_LOG = 7 ;public static final int OP_READ_CALENDAR = 8 ;public static final int OP_WRITE_CALENDAR = 9 ;public static final int OP_WIFI_SCAN = 10 ;public static final int OP_POST_NOTIFICATION = 11 ;public static final int OP_NEIGHBORING_CELLS = 12 ;public static final int OP_CALL_PHONE = 13 ;.......
sOpStrToOp是使用sOpToString所维护的字符串作为key,数组下标作为value去维护关系,所以这里的code就是数组下表,看来要加AppOps还要注意和google的兼容性。其实都是使用AppOpsService的checkOperation函数来检查的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Override public int checkOperation (int code, int uid, String packageName) { verifyIncomingUid(uid); verifyIncomingOp(code); String resolvedPackageName = resolvePackageName(uid, packageName); if (resolvedPackageName == null ) { return AppOpsManager.MODE_IGNORED; } synchronized (this ) { if (isOpRestrictedLocked(uid, code, resolvedPackageName)) { return AppOpsManager.MODE_IGNORED; } code = AppOpsManager.opToSwitch(code); UidState uidState = getUidStateLocked(uid, false ); if (uidState != null && uidState.opModes != null ) { final int uidMode = uidState.opModes.get(code); if (uidMode != AppOpsManager.MODE_ALLOWED) { return uidMode; } } Op op = getOpLocked(code, uid, resolvedPackageName, false ); if (op == null ) { return AppOpsManager.opToDefaultMode(code); } return op.mode; } } public static int opToSwitch (int op) { return sOpToSwitch[op]; } private static int [] sOpToSwitch = new int [] { OP_COARSE_LOCATION, OP_COARSE_LOCATION, OP_COARSE_LOCATION, OP_VIBRATE, OP_READ_CONTACTS, OP_WRITE_CONTACTS, OP_READ_CALL_LOG, OP_WRITE_CALL_LOG, OP_READ_CALENDAR, OP_WRITE_CALENDAR, .... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public static final int ROOT_UID = 0 ;public static final int SYSTEM_UID = 1000 ;public static final int PHONE_UID = 1001 ;public static final int SHELL_UID = 2000 ;public static final int LOG_UID = 1007 ;public static final int WIFI_UID = 1010 ;...... public static final int FIRST_APPLICATION_UID = 10000 ;public static final int LAST_APPLICATION_UID = 19999 ;public static final int FIRST_ISOLATED_UID = 99000 ;public static final int LAST_ISOLATED_UID = 99999 ;
函数很简单,使用注释中的九个步骤去判断op code对应的授权模式。 1,2,3 三个步骤都是检查参数和调用者的合理性,这里需要说明的是如果resolvedPackageName不存在的时候返回AppOpsManager.MODE_IGNORED,看来这就是一种op对应的mode,这里对mode做一下说明
//1 操作允许 public static final int MODE_ALLOWED = 0; //操作不允许,但是checkOp不会抛出安全异常,表示忽略 public static final int MODE_IGNORED = 1; //3 表示操作不允许,checkOp操作会抛出安全异常 public static final int MODE_ERRORED = 2; //4 默认行为,可能进一步检查权限 public static final int MODE_DEFAULT = 3;
步骤4检查多用户相关的授权,我们先放一下再来分析 步骤5根据code转换为op code,这里又进行了一个转换的操作,对于这里的转换,注释里面说的比较清楚,就是多个code可能对应同一个op code,所以这里要进行一次转换。大多数时候code都是和opcode相同的。 步骤7 首先检查uid下的opModes,我们在分析读取appops.xml文件的时候,有时候一个uid标签是不在pkg内部的,这种uid标签下申明的mode,优先级要高于pkg下的op,所以在这里先去判断uid的op code 对应的mode. 步骤8 则是当uid下的规则不存在,继而检查pkg下的op 步骤9是如果没有指定规则,则采用默认规则,opToDefaultMode函数也是使用查表的方式确定默认规则,表则AppOpsManager类的sOpDefaultMode变量中维护。
2.3 AppOpsManager下一个方法 1 public int checkPackage (int uid, String packageName)
这个方法会在uid下创建对应的uidState和一个Ops返回给用户,创建成功返回说明uid和packagename是对应的上的,返回AppOpsManager.MODE_ALLOWED,否则返回AppOpsManager.MODE_ERRORED
1 public int noteOp (int op, int uid, String packageName)
noteOp 函数比checkOp函数额外多出的功能就是会创建对应的op,另外会更新一些op操作的时间用于统计信息
1 public int startOp (int op, int uid, String packageName)
表示一个长时间运行的操作,比noteOp增加了一个统计信息,放在一个叫starting的集合里可以dump到,调用finishOp结束操作。
2.4 多用户检测 AppOps的另外一个功能是针对多用户的授权检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override public void setUserRestriction (int code, boolean restricted, IBinder token, int userHandle, String[] exceptionPackages) { if (Binder.getCallingPid() != Process.myPid()) { mContext.enforcePermission(Manifest.permission.MANAGE_APP_OPS_RESTRICTIONS, Binder.getCallingPid(), Binder.getCallingUid(), null ); } if (userHandle != UserHandle.getCallingUserId()) { if (mContext.checkCallingOrSelfPermission(Manifest.permission .INTERACT_ACROSS_USERS_FULL) != PackageManager.PERMISSION_GRANTED && mContext.checkCallingOrSelfPermission(Manifest.permission .INTERACT_ACROSS_USERS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Need INTERACT_ACROSS_USERS_FULL or" + " INTERACT_ACROSS_USERS to interact cross user " ); } } verifyIncomingOp(code); Preconditions.checkNotNull(token); setUserRestrictionNoCheck(code, restricted, token, userHandle, exceptionPackages); }
做了一系列检查后进入setUserRestrictionNoCheck真正的创先相应的受限规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 private void setUserRestrictionNoCheck (int code, boolean restricted, IBinder token, int userHandle, String[] exceptionPackages) { boolean notifyChange = false ; synchronized (AppOpsService.this ) { ClientRestrictionState restrictionState = mOpUserRestrictions.get(token); if (restrictionState == null ) { try { restrictionState = new ClientRestrictionState(token); } catch (RemoteException e) { return ; } mOpUserRestrictions.put(token, restrictionState); } if (restrictionState.setRestriction(code, restricted, exceptionPackages, userHandle)) { notifyChange = true ; } if (restrictionState.isDefault()) { mOpUserRestrictions.remove(token); restrictionState.destroy(); } } if (notifyChange) { notifyWatchersOfChange(code); } } public boolean setRestriction (int code, boolean restricted, String[] excludedPackages, int userId) { boolean changed = false ; if (perUserRestrictions == null && restricted) { perUserRestrictions = new SparseArray<>(); } if (perUserRestrictions != null ) { boolean [] userRestrictions = perUserRestrictions.get(userId); if (userRestrictions == null && restricted) { userRestrictions = new boolean [AppOpsManager._NUM_OP]; perUserRestrictions.put(userId, userRestrictions); } if (userRestrictions != null && userRestrictions[code] != restricted) { userRestrictions[code] = restricted; if (!restricted && isDefault(userRestrictions)) { perUserRestrictions.remove(userId); userRestrictions = null ; } changed = true ; } if (userRestrictions != null ) { final boolean noExcludedPackages = ArrayUtils.isEmpty(excludedPackages); if (perUserExcludedPackages == null && !noExcludedPackages) { perUserExcludedPackages = new SparseArray<>(); } if (perUserExcludedPackages != null && !Arrays.equals(excludedPackages, perUserExcludedPackages.get(userId))) { if (noExcludedPackages) { perUserExcludedPackages.remove(userId); if (perUserExcludedPackages.size() <= 0 ) { perUserExcludedPackages = null ; } } else { perUserExcludedPackages.put(userId, excludedPackages); } changed = true ; } } } return changed; }
这个api的作用就是限制一个user去做某件事情。
2.4 限制用户操作例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class OverlayTouchActivity extends Activity { private final IBinder mToken = new Binder(); @Override protected void onResume () { super .onResume(); setOverlayAllowed(false ); } @Override protected void onPause () { super .onPause(); setOverlayAllowed(true ); } private void setOverlayAllowed (boolean allowed) { AppOpsManager appOpsManager = getSystemService(AppOpsManager.class); if (appOpsManager != null ) { appOpsManager.setUserRestriction(AppOpsManager.OP_SYSTEM_ALERT_WINDOW, !allowed, mToken); appOpsManager.setUserRestriction(AppOpsManager.OP_TOAST_WINDOW, !allowed, mToken); } } }
在进入OverlayTouchActivity这个页面时候就不允许这个用户弹出window,除非弹框来自Privileged的包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private boolean isOpRestrictedLocked (int uid, int code, String packageName) { int userHandle = UserHandle.getUserId(uid); final int restrictionSetCount = mOpUserRestrictions.size(); for (int i = 0 ; i < restrictionSetCount; i++) { ClientRestrictionState restrictionState = mOpUserRestrictions.valueAt(i); if (restrictionState.hasRestriction(code, packageName, userHandle)) { if (AppOpsManager.opAllowSystemBypassRestriction(code)) { synchronized (this ) { Ops ops = getOpsRawLocked(uid, packageName, true ); if ((ops != null ) && ops.isPrivileged) { return false ; } } } return true ; } } return false ; }