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); + } +}