Extract appop restrictions

Extract all (or most) app-op restrictions behavior out of AppOpsService
and into LegacyAppOpsServiceInterfaceImpl.

There are two types of app-op restrictions: global restrictions and user
restrictions. Currently, the code/variables for both of these features
resides in AppOpsService. As part of ongoing refactoring work, we want
to move this functionality into another class and provide an interface
which AppOpsService can use to access it.

Test: atest AppOpsLegacyRestrictionsTest
Bug: 240200048
Change-Id: I18121136c0855d72b3b530b68dce2e2de4d40d16
This commit is contained in:
Jay Thomas Sullivan
2022-09-20 12:18:02 -07:00
parent 7144d64e3a
commit 7699ecdf36
6 changed files with 916 additions and 257 deletions

View File

@@ -0,0 +1,147 @@
/*
* 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 android.os.PackageTagsList;
import java.io.PrintWriter;
/**
* Legacy implementation for AppOpsService's app-op restrictions (global and user)
* storage and access.
*/
public interface AppOpsRestrictions {
/**
* Set or clear a global app-op restriction for the given {@code clientToken}.
*
* @param clientToken A token identifying the client this restriction applies to.
* @param code The app-op opCode to set (or clear) a restriction for.
* @param restricted {@code true} to restrict this app-op code, or {@code false} to clear an
* existing restriction.
* @return {@code true} if any restriction state was modified as a result of this operation
*/
boolean setGlobalRestriction(Object clientToken, int code, boolean restricted);
/**
* Get the state of a global app-op restriction for the given {@code clientToken}.
*
* @param clientToken A token identifying the client to get the restriction state of.
* @param code The app-op code to get the restriction state of.
* @return the restriction state
*/
boolean getGlobalRestriction(Object clientToken, int code);
/**
* Returns {@code true} if *any* global app-op restrictions are currently set for the given
* {@code clientToken}.
*
* @param clientToken A token identifying the client to check restrictions for.
* @return {@code true} if any restrictions are set
*/
boolean hasGlobalRestrictions(Object clientToken);
/**
* Clear *all* global app-op restrictions for the given {@code clientToken}.
*
* @param clientToken A token identifying the client to clear restrictions from.
* @return {@code true} if any restriction state was modified as a result of this operation
*/
boolean clearGlobalRestrictions(Object clientToken);
/**
* Set or clear a user app-op restriction for the given {@code clientToken} and {@code userId}.
*
* @param clientToken A token identifying the client this restriction applies to.
* @param code The app-op code to set (or clear) a restriction for.
* @param restricted {@code true} to restrict this app-op code, or {@code false} to
* remove any existing restriction.
* @param excludedPackageTags A list of packages and associated attribution tags to exclude
* from this restriction. Or, if {@code null}, removes any
* exclusions from this restriction.
* @return {@code true} if any restriction state was modified as a result of this operation
*/
boolean setUserRestriction(Object clientToken, int userId, int code, boolean restricted,
PackageTagsList excludedPackageTags);
/**
* Get the state of a user app-op restriction for the given {@code clientToken} and {@code
* userId}. Or, if the combination of ({{@code clientToken}, {@code userId}, @code
* packageName}, {@code attributionTag}) has been excluded via
* {@link AppOpsRestrictions#setUserRestriction}, always returns {@code false}.
*
* @param clientToken A token identifying the client this restriction applies to.
* @param userId Which userId this restriction applies to.
* @param code The app-op code to get the restriction state of.
* @param packageName A package name used to check for exclusions.
* @param attributionTag An attribution tag used to check for exclusions.
* @param isCheckOp a flag that, when {@code true}, denotes that exclusions should be
* checked by (packageName) rather than (packageName, attributionTag)
* @return the restriction state
*/
boolean getUserRestriction(Object clientToken, int userId, int code, String packageName,
String attributionTag, boolean isCheckOp);
/**
* Returns {@code true} if *any* user app-op restrictions are currently set for the given
* {@code clientToken}.
*
* @param clientToken A token identifying the client to check restrictions for.
* @return {@code true} if any restrictions are set
*/
boolean hasUserRestrictions(Object clientToken);
/**
* Clear *all* user app-op restrictions for the given {@code clientToken}.
*
* @param clientToken A token identifying the client to clear restrictions for.
* @return {@code true} if any restriction state was modified as a result of this operation
*/
boolean clearUserRestrictions(Object clientToken);
/**
* Clear *all* user app-op restrictions for the given {@code clientToken} and {@code userId}.
*
* @param clientToken A token identifying the client to clear restrictions for.
* @param userId Which userId to clear restrictions for.
* @return {@code true} if any restriction state was modified as a result of this operation
*/
boolean clearUserRestrictions(Object clientToken, Integer userId);
/**
* Returns the set of exclusions previously set by
* {@link AppOpsRestrictions#setUserRestriction} for the given {@code clientToken}
* and {@code userId}.
*
* @param clientToken A token identifying the client to get restriction exclusions for.
* @param userId Which userId to get restriction exclusions for
* @return a set of user restriction exclusions
*/
PackageTagsList getUserRestrictionExclusions(Object clientToken, int userId);
/**
* Dump the state of appop restrictions.
*
* @param printWriter writer to dump to.
* @param dumpOp if -1 then op mode listeners for all app-ops are dumped. If it's
* set to an app-op, only the watchers for that app-op are dumped.
* @param dumpPackage if not null and if dumpOp is -1, dumps watchers for the package
* name.
* @param showUserRestrictions include user restriction state in the output
*/
void dumpRestrictions(PrintWriter printWriter, int dumpOp, String dumpPackage,
boolean showUserRestrictions);
}

