From 7699ecdf364d67c9f64a8d31a3a9a76c92484882 Mon Sep 17 00:00:00 2001 From: Jay Thomas Sullivan Date: Tue, 20 Sep 2022 12:18:02 -0700 Subject: [PATCH] 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 --- .../server/appop/AppOpsRestrictions.java | 147 ++++++ .../server/appop/AppOpsRestrictionsImpl.java | 452 ++++++++++++++++++ .../android/server/appop/AppOpsService.java | 276 +---------- .../server/appop/AppOpsServiceInterface.java | 9 +- .../LegacyAppOpsServiceInterfaceImpl.java | 12 + .../appop/AppOpsLegacyRestrictionsTest.java | 277 +++++++++++ 6 files changed, 916 insertions(+), 257 deletions(-) create mode 100644 services/core/java/com/android/server/appop/AppOpsRestrictions.java create mode 100644 services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java diff --git a/services/core/java/com/android/server/appop/AppOpsRestrictions.java b/services/core/java/com/android/server/appop/AppOpsRestrictions.java new file mode 100644 index 0000000000000..f7ccd3429f114 --- /dev/null +++ b/services/core/java/com/android/server/appop/AppOpsRestrictions.java @@ -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); +} diff --git a/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java b/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java new file mode 100644 index 0000000000000..adfd2afffd78e --- /dev/null +++ b/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java @@ -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 mGlobalRestrictions = new ArrayMap<>(); + + // Map from (Object token) to (int userId) to (int code) to (boolean restricted) + private final ArrayMap> mUserRestrictions = + new ArrayMap<>(); + + // Map from (Object token) to (int userId) to (PackageTagsList packageTagsList) + private final ArrayMap> + 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 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 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 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 userIdRestrictedCodes = + mUserRestrictions.get(clientToken); + if (userIdRestrictedCodes != null) { + changed |= userIdRestrictedCodes.contains(userId); + userIdRestrictedCodes.remove(userId); + if (userIdRestrictedCodes.size() == 0) { + mUserRestrictions.remove(clientToken); + } + } + + SparseArray 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 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 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 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 userIdExcludedPackageTags = + mUserRestrictionExcludedPackageTags.get(token); + Objects.requireNonNull(userIdExcludedPackageTags); + + userIdExcludedPackageTags.put(userId, excludedPackageTags); + return true; + } else { + SparseArray 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 perUserRestrictions = mUserRestrictions.get(token); + SparseArray 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(); + } + } + } +} diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index af73c2bac6f9b..841441a5f212f 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -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 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 perUserRestrictions; - SparseArray 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 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 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(); } diff --git a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsServiceInterface.java index c4a9a4b5d7279..18f659e4c62a9 100644 --- a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java +++ b/services/core/java/com/android/server/appop/AppOpsServiceInterface.java @@ -145,6 +145,14 @@ public interface AppOpsServiceInterface { */ ArraySet 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); - } diff --git a/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java b/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java index 802669728b241..f6fff351c2323 100644 --- a/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java +++ b/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java @@ -332,6 +332,18 @@ public class LegacyAppOpsServiceInterfaceImpl implements AppOpsServiceInterface } } + @Override + public void notifyWatchersOfChange(int code, int uid) { + ArraySet 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) { diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java new file mode 100644 index 0000000000000..5dc12510368c5 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java @@ -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); + } +}