1、概述:
appops是在現有權限機制上新增的一套權限管理機制,主要針對一些高危的非必須系統應用的權限,比如在其他應用上顯示懸浮窗。
</br>
2、服務啟動:
appops服務在ActivityManagerService服務啟動的時候一同啟動:
mAppOpsService = mInjector.getAppOpsService(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);
}
}
}
});
在AppOpsService構造函數中:傳入的檔案:/data/system/appops.xml,該檔案主要記錄各應用權限情況,如下:
<pkg n="com.android.server.telecom">
<uid n="1000" p="true">
<op n="6" t="1539203309925" pu="0" pp="com.android.providers.contacts" />
<op n="7" t="1538515682359" pu="0" pp="com.android.providers.contacts" />
<op n="23" r="1538042158836" />
<op n="27" t="1538042162413" pu="0" />
</uid>
</pkg>
public AppOpsService(File storagePath, Handler handler) {
LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
mFile = new AtomicFile(storagePath);
mHandler = handler;
mLooper = Looper.myLooper();
readState();
}
通過readState将appops檔案中記錄的内容讀取到本地的數組:mUidStates中
protected final SparseArray<UidState> mUidStates = new SparseArray<>();
void readState() {
...
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;
...
}
public void systemReady() {
synchronized (this) {
boolean changed = false;
for (int i = mUidStates.size() - 1; i >= 0; i--) {
UidState uidState = mUidStates.valueAt(i);
String[] packageNames = getPackagesForUid(uidState.uid);
if (ArrayUtils.isEmpty(packageNames)) {
uidState.clear();
mUidStates.removeAt(i);
changed = true;
continue;
}
ArrayMap<String, Ops> pkgs = uidState.pkgOps;
if (pkgs == null) {
continue;
}
移除mUidState數組中已經解除安裝的應用
Iterator<Ops> it = pkgs.values().iterator();
while (it.hasNext()) {
Ops ops = it.next();
int curUid = -1;
try {
curUid = AppGlobals.getPackageManager().getPackageUid(ops.packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES,
UserHandle.getUserId(ops.uidState.uid));
} catch (RemoteException ignored) {
}
if (curUid != ops.uidState.uid) {
Slog.i(TAG, "Pruning old package " + ops.packageName
+ "/" + ops.uidState + ": new uid=" + curUid);
it.remove();
changed = true;
}
}
if (uidState.isDefault()) {
mUidStates.removeAt(i);
}
}
if (changed) {
scheduleFastWriteLocked();
}
}
設定app安裝權限政策
PackageManagerInternal packageManagerInternal = LocalServices.getService(
PackageManagerInternal.class);
packageManagerInternal.setExternalSourcesPolicy(
new PackageManagerInternal.ExternalSourcesPolicy() {
@Override
public int getPackageTrustedToInstallApps(String packageName, int uid) {
int appOpMode = checkOperation(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
uid, packageName);
switch (appOpMode) {
case AppOpsManager.MODE_ALLOWED:
return PackageManagerInternal.ExternalSourcesPolicy.USER_TRUSTED;
case AppOpsManager.MODE_ERRORED:
return PackageManagerInternal.ExternalSourcesPolicy.USER_BLOCKED;
default:
return PackageManagerInternal.ExternalSourcesPolicy.USER_DEFAULT;
}
}
});
設定app挂載權限
StorageManagerInternal storageManagerInternal = LocalServices.getService(
StorageManagerInternal.class);
storageManagerInternal.addExternalStoragePolicy(
new StorageManagerInternal.ExternalStorageMountPolicy() {
@Override
public int getMountMode(int uid, String packageName) {
if (Process.isIsolated(uid)) {
return Zygote.MOUNT_EXTERNAL_NONE;
}
if (noteOperation(AppOpsManager.OP_READ_EXTERNAL_STORAGE, uid,
packageName) != AppOpsManager.MODE_ALLOWED) {
return Zygote.MOUNT_EXTERNAL_NONE;
}
if (noteOperation(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, uid,
packageName) != AppOpsManager.MODE_ALLOWED) {
return Zygote.MOUNT_EXTERNAL_READ;
}
return Zygote.MOUNT_EXTERNAL_WRITE;
}
@Override
public boolean hasExternalStorage(int uid, String packageName) {
final int mountMode = getMountMode(uid, packageName);
return mountMode == Zygote.MOUNT_EXTERNAL_READ
|| mountMode == Zygote.MOUNT_EXTERNAL_WRITE;
}
});
}
在AppopsService構造函數中完成了将appops.xml檔案中的内容讀取到mUidStates數組中的任務,在systemready中完成了将mUidStates數組中已經解除安裝的應用資料删除,設定app的安裝應用權限政策,app的挂在sd卡權限政策,至此,appops服務啟動完成。
3、權限檢測:
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();
}
}
@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
&& uidState.opModes.indexOfKey(code) >= 0) {
return uidState.opModes.get(code);
}
Op op = getOpLocked(code, uid, resolvedPackageName, false);
if (op == null) {
return AppOpsManager.opToDefaultMode(code);
}
return op.mode;
}
}
4、權限設定、權限修改動态監聽:
/** @hide */
public void setMode(int code, int uid, String packageName, int mode) {
try {
mService.setMode(code, uid, packageName, mode);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@Override
public void startWatchingMode(int op, String packageName, IAppOpsCallback callback) {
if (callback == null) {
return;
}
synchronized (this) {
op = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op;
Callback cb = mModeWatchers.get(callback.asBinder());
if (cb == null) {
cb = new Callback(callback);
mModeWatchers.put(callback.asBinder(), cb);
}
if (op != AppOpsManager.OP_NONE) {
ArraySet<Callback> cbs = mOpModeWatchers.get(op);
if (cbs == null) {
cbs = new ArraySet<>();
mOpModeWatchers.put(op, cbs);
}
cbs.add(cb);
}
if (packageName != null) {
ArraySet<Callback> cbs = mPackageModeWatchers.get(packageName);
if (cbs == null) {
cbs = new ArraySet<>();
mPackageModeWatchers.put(packageName, cbs);
}
cbs.add(cb);
}
}
}
比如在AMS中啟動appops服務後就啟動了對OP_RUN_IN_BACKGROUND 權限修改的監聽:
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);
}
}
}
});
5、appops檔案的更新:
在關機時候,appops權限有更新的時候都會更新appops.xml檔案:
public void shutdown() {
Slog.w(TAG, "Writing app ops before shutdown...");
boolean doWrite = false;
synchronized (this) {
if (mWriteScheduled) {
mWriteScheduled = false;
doWrite = true;
}
}
if (doWrite) {
writeState();
}
}
void writeState() {
synchronized (mFile) {
List<AppOpsManager.PackageOps> allOps = getPackagesForOps(null);
FileOutputStream stream;
try {
stream = mFile.startWrite();
} catch (IOException e) {
Slog.w(TAG, "Failed to write state: " + e);
return;
}
try {
XmlSerializer out = new FastXmlSerializer();
out.setOutput(stream, StandardCharsets.UTF_8.name());
out.startDocument(null, true);
out.startTag(null, "app-ops");
final int uidStateCount = mUidStates.size();
for (int i = 0; i < uidStateCount; i++) {
UidState uidState = mUidStates.valueAt(i);
if (uidState.opModes != null && uidState.opModes.size() > 0) {
out.startTag(null, "uid");
out.attribute(null, "n", Integer.toString(uidState.uid));
SparseIntArray uidOpModes = uidState.opModes;
final int opCount = uidOpModes.size();
for (int j = 0; j < opCount; j++) {
final int op = uidOpModes.keyAt(j);
final int mode = uidOpModes.valueAt(j);
out.startTag(null, "op");
out.attribute(null, "n", Integer.toString(op));
out.attribute(null, "m", Integer.toString(mode));
out.endTag(null, "op");
}
out.endTag(null, "uid");
}
}
if (allOps != null) {
String lastPkg = null;
for (int i=0; i<allOps.size(); i++) {
AppOpsManager.PackageOps pkg = allOps.get(i);
if (!pkg.getPackageName().equals(lastPkg)) {
if (lastPkg != null) {
out.endTag(null, "pkg");
}
lastPkg = pkg.getPackageName();
out.startTag(null, "pkg");
out.attribute(null, "n", lastPkg);
}
out.startTag(null, "uid");
out.attribute(null, "n", Integer.toString(pkg.getUid()));
synchronized (this) {
Ops ops = getOpsRawLocked(pkg.getUid(), pkg.getPackageName(), false);
// Should always be present as the list of PackageOps is generated
// from Ops.
if (ops != null) {
out.attribute(null, "p", Boolean.toString(ops.isPrivileged));
} else {
out.attribute(null, "p", Boolean.toString(false));
}
}
List<AppOpsManager.OpEntry> ops = pkg.getOps();
for (int j=0; j<ops.size(); j++) {
AppOpsManager.OpEntry op = ops.get(j);
out.startTag(null, "op");
out.attribute(null, "n", Integer.toString(op.getOp()));
if (op.getMode() != AppOpsManager.opToDefaultMode(op.getOp())) {
out.attribute(null, "m", Integer.toString(op.getMode()));
}
long time = op.getTime();
if (time != 0) {
out.attribute(null, "t", Long.toString(time));
}
time = op.getRejectTime();
if (time != 0) {
out.attribute(null, "r", Long.toString(time));
}
int dur = op.getDuration();
if (dur != 0) {
out.attribute(null, "d", Integer.toString(dur));
}
int proxyUid = op.getProxyUid();
if (proxyUid != -1) {
out.attribute(null, "pu", Integer.toString(proxyUid));
}
String proxyPackageName = op.getProxyPackageName();
if (proxyPackageName != null) {
out.attribute(null, "pp", proxyPackageName);
}
out.endTag(null, "op");
}
out.endTag(null, "uid");
}
if (lastPkg != null) {
out.endTag(null, "pkg");
}
}
out.endTag(null, "app-ops");
out.endDocument();
mFile.finishWrite(stream);
} catch (IOException e) {
Slog.w(TAG, "Failed to write state, restoring backup.", e);
mFile.failWrite(stream);
}
}
}
6、懸浮窗添加過程中的權限檢查應用
添加懸浮窗代碼如下:
final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
int flag=WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_FULLSCREEN|
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE|WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
params.format = PixelFormat.TRANSLUCENT;
params.flags = flag;
Display display = windowManager.getDefaultDisplay();
Point p = new Point();
display.getRealSize(p);
params.width = p.x+100;
params.height = p.y+100;
if(android.os.Build.VERSION.SDK_INT >= 26) {
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
params.type = WindowManager.LayoutParams.TYPE_PHONE;
}
eyeView = new View(getApplicationContext());
eyeView.setBackgroundColor(Util.getColor(10));
windowManager.addView(eyeView,params);
添加視窗流程如圖:添加視窗流程
權限檢查代碼如下:
最終還是通過appops的checkOpNoThrow方法檢測該應用是否被允許顯示懸浮窗。
@Override
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
int type = attrs.type;
outAppOp[0] = AppOpsManager.OP_NONE;
if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
|| (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
|| (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
return WindowManagerGlobal.ADD_INVALID_TYPE;
}
if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
// Window manager will make sure these are okay.
return ADD_OKAY;
}
if (!isSystemAlertWindowType(type)) {
switch (type) {
case TYPE_TOAST:
// Only apps that target older than O SDK can add window without a token, after
// that we require a token so apps cannot add toasts directly as the token is
// added by the notification system.
// Window manager does the checking for this.
outAppOp[0] = OP_TOAST_WINDOW;
return ADD_OKAY;
case TYPE_DREAM:
case TYPE_INPUT_METHOD:
case TYPE_WALLPAPER:
case TYPE_PRESENTATION:
case TYPE_PRIVATE_PRESENTATION:
case TYPE_VOICE_INTERACTION:
case TYPE_ACCESSIBILITY_OVERLAY:
case TYPE_QS_DIALOG:
// The window manager will check these.
return ADD_OKAY;
}
return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
== PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
// Things get a little more interesting for alert windows...
outAppOp[0] = OP_SYSTEM_ALERT_WINDOW;
final int callingUid = Binder.getCallingUid();
// system processes will be automatically granted privilege to draw
if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
return ADD_OKAY;
}
ApplicationInfo appInfo;
try {
appInfo = mContext.getPackageManager().getApplicationInfoAsUser(
attrs.packageName,
0 /* flags */,
UserHandle.getUserId(callingUid));
} catch (PackageManager.NameNotFoundException e) {
appInfo = null;
}
if (appInfo == null || (type != TYPE_APPLICATION_OVERLAY && appInfo.targetSdkVersion >= O)) {
/**
* Apps targeting >= {@link Build.VERSION_CODES#O} are required to hold
* {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW} (system signature apps)
* permission to add alert windows that aren't
* {@link android.view.WindowManager.LayoutParams#TYPE_APPLICATION_OVERLAY}.
*/
return (mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
// check if user has enabled this operation. SecurityException will be thrown if this app
// has not been allowed by the user
final int mode = mAppOpsManager.checkOpNoThrow(outAppOp[0], callingUid, attrs.packageName);
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
case AppOpsManager.MODE_IGNORED:
// although we return ADD_OKAY for MODE_IGNORED, the added window will
// actually be hidden in WindowManagerService
return ADD_OKAY;
case AppOpsManager.MODE_ERRORED:
// Don't crash legacy apps
if (appInfo.targetSdkVersion < M) {
return ADD_OKAY;
}
return ADD_PERMISSION_DENIED;
default:
// in the default mode, we will make a decision here based on
// checkCallingPermission()
return (mContext.checkCallingOrSelfPermission(SYSTEM_ALERT_WINDOW)
== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
}