View File

@@ -0,0 +1,452 @@
/*
* 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 android.annotation.RequiresPermission;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.UserInfo;
import android.os.Handler;
import android.os.PackageTagsList;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import java.io.PrintWriter;
import java.util.List;
import java.util.Objects;
/**
* Implementation for AppOpsService's app-op restrictions (global and user) storage and retrieval.
*/
public class AppOpsRestrictionsImpl implements AppOpsRestrictions {
private static final int UID_ANY = -2;
private Context mContext;
private Handler mHandler;
private AppOpsServiceInterface mAppOpsServiceInterface;
// Map from (Object token) to (int code) to (boolean restricted)
private final ArrayMap<Object, SparseBooleanArray> mGlobalRestrictions = new ArrayMap<>();
// Map from (Object token) to (int userId) to (int code) to (boolean restricted)
private final ArrayMap<Object, SparseArray<SparseBooleanArray>> mUserRestrictions =
new ArrayMap<>();
// Map from (Object token) to (int userId) to (PackageTagsList packageTagsList)
private final ArrayMap<Object, SparseArray<PackageTagsList>>
mUserRestrictionExcludedPackageTags = new ArrayMap<>();
public AppOpsRestrictionsImpl(Context context, Handler handler,
AppOpsServiceInterface appOpsServiceInterface) {
mContext = context;
mHandler = handler;
mAppOpsServiceInterface = appOpsServiceInterface;
}
@Override
public boolean setGlobalRestriction(Object clientToken, int code, boolean restricted) {
if (restricted) {
if (!mGlobalRestrictions.containsKey(clientToken)) {
mGlobalRestrictions.put(clientToken, new SparseBooleanArray());
}
SparseBooleanArray restrictedCodes = mGlobalRestrictions.get(clientToken);
Objects.requireNonNull(restrictedCodes);
boolean changed = !restrictedCodes.get(code);
restrictedCodes.put(code, true);
return changed;
} else {
SparseBooleanArray restrictedCodes = mGlobalRestrictions.get(clientToken);
if (restrictedCodes == null) {
return false;
}
boolean changed = restrictedCodes.get(code);
restrictedCodes.delete(code);
if (restrictedCodes.size() == 0) {
mGlobalRestrictions.remove(clientToken);
}
return changed;
}
}
@Override
public boolean getGlobalRestriction(Object clientToken, int code) {
SparseBooleanArray restrictedCodes = mGlobalRestrictions.get(clientToken);
if (restrictedCodes == null) {
return false;
}
return restrictedCodes.get(code);
}
@Override
public boolean hasGlobalRestrictions(Object clientToken) {
return mGlobalRestrictions.containsKey(clientToken);
}
@Override
public boolean clearGlobalRestrictions(Object clientToken) {
return mGlobalRestrictions.remove(clientToken) != null;
}
@RequiresPermission(anyOf = {
android.Manifest.permission.MANAGE_USERS,
android.Manifest.permission.CREATE_USERS
})
@Override
public boolean setUserRestriction(Object clientToken, int userId, int code,
boolean restricted,
PackageTagsList excludedPackageTags) {
int[] userIds = resolveUserId(userId);
boolean changed = false;
for (int i = 0; i < userIds.length; i++) {
changed |= putUserRestriction(clientToken, userIds[i], code, restricted);
changed |= putUserRestrictionExclusions(clientToken, userIds[i],
excludedPackageTags);
}
return changed;
}
@RequiresPermission(anyOf = {
android.Manifest.permission.MANAGE_USERS,
android.Manifest.permission.CREATE_USERS
})
private int[] resolveUserId(int userId) {
int[] userIds;
if (userId == UserHandle.USER_ALL) {
// TODO(b/162888972): this call is returning all users, not just live ones - we
// need to either fix the method called, or rename the variable
List<UserInfo> liveUsers = UserManager.get(mContext).getUsers();
userIds = new int[liveUsers.size()];
for (int i = 0; i < liveUsers.size(); i++) {
userIds[i] = liveUsers.get(i).id;
}
} else {
userIds = new int[]{userId};
}
return userIds;
}
@Override
public boolean hasUserRestrictions(Object clientToken) {
return mUserRestrictions.containsKey(clientToken);
}
private boolean getUserRestriction(Object clientToken, int userId, int code) {
SparseArray<SparseBooleanArray> userIdRestrictedCodes =
mUserRestrictions.get(clientToken);
if (userIdRestrictedCodes == null) {
return false;
}
SparseBooleanArray restrictedCodes = userIdRestrictedCodes.get(userId);
if (restrictedCodes == null) {
return false;
}
return restrictedCodes.get(code);
}
@Override
public boolean getUserRestriction(Object clientToken, int userId, int code, String packageName,
String attributionTag, boolean isCheckOp) {
boolean restricted = getUserRestriction(clientToken, userId, code);
if (!restricted) {
return false;
}
PackageTagsList perUserExclusions = getUserRestrictionExclusions(clientToken, userId);
if (perUserExclusions == null) {
return true;
}
// TODO (b/240617242) add overload for checkOp to support attribution tags
if (isCheckOp) {
return !perUserExclusions.includes(packageName);
}
return !perUserExclusions.contains(packageName, attributionTag);
}
@Override
public boolean clearUserRestrictions(Object clientToken) {
boolean changed = false;
SparseBooleanArray allUserRestrictedCodes = collectAllUserRestrictedCodes(clientToken);
changed |= mUserRestrictions.remove(clientToken) != null;
changed |= mUserRestrictionExcludedPackageTags.remove(clientToken) != null;
notifyAllUserRestrictions(allUserRestrictedCodes);
return changed;
}
private SparseBooleanArray collectAllUserRestrictedCodes(Object clientToken) {
SparseBooleanArray allRestrictedCodes = new SparseBooleanArray();
SparseArray<SparseBooleanArray> userIdRestrictedCodes = mUserRestrictions.get(clientToken);
if (userIdRestrictedCodes == null) {
return allRestrictedCodes;
}
int userIdRestrictedCodesSize = userIdRestrictedCodes.size();
for (int i = 0; i < userIdRestrictedCodesSize; i++) {
SparseBooleanArray restrictedCodes = userIdRestrictedCodes.valueAt(i);
int restrictedCodesSize = restrictedCodes.size();
for (int j = 0; j < restrictedCodesSize; j++) {
int code = restrictedCodes.keyAt(j);
allRestrictedCodes.put(code, true);
}
}
return allRestrictedCodes;
}
// TODO: For clearUserRestrictions, we are calling notifyOpChanged from within the
// LegacyAppOpsServiceInterfaceImpl class. But, for all other changes to restrictions, we're
// calling it from within AppOpsService. This is awkward, and we should probably do it one
// way or the other.
private void notifyAllUserRestrictions(SparseBooleanArray allUserRestrictedCodes) {
int restrictedCodesSize = allUserRestrictedCodes.size();
for (int j = 0; j < restrictedCodesSize; j++) {
int code = allUserRestrictedCodes.keyAt(j);
mHandler.post(() -> mAppOpsServiceInterface.notifyWatchersOfChange(code, UID_ANY));
}
}
@Override
public boolean clearUserRestrictions(Object clientToken, Integer userId) {
boolean changed = false;
SparseArray<SparseBooleanArray> userIdRestrictedCodes =
mUserRestrictions.get(clientToken);
if (userIdRestrictedCodes != null) {
changed |= userIdRestrictedCodes.contains(userId);
userIdRestrictedCodes.remove(userId);
if (userIdRestrictedCodes.size() == 0) {
mUserRestrictions.remove(clientToken);
}
}
SparseArray<PackageTagsList> userIdPackageTags =
mUserRestrictionExcludedPackageTags.get(clientToken);
if (userIdPackageTags != null) {
changed |= userIdPackageTags.contains(userId);
userIdPackageTags.remove(userId);
if (userIdPackageTags.size() == 0) {
mUserRestrictionExcludedPackageTags.remove(clientToken);
}
}
return changed;
}
private boolean putUserRestriction(Object token, int userId, int code, boolean restricted) {
boolean changed = false;
if (restricted) {
if (!mUserRestrictions.containsKey(token)) {
mUserRestrictions.put(token, new SparseArray<>());
}
SparseArray<SparseBooleanArray> userIdRestrictedCodes = mUserRestrictions.get(token);
Objects.requireNonNull(userIdRestrictedCodes);
if (!userIdRestrictedCodes.contains(userId)) {
userIdRestrictedCodes.put(userId, new SparseBooleanArray());
}
SparseBooleanArray restrictedCodes = userIdRestrictedCodes.get(userId);
changed = !restrictedCodes.get(code);
restrictedCodes.put(code, restricted);
} else {
SparseArray<SparseBooleanArray> userIdRestrictedCodes = mUserRestrictions.get(token);
if (userIdRestrictedCodes == null) {
return false;
}
SparseBooleanArray restrictedCodes = userIdRestrictedCodes.get(userId);
if (restrictedCodes == null) {
return false;
}
changed = restrictedCodes.get(code);
restrictedCodes.delete(code);
if (restrictedCodes.size() == 0) {
userIdRestrictedCodes.remove(userId);
}
if (userIdRestrictedCodes.size() == 0) {
mUserRestrictions.remove(token);
}
}
return changed;
}
@Override
public PackageTagsList getUserRestrictionExclusions(Object clientToken, int userId) {
SparseArray<PackageTagsList> userIdPackageTags =
mUserRestrictionExcludedPackageTags.get(clientToken);
if (userIdPackageTags == null) {
return null;
}
return userIdPackageTags.get(userId);
}
private boolean putUserRestrictionExclusions(Object token, int userId,
PackageTagsList excludedPackageTags) {
boolean addingExclusions = excludedPackageTags != null && !excludedPackageTags.isEmpty();
if (addingExclusions) {
if (!mUserRestrictionExcludedPackageTags.containsKey(token)) {
mUserRestrictionExcludedPackageTags.put(token, new SparseArray<>());
}
SparseArray<PackageTagsList> userIdExcludedPackageTags =
mUserRestrictionExcludedPackageTags.get(token);
Objects.requireNonNull(userIdExcludedPackageTags);
userIdExcludedPackageTags.put(userId, excludedPackageTags);
return true;
} else {
SparseArray<PackageTagsList> userIdExclusions =
mUserRestrictionExcludedPackageTags.get(token);
if (userIdExclusions == null) {
return false;
}
boolean changed = userIdExclusions.get(userId) != null;
userIdExclusions.remove(userId);
if (userIdExclusions.size() == 0) {
mUserRestrictionExcludedPackageTags.remove(token);
}
return changed;
}
}
@Override
public void dumpRestrictions(PrintWriter pw, int code, String dumpPackage,
boolean showUserRestrictions) {
final int globalRestrictionCount = mGlobalRestrictions.size();
for (int i = 0; i < globalRestrictionCount; i++) {
Object token = mGlobalRestrictions.keyAt(i);
SparseBooleanArray restrictedOps = mGlobalRestrictions.valueAt(i);
pw.println(" Global restrictions for token " + token + ":");
StringBuilder restrictedOpsValue = new StringBuilder();
restrictedOpsValue.append("[");
final int restrictedOpCount = restrictedOps.size();
for (int j = 0; j < restrictedOpCount; j++) {
if (restrictedOpsValue.length() > 1) {
restrictedOpsValue.append(", ");
}
restrictedOpsValue.append(AppOpsManager.opToName(restrictedOps.keyAt(j)));
}
restrictedOpsValue.append("]");
pw.println(" Restricted ops: " + restrictedOpsValue);
}
if (!showUserRestrictions) {
return;
}
final int userRestrictionCount = mUserRestrictions.size();
for (int i = 0; i < userRestrictionCount; i++) {
Object token = mUserRestrictions.keyAt(i);
SparseArray<SparseBooleanArray> perUserRestrictions = mUserRestrictions.get(token);
SparseArray<PackageTagsList> perUserExcludedPackageTags =
mUserRestrictionExcludedPackageTags.get(token);
boolean printedTokenHeader = false;
final int restrictionCount = perUserRestrictions != null
? perUserRestrictions.size() : 0;
if (restrictionCount > 0 && dumpPackage == null) {
boolean printedOpsHeader = false;
for (int j = 0; j < restrictionCount; j++) {
int userId = perUserRestrictions.keyAt(j);
SparseBooleanArray restrictedOps = perUserRestrictions.valueAt(j);
if (restrictedOps == null) {
continue;
}
if (code >= 0 && !restrictedOps.get(code)) {
continue;
}
if (!printedTokenHeader) {
pw.println(" User restrictions for token " + token + ":");
printedTokenHeader = true;
}
if (!printedOpsHeader) {
pw.println(" Restricted ops:");
printedOpsHeader = true;
}
StringBuilder restrictedOpsValue = new StringBuilder();
restrictedOpsValue.append("[");
final int restrictedOpCount = restrictedOps.size();
for (int k = 0; k < restrictedOpCount; k++) {
int restrictedOp = restrictedOps.keyAt(k);
if (restrictedOpsValue.length() > 1) {
restrictedOpsValue.append(", ");
}
restrictedOpsValue.append(AppOpsManager.opToName(restrictedOp));
}
restrictedOpsValue.append("]");
pw.print(" ");
pw.print("user: ");
pw.print(userId);
pw.print(" restricted ops: ");
pw.println(restrictedOpsValue);
}
}
final int excludedPackageCount = perUserExcludedPackageTags != null
? perUserExcludedPackageTags.size() : 0;
if (excludedPackageCount > 0 && code < 0) {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
ipw.increaseIndent();
boolean printedPackagesHeader = false;
for (int j = 0; j < excludedPackageCount; j++) {
int userId = perUserExcludedPackageTags.keyAt(j);
PackageTagsList packageNames =
perUserExcludedPackageTags.valueAt(j);
if (packageNames == null) {
continue;
}
boolean hasPackage;
if (dumpPackage != null) {
hasPackage = packageNames.includes(dumpPackage);
} else {
hasPackage = true;
}
if (!hasPackage) {
continue;
}
if (!printedTokenHeader) {
ipw.println("User restrictions for token " + token + ":");
printedTokenHeader = true;
}
ipw.increaseIndent();
if (!printedPackagesHeader) {
ipw.println("Excluded packages:");
printedPackagesHeader = true;
}
ipw.increaseIndent();
ipw.print("user: ");
ipw.print(userId);
ipw.println(" packages: ");
ipw.increaseIndent();
packageNames.dump(ipw);
ipw.decreaseIndent();
ipw.decreaseIndent();
ipw.decreaseIndent();
}
ipw.decreaseIndent();
}
}
}
}

