blob: 4dfd9b076354706ceb0f96c775f22b618431fcec [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.appop;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.OP_NONE;
import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM;
import static android.app.AppOpsManager.OP_USE_FULL_SCREEN_INTENT;
import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES;
import static android.app.AppOpsManager.opRestrictsRead;
import static android.app.AppOpsManager.opToDefaultMode;
import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.AppOpsManager.Mode;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserPackage;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
import android.permission.PermissionManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.permission.PermissionManagerServiceInternal;
import libcore.util.EmptyArray;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Legacy implementation for App-ops service's app-op mode (uid and package) storage and access.
* In the future this class will also include mode callbacks and op restrictions.
*/
public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface {
static final String TAG = "LegacyAppOpsServiceInterfaceImpl";
private static final boolean DEBUG = false;
// Write at most every 30 minutes.
private static final long WRITE_DELAY = DEBUG ? 1000 : 30 * 60 * 1000;
/**
* Sentinel integer version to denote that there was no appops.xml found on boot.
* This will happen when a device boots with no existing userdata.
*/
private static final int NO_FILE_VERSION = -2;
/**
* Sentinel integer version to denote that there was no version in the appops.xml found on boot.
* This means the file is coming from a build before versioning was added.
*/
private static final int NO_VERSION = -1;
/**
* Increment by one every time and add the corresponding upgrade logic in
* {@link #upgradeLocked(int)} below. The first version was 1.
*/
@VisibleForTesting
static final int CURRENT_VERSION = 4;
/**
* This stores the version of appops.xml seen at boot. If this is smaller than
* {@link #CURRENT_VERSION}, then we will run {@link #upgradeLocked(int)} on startup.
*/
private int mVersionAtBoot = NO_FILE_VERSION;
// Must be the same object that the AppOpsService is using for locking.
final Object mLock;
final Handler mHandler;
final Context mContext;
final SparseArray<int[]> mSwitchedOps;
@GuardedBy("mLock")
@VisibleForTesting
final SparseArray<SparseIntArray> mUidModes = new SparseArray<>();
@GuardedBy("mLock")
final SparseArray<ArrayMap<String, SparseIntArray>> mUserPackageModes = new SparseArray<>();
final SparseArray<ArraySet<OnOpModeChangedListener>> mOpModeWatchers = new SparseArray<>();
final ArrayMap<String, ArraySet<OnOpModeChangedListener>> mPackageModeWatchers =
new ArrayMap<>();
final AtomicFile mFile;
final Runnable mWriteRunner = new Runnable() {
public void run() {
synchronized (mLock) {
mWriteScheduled = false;
mFastWriteScheduled = false;
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
writeState();
return null;
}
};
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
}
}
};
boolean mWriteScheduled;
boolean mFastWriteScheduled;
// Constant meaning that any UID should be matched when dispatching callbacks
private static final int UID_ANY = -2;
AppOpsCheckingServiceImpl(File storageFile,
@NonNull Object lock, Handler handler, Context context,
SparseArray<int[]> switchedOps) {
this.mFile = new AtomicFile(storageFile);
this.mLock = lock;
this.mHandler = handler;
this.mContext = context;
this.mSwitchedOps = switchedOps;
}
@Override
public void systemReady() {
synchronized (mLock) {
// TODO: This file version upgrade code may still need to happen after we switch to
// another implementation of AppOpsCheckingServiceInterface.
upgradeLocked(mVersionAtBoot);
}
}
@Override
public SparseIntArray getNonDefaultUidModes(int uid) {
synchronized (mLock) {
SparseIntArray opModes = mUidModes.get(uid, null);
if (opModes == null) {
return new SparseIntArray();
}
return opModes.clone();
}
}
@Override
public SparseIntArray getNonDefaultPackageModes(String packageName, int userId) {
synchronized (mLock) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId);
if (packageModes == null) {
return new SparseIntArray();
}
SparseIntArray opModes = packageModes.get(packageName);
if (opModes == null) {
return new SparseIntArray();
}
return opModes.clone();
}
}
@Override
public int getUidMode(int uid, int op) {
synchronized (mLock) {
SparseIntArray opModes = mUidModes.get(uid, null);
if (opModes == null) {
return AppOpsManager.opToDefaultMode(op);
}
return opModes.get(op, AppOpsManager.opToDefaultMode(op));
}
}
@Override
public boolean setUidMode(int uid, int op, int mode) {
final int defaultMode = AppOpsManager.opToDefaultMode(op);
synchronized (mLock) {
SparseIntArray opModes = mUidModes.get(uid, null);
if (opModes == null) {
if (mode != defaultMode) {
opModes = new SparseIntArray();
mUidModes.put(uid, opModes);
opModes.put(op, mode);
scheduleWriteLocked();
}
} else {
if (opModes.indexOfKey(op) >= 0 && opModes.get(op) == mode) {
return false;
}
if (mode == defaultMode) {
opModes.delete(op);
if (opModes.size() <= 0) {
opModes = null;
mUidModes.delete(uid);
}
} else {
opModes.put(op, mode);
}
scheduleWriteLocked();
}
}
return true;
}
@Override
public int getPackageMode(String packageName, int op, @UserIdInt int userId) {
synchronized (mLock) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
if (packageModes == null) {
return AppOpsManager.opToDefaultMode(op);
}
SparseIntArray opModes = packageModes.getOrDefault(packageName, null);
if (opModes == null) {
return AppOpsManager.opToDefaultMode(op);
}
return opModes.get(op, AppOpsManager.opToDefaultMode(op));
}
}
@Override
public void setPackageMode(String packageName, int op, @Mode int mode, @UserIdInt int userId) {
final int defaultMode = AppOpsManager.opToDefaultMode(op);
synchronized (mLock) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
if (packageModes == null) {
packageModes = new ArrayMap<>();
mUserPackageModes.put(userId, packageModes);
}
SparseIntArray opModes = packageModes.get(packageName);
if (opModes == null) {
if (mode != defaultMode) {
opModes = new SparseIntArray();
packageModes.put(packageName, opModes);
opModes.put(op, mode);
scheduleWriteLocked();
}
} else {
if (opModes.indexOfKey(op) >= 0 && opModes.get(op) == mode) {
return;
}
if (mode == defaultMode) {
opModes.delete(op);
if (opModes.size() <= 0) {
opModes = null;
packageModes.remove(packageName);
}
} else {
opModes.put(op, mode);
}
scheduleWriteLocked();
}
}
}
@Override
public void removeUid(int uid) {
synchronized (mLock) {
SparseIntArray opModes = mUidModes.get(uid);
if (opModes == null) {
return;
}
mUidModes.remove(uid);
scheduleFastWriteLocked();
}
}
@Override
public boolean areUidModesDefault(int uid) {
synchronized (mLock) {
SparseIntArray opModes = mUidModes.get(uid);
return (opModes == null || opModes.size() <= 0);
}
}
@Override
public boolean arePackageModesDefault(@NonNull String packageName, @UserIdInt int userId) {
synchronized (mLock) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
if (packageModes == null) {
return true;
}
SparseIntArray opModes = packageModes.get(packageName);
return (opModes == null || opModes.size() <= 0);
}
}
@Override
public boolean removePackage(String packageName, @UserIdInt int userId) {
synchronized (mLock) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
if (packageModes == null) {
return false;
}
SparseIntArray ops = packageModes.remove(packageName);
if (ops != null) {
scheduleFastWriteLocked();
return true;
}
return false;
}
}
@Override
public void clearAllModes() {
synchronized (mLock) {
mUidModes.clear();
mUserPackageModes.clear();
}
}
@Override
public void startWatchingOpModeChanged(@NonNull OnOpModeChangedListener changedListener,
int op) {
Objects.requireNonNull(changedListener);
synchronized (mLock) {
ArraySet<OnOpModeChangedListener> modeWatcherSet = mOpModeWatchers.get(op);
if (modeWatcherSet == null) {
modeWatcherSet = new ArraySet<>();
mOpModeWatchers.put(op, modeWatcherSet);
}
modeWatcherSet.add(changedListener);
}
}
@Override
public void startWatchingPackageModeChanged(@NonNull OnOpModeChangedListener changedListener,
@NonNull String packageName) {
Objects.requireNonNull(changedListener);
Objects.requireNonNull(packageName);
synchronized (mLock) {
ArraySet<OnOpModeChangedListener> modeWatcherSet =
mPackageModeWatchers.get(packageName);
if (modeWatcherSet == null) {
modeWatcherSet = new ArraySet<>();
mPackageModeWatchers.put(packageName, modeWatcherSet);
}
modeWatcherSet.add(changedListener);
}
}
@Override
public void removeListener(@NonNull OnOpModeChangedListener changedListener) {
Objects.requireNonNull(changedListener);
synchronized (mLock) {
for (int i = mOpModeWatchers.size() - 1; i >= 0; i--) {
ArraySet<OnOpModeChangedListener> cbs = mOpModeWatchers.valueAt(i);
cbs.remove(changedListener);
if (cbs.size() <= 0) {
mOpModeWatchers.removeAt(i);
}
}
for (int i = mPackageModeWatchers.size() - 1; i >= 0; i--) {
ArraySet<OnOpModeChangedListener> cbs = mPackageModeWatchers.valueAt(i);
cbs.remove(changedListener);
if (cbs.size() <= 0) {
mPackageModeWatchers.removeAt(i);
}
}
}
}
@Override
public ArraySet<OnOpModeChangedListener> getOpModeChangedListeners(int op) {
synchronized (mLock) {
ArraySet<OnOpModeChangedListener> modeChangedListenersSet = mOpModeWatchers.get(op);
if (modeChangedListenersSet == null) {
return new ArraySet<>();
}
return new ArraySet<>(modeChangedListenersSet);
}
}
@Override
public ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners(
@NonNull String packageName) {
Objects.requireNonNull(packageName);
synchronized (mLock) {
ArraySet<OnOpModeChangedListener> modeChangedListenersSet =
mPackageModeWatchers.get(packageName);
if (modeChangedListenersSet == null) {
return new ArraySet<>();
}
return new ArraySet<>(modeChangedListenersSet);
}
}
@Override
public void notifyWatchersOfChange(int code, int uid) {
ArraySet<OnOpModeChangedListener> listenerSet = getOpModeChangedListeners(code);
if (listenerSet == null) {
return;
}
for (int i = 0; i < listenerSet.size(); i++) {
final OnOpModeChangedListener listener = listenerSet.valueAt(i);
notifyOpChanged(listener, code, uid, null);
}
}
@Override
public void notifyOpChanged(@NonNull OnOpModeChangedListener onModeChangedListener, int code,
int uid, @Nullable String packageName) {
Objects.requireNonNull(onModeChangedListener);
if (uid != UID_ANY && onModeChangedListener.getWatchingUid() >= 0
&& onModeChangedListener.getWatchingUid() != uid) {
return;
}
// See CALL_BACK_ON_CHANGED_LISTENER_WITH_SWITCHED_OP_CHANGE
int[] switchedCodes;
if (onModeChangedListener.getWatchedOpCode() == ALL_OPS) {
switchedCodes = mSwitchedOps.get(code);
} else if (onModeChangedListener.getWatchedOpCode() == OP_NONE) {
switchedCodes = new int[]{code};
} else {
switchedCodes = new int[]{onModeChangedListener.getWatchedOpCode()};
}
for (int switchedCode : switchedCodes) {
// There are features watching for mode changes such as window manager
// and location manager which are in our process. The callbacks in these
// features may require permissions our remote caller does not have.
final long identity = Binder.clearCallingIdentity();
try {
if (shouldIgnoreCallback(switchedCode, onModeChangedListener.getCallingPid(),
onModeChangedListener.getCallingUid())) {
continue;
}
onModeChangedListener.onOpModeChanged(switchedCode, uid, packageName);
} catch (RemoteException e) {
/* ignore */
} finally {
Binder.restoreCallingIdentity(identity);
}
}
}
private boolean shouldIgnoreCallback(int op, int watcherPid, int watcherUid) {
// If it's a restricted read op, ignore it if watcher doesn't have manage ops permission,
// as watcher should not use this to signal if the value is changed.
return opRestrictsRead(op) && mContext.checkPermission(Manifest.permission.MANAGE_APPOPS,
watcherPid, watcherUid) != PackageManager.PERMISSION_GRANTED;
}
@Override
public void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground,
@Nullable OnOpModeChangedListener callbackToIgnore) {
String[] uidPackageNames = getPackagesForUid(uid);
ArrayMap<OnOpModeChangedListener, ArraySet<String>> callbackSpecs = null;
synchronized (mLock) {
ArraySet<OnOpModeChangedListener> callbacks = mOpModeWatchers.get(code);
if (callbacks != null) {
final int callbackCount = callbacks.size();
for (int i = 0; i < callbackCount; i++) {
OnOpModeChangedListener callback = callbacks.valueAt(i);
if (onlyForeground && (callback.getFlags()
& WATCH_FOREGROUND_CHANGES) == 0) {
continue;
}
ArraySet<String> changedPackages = new ArraySet<>();
Collections.addAll(changedPackages, uidPackageNames);
if (callbackSpecs == null) {
callbackSpecs = new ArrayMap<>();
}
callbackSpecs.put(callback, changedPackages);
}
}
for (String uidPackageName : uidPackageNames) {
callbacks = mPackageModeWatchers.get(uidPackageName);
if (callbacks != null) {
if (callbackSpecs == null) {
callbackSpecs = new ArrayMap<>();
}
final int callbackCount = callbacks.size();
for (int i = 0; i < callbackCount; i++) {
OnOpModeChangedListener callback = callbacks.valueAt(i);
if (onlyForeground && (callback.getFlags()
& WATCH_FOREGROUND_CHANGES) == 0) {
continue;
}
ArraySet<String> changedPackages = callbackSpecs.get(callback);
if (changedPackages == null) {
changedPackages = new ArraySet<>();
callbackSpecs.put(callback, changedPackages);
}
changedPackages.add(uidPackageName);
}
}
}
if (callbackSpecs != null && callbackToIgnore != null) {
callbackSpecs.remove(callbackToIgnore);
}
}
if (callbackSpecs == null) {
return;
}
for (int i = 0; i < callbackSpecs.size(); i++) {
final OnOpModeChangedListener callback = callbackSpecs.keyAt(i);
final ArraySet<String> reportedPackageNames = callbackSpecs.valueAt(i);
if (reportedPackageNames == null) {
mHandler.sendMessage(PooledLambda.obtainMessage(
AppOpsCheckingServiceImpl::notifyOpChanged,
this, callback, code, uid, (String) null));
} else {
final int reportedPackageCount = reportedPackageNames.size();
for (int j = 0; j < reportedPackageCount; j++) {
final String reportedPackageName = reportedPackageNames.valueAt(j);
mHandler.sendMessage(PooledLambda.obtainMessage(
AppOpsCheckingServiceImpl::notifyOpChanged,
this, callback, code, uid, reportedPackageName));
}
}
}
}
private static String[] getPackagesForUid(int uid) {
String[] packageNames = null;
// Very early during boot the package manager is not yet or not yet fully started. At this
// time there are no packages yet.
if (AppGlobals.getPackageManager() != null) {
try {
packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid);
} catch (RemoteException e) {
/* ignore - local call */
}
}
if (packageNames == null) {
return EmptyArray.STRING;
}
return packageNames;
}
@Override
public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) {
synchronized (mLock) {
return evalForegroundOps(mUidModes.get(uid), foregroundOps);
}
}
@Override
public SparseBooleanArray evalForegroundPackageOps(String packageName,
SparseBooleanArray foregroundOps, @UserIdInt int userId) {
synchronized (mLock) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null);
return evalForegroundOps(packageModes == null ? null : packageModes.get(packageName),
foregroundOps);
}
}
private SparseBooleanArray evalForegroundOps(SparseIntArray opModes,
SparseBooleanArray foregroundOps) {
SparseBooleanArray tempForegroundOps = foregroundOps;
if (opModes != null) {
for (int i = opModes.size() - 1; i >= 0; i--) {
if (opModes.valueAt(i) == AppOpsManager.MODE_FOREGROUND) {
if (tempForegroundOps == null) {
tempForegroundOps = new SparseBooleanArray();
}
evalForegroundWatchers(opModes.keyAt(i), tempForegroundOps);
}
}
}
return tempForegroundOps;
}
private void evalForegroundWatchers(int op, SparseBooleanArray foregroundOps) {
boolean curValue = foregroundOps.get(op, false);
ArraySet<OnOpModeChangedListener> listenerSet = mOpModeWatchers.get(op);
if (listenerSet != null) {
for (int cbi = listenerSet.size() - 1; !curValue && cbi >= 0; cbi--) {
if ((listenerSet.valueAt(cbi).getFlags()
& AppOpsManager.WATCH_FOREGROUND_CHANGES) != 0) {
curValue = true;
}
}
}
foregroundOps.put(op, curValue);
}
@Override
public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage,
PrintWriter printWriter) {
boolean needSep = false;
if (mOpModeWatchers.size() > 0) {
boolean printedHeader = false;
for (int i = 0; i < mOpModeWatchers.size(); i++) {
if (dumpOp >= 0 && dumpOp != mOpModeWatchers.keyAt(i)) {
continue;
}
boolean printedOpHeader = false;
ArraySet<OnOpModeChangedListener> modeChangedListenerSet =
mOpModeWatchers.valueAt(i);
for (int j = 0; j < modeChangedListenerSet.size(); j++) {
final OnOpModeChangedListener listener = modeChangedListenerSet.valueAt(j);
if (dumpPackage != null
&& dumpUid != UserHandle.getAppId(listener.getWatchingUid())) {
continue;
}
needSep = true;
if (!printedHeader) {
printWriter.println(" Op mode watchers:");
printedHeader = true;
}
if (!printedOpHeader) {
printWriter.print(" Op ");
printWriter.print(AppOpsManager.opToName(mOpModeWatchers.keyAt(i)));
printWriter.println(":");
printedOpHeader = true;
}
printWriter.print(" #"); printWriter.print(j); printWriter.print(": ");
printWriter.println(listener.toString());
}
}
}
if (mPackageModeWatchers.size() > 0 && dumpOp < 0) {
boolean printedHeader = false;
for (int i = 0; i < mPackageModeWatchers.size(); i++) {
if (dumpPackage != null
&& !dumpPackage.equals(mPackageModeWatchers.keyAt(i))) {
continue;
}
needSep = true;
if (!printedHeader) {
printWriter.println(" Package mode watchers:");
printedHeader = true;
}
printWriter.print(" Pkg "); printWriter.print(mPackageModeWatchers.keyAt(i));
printWriter.println(":");
ArraySet<OnOpModeChangedListener> modeChangedListenerSet =
mPackageModeWatchers.valueAt(i);
for (int j = 0; j < modeChangedListenerSet.size(); j++) {
printWriter.print(" #"); printWriter.print(j); printWriter.print(": ");
printWriter.println(modeChangedListenerSet.valueAt(j).toString());
}
}
}
return needSep;
}
private void scheduleWriteLocked() {
if (!mWriteScheduled) {
mWriteScheduled = true;
mHandler.postDelayed(mWriteRunner, WRITE_DELAY);
}
}
private void scheduleFastWriteLocked() {
if (!mFastWriteScheduled) {
mWriteScheduled = true;
mFastWriteScheduled = true;
mHandler.removeCallbacks(mWriteRunner);
mHandler.postDelayed(mWriteRunner, 10 * 1000);
}
}
@Override
public void writeState() {
synchronized (mFile) {
FileOutputStream stream;
try {
stream = mFile.startWrite();
} catch (IOException e) {
Slog.w(TAG, "Failed to write state: " + e);
return;
}
try {
TypedXmlSerializer out = Xml.resolveSerializer(stream);
out.startDocument(null, true);
out.startTag(null, "app-ops");
out.attributeInt(null, "v", CURRENT_VERSION);
SparseArray<SparseIntArray> uidModesCopy = new SparseArray<>();
SparseArray<ArrayMap<String, SparseIntArray>> userPackageModesCopy =
new SparseArray<>();
int uidModesSize;
int usersSize;
synchronized (mLock) {
uidModesSize = mUidModes.size();
for (int uidIdx = 0; uidIdx < uidModesSize; uidIdx++) {
int uid = mUidModes.keyAt(uidIdx);
SparseIntArray modes = mUidModes.valueAt(uidIdx);
uidModesCopy.put(uid, modes.clone());
}
usersSize = mUserPackageModes.size();
for (int userIdx = 0; userIdx < usersSize; userIdx++) {
int user = mUserPackageModes.keyAt(userIdx);
ArrayMap<String, SparseIntArray> packageModes =
mUserPackageModes.valueAt(userIdx);
ArrayMap<String, SparseIntArray> packageModesCopy = new ArrayMap<>();
userPackageModesCopy.put(user, packageModesCopy);
for (int pkgIdx = 0, packageModesSize = packageModes.size();
pkgIdx < packageModesSize; pkgIdx++) {
String pkg = packageModes.keyAt(pkgIdx);
SparseIntArray modes = packageModes.valueAt(pkgIdx);
packageModesCopy.put(pkg, modes.clone());
}
}
}
for (int uidStateNum = 0; uidStateNum < uidModesSize; uidStateNum++) {
int uid = uidModesCopy.keyAt(uidStateNum);
SparseIntArray modes = uidModesCopy.valueAt(uidStateNum);
out.startTag(null, "uid");
out.attributeInt(null, "n", uid);
final int modesSize = modes.size();
for (int modeIdx = 0; modeIdx < modesSize; modeIdx++) {
final int op = modes.keyAt(modeIdx);
final int mode = modes.valueAt(modeIdx);
out.startTag(null, "op");
out.attributeInt(null, "n", op);
out.attributeInt(null, "m", mode);
out.endTag(null, "op");
}
out.endTag(null, "uid");
}
for (int userIdx = 0; userIdx < usersSize; userIdx++) {
int userId = userPackageModesCopy.keyAt(userIdx);
ArrayMap<String, SparseIntArray> packageModes =
userPackageModesCopy.valueAt(userIdx);
out.startTag(null, "user");
out.attributeInt(null, "n", userId);
int packageModesSize = packageModes.size();
for (int pkgIdx = 0; pkgIdx < packageModesSize; pkgIdx++) {
String pkg = packageModes.keyAt(pkgIdx);
SparseIntArray modes = packageModes.valueAt(pkgIdx);
out.startTag(null, "pkg");
out.attribute(null, "n", pkg);
final int modesSize = modes.size();
for (int modeIdx = 0; modeIdx < modesSize; modeIdx++) {
final int op = modes.keyAt(modeIdx);
final int mode = modes.valueAt(modeIdx);
out.startTag(null, "op");
out.attributeInt(null, "n", op);
out.attributeInt(null, "m", mode);
out.endTag(null, "op");
}
out.endTag(null, "pkg");
}
out.endTag(null, "user");
}
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);
}
}
}
/* Current format
<uid>
<op>
</uid>
<user>
<pkg>
<op>
</pkg>
</user>
*/
@Override
public void readState() {
synchronized (mFile) {
synchronized (mLock) {
FileInputStream stream;
try {
stream = mFile.openRead();
} catch (FileNotFoundException e) {
Slog.i(TAG, "No existing app ops " + mFile.getBaseFile() + "; starting empty");
mVersionAtBoot = NO_FILE_VERSION;
return;
}
try {
TypedXmlPullParser parser = Xml.resolvePullParser(stream);
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// Parse next until we reach the start or end
}
if (type != XmlPullParser.START_TAG) {
throw new IllegalStateException("no start tag found");
}
mVersionAtBoot = parser.getAttributeInt(null, "v", NO_VERSION);
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")) {
// version 2 has the structure pkg -> uid -> op ->
// in version 3, since pkg and uid states are kept completely
// independent we switch to user -> pkg -> op
readPackage(parser);
} else if (tagName.equals("uid")) {
readUidOps(parser);
} else if (tagName.equals("user")) {
readUser(parser);
} else {
Slog.w(TAG, "Unknown element under <app-ops>: "
+ parser.getName());
XmlUtils.skipCurrentTag(parser);
}
}
return;
} catch (XmlPullParserException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
@Override
public void shutdown() {
boolean doWrite = false;
synchronized (this) {
if (mWriteScheduled) {
mWriteScheduled = false;
mFastWriteScheduled = false;
mHandler.removeCallbacks(mWriteRunner);
doWrite = true;
}
}
if (doWrite) {
writeState();
}
}
@GuardedBy("mLock")
private void readUidOps(TypedXmlPullParser parser) throws NumberFormatException,
XmlPullParserException, IOException {
final int uid = parser.getAttributeInt(null, "n");
SparseIntArray modes = mUidModes.get(uid);
if (modes == null) {
modes = new SparseIntArray();
mUidModes.put(uid, modes);
}
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 = parser.getAttributeInt(null, "n");
final int mode = parser.getAttributeInt(null, "m");
if (mode != opToDefaultMode(code)) {
modes.put(code, mode);
}
} else {
Slog.w(TAG, "Unknown element under <uid>: "
+ parser.getName());
XmlUtils.skipCurrentTag(parser);
}
}
}
/*
* Used for migration when pkg is the depth=1 tag
*/
@GuardedBy("mLock")
private void readPackage(TypedXmlPullParser 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);
}
}
}
/*
* Used for migration when uid is the depth=2 tag
*/
@GuardedBy("mLock")
private void readUid(TypedXmlPullParser parser, String pkgName)
throws NumberFormatException, XmlPullParserException, IOException {
int userId = UserHandle.getUserId(parser.getAttributeInt(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")) {
readOp(parser, userId, pkgName);
} else {
Slog.w(TAG, "Unknown element under <pkg>: "
+ parser.getName());
XmlUtils.skipCurrentTag(parser);
}
}
}
@GuardedBy("mLock")
private void readUser(TypedXmlPullParser parser)
throws NumberFormatException, XmlPullParserException, IOException {
int userId = parser.getAttributeInt(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("pkg")) {
readPackage(parser, userId);
} else {
Slog.w(TAG, "Unknown element under <user>: "
+ parser.getName());
XmlUtils.skipCurrentTag(parser);
}
}
}
@GuardedBy("mLock")
private void readPackage(TypedXmlPullParser parser, int userId)
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("op")) {
readOp(parser, userId, pkgName);
} else {
Slog.w(TAG, "Unknown element under <pkg>: "
+ parser.getName());
XmlUtils.skipCurrentTag(parser);
}
}
}
@GuardedBy("mLock")
private void readOp(TypedXmlPullParser parser, int userId, @NonNull String pkgName)
throws NumberFormatException, XmlPullParserException {
final int opCode = parser.getAttributeInt(null, "n");
final int defaultMode = AppOpsManager.opToDefaultMode(opCode);
final int mode = parser.getAttributeInt(null, "m", defaultMode);
if (mode != defaultMode) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId);
if (packageModes == null) {
packageModes = new ArrayMap<>();
mUserPackageModes.put(userId, packageModes);
}
SparseIntArray modes = packageModes.get(pkgName);
if (modes == null) {
modes = new SparseIntArray();
packageModes.put(pkgName, modes);
}
modes.put(opCode, mode);
}
}
@GuardedBy("mLock")
private void upgradeLocked(int oldVersion) {
if (oldVersion == NO_FILE_VERSION || oldVersion >= CURRENT_VERSION) {
return;
}
Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION);
switch (oldVersion) {
case NO_VERSION:
upgradeRunAnyInBackgroundLocked();
// fall through
case 1:
upgradeScheduleExactAlarmLocked();
// fall through
case 2:
// split the appops.xml into appops.xml to store appop state and apppops_access.xml
// to store app-op access.
// fall through
case 3:
resetUseFullScreenIntentLocked();
// fall through
}
scheduleFastWriteLocked();
}
/**
* For all installed apps at time of upgrade, OP_RUN_ANY_IN_BACKGROUND will inherit the mode
* from RUN_IN_BACKGROUND.
*/
@VisibleForTesting
@GuardedBy("mLock")
void upgradeRunAnyInBackgroundLocked() {
final int uidModesSize = mUidModes.size();
for (int uidIdx = 0; uidIdx < uidModesSize; uidIdx++) {
SparseIntArray modesForUid = mUidModes.valueAt(uidIdx);
final int idx = modesForUid.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND);
if (idx >= 0) {
// Only non-default should exist in the map
modesForUid.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, modesForUid.valueAt(idx));
}
}
final int usersSize = mUserPackageModes.size();
for (int userIdx = 0; userIdx < usersSize; userIdx++) {
ArrayMap<String, SparseIntArray> packageModes =
mUserPackageModes.valueAt(userIdx);
for (int pkgIdx = 0, packageModesSize = packageModes.size();
pkgIdx < packageModesSize; pkgIdx++) {
SparseIntArray modes = packageModes.valueAt(pkgIdx);
final int idx = modes.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND);
if (idx >= 0) {
// Only non-default should exist in the map
modes.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, modes.valueAt(idx));
}
}
}
}
/**
* The interpretation of the default mode - MODE_DEFAULT - for OP_SCHEDULE_EXACT_ALARM is
* changing. Simultaneously, we want to change this op's mode from MODE_DEFAULT to MODE_ALLOWED
* for already installed apps. For newer apps, it will stay as MODE_DEFAULT.
*/
@VisibleForTesting
@GuardedBy("mLock")
void upgradeScheduleExactAlarmLocked() {
final PermissionManagerServiceInternal pmsi = LocalServices.getService(
PermissionManagerServiceInternal.class);
final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
final String[] packagesDeclaringPermission = pmsi.getAppOpPermissionPackages(
AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM));
final int[] userIds = umi.getUserIds();
for (final String pkg : packagesDeclaringPermission) {
for (int userId : userIds) {
final int uid = pmi.getPackageUid(pkg, 0, userId);
final int oldMode = getUidMode(uid, OP_SCHEDULE_EXACT_ALARM);
if (oldMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) {
setUidMode(uid, OP_SCHEDULE_EXACT_ALARM, MODE_ALLOWED);
}
}
// This appop is meant to be controlled at a uid level. So we leave package modes as
// they are.
}
}
/**
* A cleanup step for U Beta 2 that reverts the OP_USE_FULL_SCREEN_INTENT's mode to MODE_DEFAULT
* if the permission flags for the USE_FULL_SCREEN_INTENT permission does not have USER_SET.
*/
@VisibleForTesting
@GuardedBy("mLock")
void resetUseFullScreenIntentLocked() {
final PermissionManagerServiceInternal pmsi = LocalServices.getService(
PermissionManagerServiceInternal.class);
final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
final PermissionManager permissionManager =
mContext.getSystemService(PermissionManager.class);
final String permissionName = AppOpsManager.opToPermission(OP_USE_FULL_SCREEN_INTENT);
final String[] packagesDeclaringPermission =
pmsi.getAppOpPermissionPackages(permissionName);
final int[] userIds = umi.getUserIds();
for (final String pkg : packagesDeclaringPermission) {
for (int userId : userIds) {
final int uid = pmi.getPackageUid(pkg, 0, userId);
final int flags = permissionManager.getPermissionFlags(pkg, permissionName,
UserHandle.of(userId));
if ((flags & PackageManager.FLAG_PERMISSION_USER_SET) == 0) {
setUidMode(uid, OP_USE_FULL_SCREEN_INTENT,
AppOpsManager.opToDefaultMode(OP_USE_FULL_SCREEN_INTENT));
}
}
}
}
@VisibleForTesting
List<Integer> getUidsWithNonDefaultModes() {
List<Integer> result = new ArrayList<>();
synchronized (mLock) {
for (int i = 0; i < mUidModes.size(); i++) {
SparseIntArray modes = mUidModes.valueAt(i);
if (modes.size() > 0) {
result.add(mUidModes.keyAt(i));
}
}
}
return result;
}
@VisibleForTesting
List<UserPackage> getPackagesWithNonDefaultModes() {
List<UserPackage> result = new ArrayList<>();
synchronized (mLock) {
for (int i = 0; i < mUserPackageModes.size(); i++) {
ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.valueAt(i);
for (int j = 0; j < packageModes.size(); j++) {
SparseIntArray modes = packageModes.valueAt(j);
if (modes.size() > 0) {
result.add(
UserPackage.of(mUserPackageModes.keyAt(i), packageModes.keyAt(j)));
}
}
}
}
return result;
}
}