View File

@@ -101,7 +101,6 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.PermissionInfo;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.hardware.camera2.CameraDevice.CAMERA_AUDIO_RESTRICTION;
import android.net.Uri;
@@ -122,14 +121,12 @@ import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.StorageManagerInternal;
import android.permission.PermissionManager;
import android.provider.Settings;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.IndentingPrintWriter;
import android.util.KeyValueListParser;
import android.util.LongSparseArray;
import android.util.Pair;
@@ -367,6 +364,9 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
/** Interface for app-op modes.*/
@VisibleForTesting AppOpsServiceInterface mAppOpsServiceInterface;
/** Interface for app-op restrictions.*/
@VisibleForTesting AppOpsRestrictions mAppOpsRestrictions;
private AppOpsUidStateTracker mUidStateTracker;
/** Hands the definition of foreground and uid states */
@@ -1714,6 +1714,8 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
}
mAppOpsServiceInterface =
new LegacyAppOpsServiceInterfaceImpl(this, this, handler, context, mSwitchedOps);
mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler,
mAppOpsServiceInterface);
LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
mFile = new AtomicFile(storagePath, "appops");
@@ -6135,124 +6137,8 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
pw.println();
}
final int globalRestrictionCount = mOpGlobalRestrictions.size();
for (int i = 0; i < globalRestrictionCount; i++) {
IBinder token = mOpGlobalRestrictions.keyAt(i);
ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.valueAt(i);
ArraySet<Integer> restrictedOps = restrictionState.mRestrictedOps;
pw.println(" Global restrictions for token " + token + ":");
StringBuilder restrictedOpsValue = new StringBuilder();
restrictedOpsValue.append("[");
final int restrictedOpCount = restrictedOps.size();
for (int j = 0; j < restrictedOpCount; j++) {
if (restrictedOpsValue.length() > 1) {
restrictedOpsValue.append(", ");
}
restrictedOpsValue.append(AppOpsManager.opToName(restrictedOps.valueAt(j)));
}
restrictedOpsValue.append("]");
pw.println(" Restricted ops: " + restrictedOpsValue);
}
final int userRestrictionCount = mOpUserRestrictions.size();
for (int i = 0; i < userRestrictionCount; i++) {
IBinder token = mOpUserRestrictions.keyAt(i);
ClientUserRestrictionState restrictionState = mOpUserRestrictions.valueAt(i);
boolean printedTokenHeader = false;
if (dumpMode >= 0 || dumpWatchers || dumpHistory) {
continue;
}
final int restrictionCount = restrictionState.perUserRestrictions != null
? restrictionState.perUserRestrictions.size() : 0;
if (restrictionCount > 0 && dumpPackage == null) {
boolean printedOpsHeader = false;
for (int j = 0; j < restrictionCount; j++) {
int userId = restrictionState.perUserRestrictions.keyAt(j);
boolean[] restrictedOps = restrictionState.perUserRestrictions.valueAt(j);
if (restrictedOps == null) {
continue;
}
if (dumpOp >= 0 && (dumpOp >= restrictedOps.length
|| !restrictedOps[dumpOp])) {
continue;
}
if (!printedTokenHeader) {
pw.println(" User restrictions for token " + token + ":");
printedTokenHeader = true;
}
if (!printedOpsHeader) {
pw.println(" Restricted ops:");
printedOpsHeader = true;
}
StringBuilder restrictedOpsValue = new StringBuilder();
restrictedOpsValue.append("[");
final int restrictedOpCount = restrictedOps.length;
for (int k = 0; k < restrictedOpCount; k++) {
if (restrictedOps[k]) {
if (restrictedOpsValue.length() > 1) {
restrictedOpsValue.append(", ");
}
restrictedOpsValue.append(AppOpsManager.opToName(k));
}
}
restrictedOpsValue.append("]");
pw.print(" "); pw.print("user: "); pw.print(userId);
pw.print(" restricted ops: "); pw.println(restrictedOpsValue);
}
}
final int excludedPackageCount = restrictionState.perUserExcludedPackageTags != null
? restrictionState.perUserExcludedPackageTags.size() : 0;
if (excludedPackageCount > 0 && dumpOp < 0) {
IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
ipw.increaseIndent();
boolean printedPackagesHeader = false;
for (int j = 0; j < excludedPackageCount; j++) {
int userId = restrictionState.perUserExcludedPackageTags.keyAt(j);
PackageTagsList packageNames =
restrictionState.perUserExcludedPackageTags.valueAt(j);
if (packageNames == null) {
continue;
}
boolean hasPackage;
if (dumpPackage != null) {
hasPackage = packageNames.includes(dumpPackage);
} else {
hasPackage = true;
}
if (!hasPackage) {
continue;
}
if (!printedTokenHeader) {
ipw.println("User restrictions for token " + token + ":");
printedTokenHeader = true;
}
ipw.increaseIndent();
if (!printedPackagesHeader) {
ipw.println("Excluded packages:");
printedPackagesHeader = true;
}
ipw.increaseIndent();
ipw.print("user: ");
ipw.print(userId);
ipw.println(" packages: ");
ipw.increaseIndent();
packageNames.dump(ipw);
ipw.decreaseIndent();
ipw.decreaseIndent();
ipw.decreaseIndent();
}
ipw.decreaseIndent();
}
}
boolean showUserRestrictions = !(dumpMode < 0 && !dumpWatchers && !dumpHistory);
mAppOpsRestrictions.dumpRestrictions(pw, dumpOp, dumpPackage, showUserRestrictions);
if (!dumpHistory && !dumpWatchers) {
pw.println();
@@ -6872,8 +6758,6 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
private final class ClientUserRestrictionState implements DeathRecipient {
private final IBinder token;
SparseArray<boolean[]> perUserRestrictions;
SparseArray<PackageTagsList> perUserExcludedPackageTags;
ClientUserRestrictionState(IBinder token)
throws RemoteException {
@@ -6883,134 +6767,29 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
public boolean setRestriction(int code, boolean restricted,
PackageTagsList excludedPackageTags, int userId) {
boolean changed = false;
if (perUserRestrictions == null && restricted) {
perUserRestrictions = new SparseArray<>();
}
int[] users;
if (userId == UserHandle.USER_ALL) {
// TODO(b/162888972): this call is returning all users, not just live ones - we
// need to either fix the method called, or rename the variable
List<UserInfo> liveUsers = UserManager.get(mContext).getUsers();
users = new int[liveUsers.size()];
for (int i = 0; i < liveUsers.size(); i++) {
users[i] = liveUsers.get(i).id;
}
} else {
users = new int[]{userId};
}
if (perUserRestrictions != null) {
int numUsers = users.length;
for (int i = 0; i < numUsers; i++) {
int thisUserId = users[i];
boolean[] userRestrictions = perUserRestrictions.get(thisUserId);
if (userRestrictions == null && restricted) {
userRestrictions = new boolean[AppOpsManager._NUM_OP];
perUserRestrictions.put(thisUserId, userRestrictions);
}
if (userRestrictions != null && userRestrictions[code] != restricted) {
userRestrictions[code] = restricted;
if (!restricted && isDefault(userRestrictions)) {
perUserRestrictions.remove(thisUserId);
userRestrictions = null;
}
changed = true;
}
if (userRestrictions != null) {
final boolean noExcludedPackages =
excludedPackageTags == null || excludedPackageTags.isEmpty();
if (perUserExcludedPackageTags == null && !noExcludedPackages) {
perUserExcludedPackageTags = new SparseArray<>();
}
if (perUserExcludedPackageTags != null) {
if (noExcludedPackages) {
perUserExcludedPackageTags.remove(thisUserId);
if (perUserExcludedPackageTags.size() <= 0) {
perUserExcludedPackageTags = null;
}
} else {
perUserExcludedPackageTags.put(thisUserId, excludedPackageTags);
}
changed = true;
}
}
}
}
return changed;
return mAppOpsRestrictions.setUserRestriction(token, userId, code,
restricted, excludedPackageTags);
}
public boolean hasRestriction(int restriction, String packageName, String attributionTag,
public boolean hasRestriction(int code, String packageName, String attributionTag,
int userId, boolean isCheckOp) {
if (perUserRestrictions == null) {
return false;
}
boolean[] restrictions = perUserRestrictions.get(userId);
if (restrictions == null) {
return false;
}
if (!restrictions[restriction]) {
return false;
}
if (perUserExcludedPackageTags == null) {
return true;
}
PackageTagsList perUserExclusions = perUserExcludedPackageTags.get(userId);
if (perUserExclusions == null) {
return true;
}
// TODO (b/240617242) add overload for checkOp to support attribution tags
if (isCheckOp) {
return !perUserExclusions.includes(packageName);
}
return !perUserExclusions.contains(packageName, attributionTag);
return mAppOpsRestrictions.getUserRestriction(token, userId, code, packageName,
attributionTag, isCheckOp);
}
public void removeUser(int userId) {
if (perUserExcludedPackageTags != null) {
perUserExcludedPackageTags.remove(userId);
if (perUserExcludedPackageTags.size() <= 0) {
perUserExcludedPackageTags = null;
}
}
if (perUserRestrictions != null) {
perUserRestrictions.remove(userId);
if (perUserRestrictions.size() <= 0) {
perUserRestrictions = null;
}
}
mAppOpsRestrictions.clearUserRestrictions(token, userId);
}
public boolean isDefault() {
return perUserRestrictions == null || perUserRestrictions.size() <= 0;
return !mAppOpsRestrictions.hasUserRestrictions(token);
}
@Override
public void binderDied() {
synchronized (AppOpsService.this) {
mAppOpsRestrictions.clearUserRestrictions(token);
mOpUserRestrictions.remove(token);
if (perUserRestrictions == null) {
return;
}
final int userCount = perUserRestrictions.size();
for (int i = 0; i < userCount; i++) {
final boolean[] restrictions = perUserRestrictions.valueAt(i);
final int restrictionCount = restrictions.length;
for (int j = 0; j < restrictionCount; j++) {
if (restrictions[j]) {
final int changedCode = j;
mHandler.post(() -> notifyWatchersOfChange(changedCode, UID_ANY));
}
}
}
destroy();
}
}
@@ -7018,23 +6797,10 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
public void destroy() {
token.unlinkToDeath(this, 0);
}
private boolean isDefault(boolean[] array) {
if (ArrayUtils.isEmpty(array)) {
return true;
}
for (boolean value : array) {
if (value) {
return false;
}
}
return true;
}
}
private final class ClientGlobalRestrictionState implements DeathRecipient {
final IBinder mToken;
final ArraySet<Integer> mRestrictedOps = new ArraySet<>();
ClientGlobalRestrictionState(IBinder token)
throws RemoteException {
@@ -7043,23 +6809,21 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch
}
boolean setRestriction(int code, boolean restricted) {
if (restricted) {
return mRestrictedOps.add(code);
} else {
return mRestrictedOps.remove(code);
}
return mAppOpsRestrictions.setGlobalRestriction(mToken, code, restricted);
}
boolean hasRestriction(int code) {
return mRestrictedOps.contains(code);
return mAppOpsRestrictions.getGlobalRestriction(mToken, code);
}
boolean isDefault() {
return mRestrictedOps.isEmpty();
return !mAppOpsRestrictions.hasGlobalRestrictions(mToken);
}
@Override
public void binderDied() {
mAppOpsRestrictions.clearGlobalRestrictions(mToken);
mOpGlobalRestrictions.remove(mToken);
destroy();
}

View File

@@ -145,6 +145,14 @@ public interface AppOpsServiceInterface {
*/
ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners(@NonNull String packageName);
/**
* Temporary API which will be removed once we can safely untangle the methods that use this.
* Notify that the app-op's mode is changed by triggering the change listener.
* @param op App-op whose mode has changed
* @param uid user id associated with the app-op (or, if UID_ANY, notifies all users)
*/
void notifyWatchersOfChange(int op, int uid);
/**
* Temporary API which will be removed once we can safely untangle the methods that use this.
* Notify that the app-op's mode is changed by triggering the change listener.
@@ -198,5 +206,4 @@ public interface AppOpsServiceInterface {
* @param printWriter writer to dump to.
*/
boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, PrintWriter printWriter);
}

View File

@@ -332,6 +332,18 @@ public class LegacyAppOpsServiceInterfaceImpl implements AppOpsServiceInterface
}
}
@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) {

View File

@@ -0,0 +1,277 @@
/*
* 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.OP_COARSE_LOCATION;
import static android.app.AppOpsManager.OP_FINE_LOCATION;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import android.os.Handler;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.quality.Strictness;
public class AppOpsLegacyRestrictionsTest {
private static final int UID_ANY = -2;
final Object mClientToken = new Object();
final int mUserId1 = 65001;
final int mUserId2 = 65002;
final int mOpCode1 = OP_COARSE_LOCATION;
final int mOpCode2 = OP_FINE_LOCATION;
final String mPackageName = "com.example.test";
final String mAttributionTag = "test-attribution-tag";
StaticMockitoSession mSession;
@Mock
AppOpsService.Constants mConstants;
@Mock
Context mContext;
@Mock
Handler mHandler;
@Mock
AppOpsServiceInterface mLegacyAppOpsService;
AppOpsRestrictions mAppOpsRestrictions;
@Before
public void setUp() {
mSession = ExtendedMockito.mockitoSession()
.initMocks(this)
.strictness(Strictness.LENIENT)
.startMocking();
mConstants.TOP_STATE_SETTLE_TIME = 10 * 1000L;
mConstants.FG_SERVICE_STATE_SETTLE_TIME = 5 * 1000L;
mConstants.BG_STATE_SETTLE_TIME = 1 * 1000L;
Mockito.when(mHandler.post(Mockito.any(Runnable.class))).then(inv -> {
Runnable r = inv.getArgument(0);
r.run();
return true;
});
mAppOpsRestrictions = new AppOpsRestrictionsImpl(mContext, mHandler, mLegacyAppOpsService);
}
@After
public void tearDown() {
mSession.finishMocking();
}
@Test
public void testSetAndGetSingleGlobalRestriction() {
// Verify: empty
assertEquals(false, mAppOpsRestrictions.hasGlobalRestrictions(mClientToken));
assertEquals(false, mAppOpsRestrictions.getGlobalRestriction(mClientToken, mOpCode1));
// Act: add a restriction
assertEquals(true, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode1, true));
// Act: add same restriction again (expect false; should be no-op)
assertEquals(false, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode1, true));
// Verify: not empty
assertEquals(true, mAppOpsRestrictions.hasGlobalRestrictions(mClientToken));
assertEquals(true, mAppOpsRestrictions.getGlobalRestriction(mClientToken, mOpCode1));
// Act: remove the restriction
assertEquals(true, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode1, false));
// Act: remove same restriction again (expect false; should be no-op)
assertEquals(false,
mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode1, false));
// Verify: empty
assertEquals(false, mAppOpsRestrictions.hasGlobalRestrictions(mClientToken));
assertEquals(false, mAppOpsRestrictions.getGlobalRestriction(mClientToken, mOpCode1));
}
@Test
public void testSetAndGetDoubleGlobalRestriction() {
// Act: add opCode1 restriction
assertEquals(true, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode1, true));
// Act: add opCode2 restriction
assertEquals(true, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode2, true));
// Verify: not empty
assertEquals(true, mAppOpsRestrictions.hasGlobalRestrictions(mClientToken));
// Act: remove opCode1 restriction
assertEquals(true, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode1, false));
// Verify: not empty
assertEquals(true, mAppOpsRestrictions.hasGlobalRestrictions(mClientToken));
// Act: remove opCode2 restriction
assertEquals(true, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode2, false));
// Verify: empty
assertEquals(false, mAppOpsRestrictions.hasGlobalRestrictions(mClientToken));
}
@Test
public void testClearGlobalRestrictions() {
// Act: clear (should be no-op)
assertEquals(false, mAppOpsRestrictions.clearGlobalRestrictions(mClientToken));
// Act: add opCodes
assertEquals(true, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode1, true));
assertEquals(true, mAppOpsRestrictions.setGlobalRestriction(mClientToken, mOpCode2, true));
// Verify: not empty
assertEquals(true, mAppOpsRestrictions.hasGlobalRestrictions(mClientToken));
// Act: clear
assertEquals(true, mAppOpsRestrictions.clearGlobalRestrictions(mClientToken));
// Verify: empty
assertEquals(false, mAppOpsRestrictions.hasGlobalRestrictions(mClientToken));
// Act: clear (should be no-op)
assertEquals(false, mAppOpsRestrictions.clearGlobalRestrictions(mClientToken));
}
@Test
public void testSetAndGetSingleUserRestriction() {
// Verify: empty
assertEquals(false, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
assertEquals(false, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, false));
assertEquals(false, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, true));
// Act: add a restriction
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode1, true, null));
// Act: add the restriction again (should be no-op)
assertEquals(false, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode1, true, null));
// Verify: not empty
assertEquals(true, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, false));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, true));
// Act: remove the restriction
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode1, false, null));
// Act: remove the restriction again (should be no-op)
assertEquals(false, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode1, false, null));
// Verify: empty
assertEquals(false, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
assertEquals(false, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, false));
}
@Test
public void testSetAndGetDoubleUserRestriction() {
// Act: add opCode1 restriction
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode1, true, null));
// Act: add opCode2 restriction
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode2, true, null));
// Verify: not empty
assertEquals(true, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, false));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, true));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode2, mPackageName, mAttributionTag, false));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode2, mPackageName, mAttributionTag, true));
// Act: remove opCode1 restriction
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode1, false, null));
// Verify: opCode1 is removed but not opCode22
assertEquals(true, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
assertEquals(false, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, false));
assertEquals(false, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, true));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode2, mPackageName, mAttributionTag, false));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode2, mPackageName, mAttributionTag, true));
// Act: remove opCode2 restriction
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode2, false, null));
// Verify: empty
assertEquals(false, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode2, mPackageName, mAttributionTag, false));
assertEquals(false, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode2, mPackageName, mAttributionTag, true));
assertEquals(false, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
}
@Test
public void testClearUserRestrictionsAllUsers() {
// Act: clear (should be no-op)
assertEquals(false, mAppOpsRestrictions.clearUserRestrictions(mClientToken));
// Act: add restrictions
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode1, true, null));
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode2, true, null));
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId2, mOpCode1, true, null));
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId2, mOpCode2, true, null));
// Verify: not empty
assertEquals(true, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
// Act: clear all user restrictions
assertEquals(true, mAppOpsRestrictions.clearUserRestrictions(mClientToken));
// Verify: empty
assertEquals(false, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
}
@Test
public void testClearUserRestrictionsSpecificUsers() {
// Act: clear (should be no-op)
assertEquals(false, mAppOpsRestrictions.clearUserRestrictions(mClientToken, mUserId1));
// Act: add restrictions
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode1, true, null));
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId1, mOpCode2, true, null));
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId2, mOpCode1, true, null));
assertEquals(true, mAppOpsRestrictions.setUserRestriction(
mClientToken, mUserId2, mOpCode2, true, null));
// Verify: not empty
assertEquals(true, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
// Act: clear userId1
assertEquals(true, mAppOpsRestrictions.clearUserRestrictions(mClientToken, mUserId1));
// Act: clear userId1 again (should be no-op)
assertEquals(false, mAppOpsRestrictions.clearUserRestrictions(mClientToken, mUserId1));
// Verify: userId1 is removed but not userId2
assertEquals(false, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId1, mOpCode1, mPackageName, mAttributionTag, false));
assertEquals(true, mAppOpsRestrictions.getUserRestriction(
mClientToken, mUserId2, mOpCode2, mPackageName, mAttributionTag, false));
// Act: clear userId2
assertEquals(true, mAppOpsRestrictions.clearUserRestrictions(mClientToken, mUserId2));
// Act: clear userId2 again (should be no-op)
assertEquals(false, mAppOpsRestrictions.clearUserRestrictions(mClientToken, mUserId2));
// Verify: empty
assertEquals(false, mAppOpsRestrictions.hasUserRestrictions(mClientToken));
}
@Test
public void testNotify() {
mAppOpsRestrictions.setUserRestriction(mClientToken, mUserId1, mOpCode1, true, null);
mAppOpsRestrictions.clearUserRestrictions(mClientToken);
Mockito.verify(mLegacyAppOpsService, Mockito.times(1))
.notifyWatchersOfChange(mOpCode1, UID_ANY);
}
}