Merge "Move DisposableBroadcastReceiverAsUser"
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index 05daf63..a87b133 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -147,119 +147,146 @@
     @MainThread
     @Override
     public void executeMessage(Message msg) {
-        InputMethod inputMethod = mInputMethod.get();
-        // Need a valid reference to the inputMethod for everything except a dump.
-        if (inputMethod == null && msg.what != DO_DUMP) {
-            Log.w(TAG, "Input method reference was null, ignoring message: " + msg.what);
-            return;
-        }
-
+        final InputMethod inputMethod = mInputMethod.get();
+        final InputMethodServiceInternal target = mTarget.get();
         switch (msg.what) {
             case DO_DUMP: {
-                InputMethodServiceInternal target = mTarget.get();
-                if (target == null) {
-                    return;
-                }
                 SomeArgs args = (SomeArgs)msg.obj;
-                try {
-                    target.dump((FileDescriptor) args.arg1,
-                            (PrintWriter) args.arg2, (String[]) args.arg3);
-                } catch (RuntimeException e) {
-                    ((PrintWriter)args.arg2).println("Exception: " + e);
-                }
-                synchronized (args.arg4) {
-                    ((CountDownLatch)args.arg4).countDown();
+                if (isValid(inputMethod, target, "DO_DUMP")) {
+                    final FileDescriptor fd = (FileDescriptor) args.arg1;
+                    final PrintWriter fout = (PrintWriter) args.arg2;
+                    final String[] dumpArgs = (String[]) args.arg3;
+                    final CountDownLatch latch = (CountDownLatch) args.arg4;
+                    try {
+                        target.dump(fd, fout, dumpArgs);
+                    } catch (RuntimeException e) {
+                        fout.println("Exception: " + e);
+                    } finally {
+                        latch.countDown();
+                    }
                 }
                 args.recycle();
                 return;
             }
             case DO_INITIALIZE_INTERNAL:
-                inputMethod.initializeInternal((IInputMethod.InitParams) msg.obj);
+                if (isValid(inputMethod, target, "DO_INITIALIZE_INTERNAL")) {
+                    inputMethod.initializeInternal((IInputMethod.InitParams) msg.obj);
+                }
                 return;
             case DO_SET_INPUT_CONTEXT: {
-                inputMethod.bindInput((InputBinding)msg.obj);
+                if (isValid(inputMethod, target, "DO_SET_INPUT_CONTEXT")) {
+                    inputMethod.bindInput((InputBinding) msg.obj);
+                }
                 return;
             }
             case DO_UNSET_INPUT_CONTEXT:
-                inputMethod.unbindInput();
+                if (isValid(inputMethod, target, "DO_UNSET_INPUT_CONTEXT")) {
+                    inputMethod.unbindInput();
+                }
                 return;
             case DO_START_INPUT: {
                 final SomeArgs args = (SomeArgs) msg.obj;
-                final InputConnection inputConnection = (InputConnection) args.arg1;
-                final IInputMethod.StartInputParams params =
-                        (IInputMethod.StartInputParams) args.arg2;
-                inputMethod.dispatchStartInput(inputConnection, params);
+                if (isValid(inputMethod, target, "DO_START_INPUT")) {
+                    final InputConnection inputConnection = (InputConnection) args.arg1;
+                    final IInputMethod.StartInputParams params =
+                            (IInputMethod.StartInputParams) args.arg2;
+                    inputMethod.dispatchStartInput(inputConnection, params);
+                }
                 args.recycle();
                 return;
             }
             case DO_ON_NAV_BUTTON_FLAGS_CHANGED:
-                inputMethod.onNavButtonFlagsChanged(msg.arg1);
+                if (isValid(inputMethod, target, "DO_ON_NAV_BUTTON_FLAGS_CHANGED")) {
+                    inputMethod.onNavButtonFlagsChanged(msg.arg1);
+                }
                 return;
             case DO_CREATE_SESSION: {
                 SomeArgs args = (SomeArgs)msg.obj;
-                inputMethod.createSession(new InputMethodSessionCallbackWrapper(
-                        mContext, (InputChannel) args.arg1,
-                        (IInputMethodSessionCallback) args.arg2));
+                if (isValid(inputMethod, target, "DO_CREATE_SESSION")) {
+                    inputMethod.createSession(new InputMethodSessionCallbackWrapper(
+                            mContext, (InputChannel) args.arg1,
+                            (IInputMethodSessionCallback) args.arg2));
+                }
                 args.recycle();
                 return;
             }
             case DO_SET_SESSION_ENABLED:
-                inputMethod.setSessionEnabled((InputMethodSession)msg.obj,
-                        msg.arg1 != 0);
+                if (isValid(inputMethod, target, "DO_SET_SESSION_ENABLED")) {
+                    inputMethod.setSessionEnabled((InputMethodSession) msg.obj, msg.arg1 != 0);
+                }
                 return;
             case DO_SHOW_SOFT_INPUT: {
                 final SomeArgs args = (SomeArgs)msg.obj;
-                inputMethod.showSoftInputWithToken(
-                        msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1);
+                if (isValid(inputMethod, target, "DO_SHOW_SOFT_INPUT")) {
+                    inputMethod.showSoftInputWithToken(
+                            msg.arg1, (ResultReceiver) args.arg2, (IBinder) args.arg1);
+                }
                 args.recycle();
                 return;
             }
             case DO_HIDE_SOFT_INPUT: {
                 final SomeArgs args = (SomeArgs) msg.obj;
-                inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2,
-                        (IBinder) args.arg1);
+                if (isValid(inputMethod, target, "DO_HIDE_SOFT_INPUT")) {
+                    inputMethod.hideSoftInputWithToken(msg.arg1, (ResultReceiver) args.arg2,
+                            (IBinder) args.arg1);
+                }
                 args.recycle();
                 return;
             }
             case DO_CHANGE_INPUTMETHOD_SUBTYPE:
-                inputMethod.changeInputMethodSubtype((InputMethodSubtype)msg.obj);
+                if (isValid(inputMethod, target, "DO_CHANGE_INPUTMETHOD_SUBTYPE")) {
+                    inputMethod.changeInputMethodSubtype((InputMethodSubtype) msg.obj);
+                }
                 return;
             case DO_CREATE_INLINE_SUGGESTIONS_REQUEST: {
                 final SomeArgs args = (SomeArgs) msg.obj;
-                inputMethod.onCreateInlineSuggestionsRequest(
-                        (InlineSuggestionsRequestInfo) args.arg1,
-                        (IInlineSuggestionsRequestCallback) args.arg2);
+                if (isValid(inputMethod, target, "DO_CREATE_INLINE_SUGGESTIONS_REQUEST")) {
+                    inputMethod.onCreateInlineSuggestionsRequest(
+                            (InlineSuggestionsRequestInfo) args.arg1,
+                            (IInlineSuggestionsRequestCallback) args.arg2);
+                }
                 args.recycle();
                 return;
             }
             case DO_CAN_START_STYLUS_HANDWRITING: {
-                inputMethod.canStartStylusHandwriting(msg.arg1);
+                if (isValid(inputMethod, target, "DO_CAN_START_STYLUS_HANDWRITING")) {
+                    inputMethod.canStartStylusHandwriting(msg.arg1);
+                }
                 return;
             }
             case DO_UPDATE_TOOL_TYPE: {
-                inputMethod.updateEditorToolType(msg.arg1);
+                if (isValid(inputMethod, target, "DO_UPDATE_TOOL_TYPE")) {
+                    inputMethod.updateEditorToolType(msg.arg1);
+                }
                 return;
             }
             case DO_START_STYLUS_HANDWRITING: {
                 final SomeArgs args = (SomeArgs) msg.obj;
-                inputMethod.startStylusHandwriting(msg.arg1, (InputChannel) args.arg1,
-                        (List<MotionEvent>) args.arg2);
+                if (isValid(inputMethod, target, "DO_START_STYLUS_HANDWRITING")) {
+                    inputMethod.startStylusHandwriting(msg.arg1, (InputChannel) args.arg1,
+                            (List<MotionEvent>) args.arg2);
+                }
                 args.recycle();
                 return;
             }
             case DO_INIT_INK_WINDOW: {
-                inputMethod.initInkWindow();
+                if (isValid(inputMethod, target, "DO_INIT_INK_WINDOW")) {
+                    inputMethod.initInkWindow();
+                }
                 return;
             }
             case DO_FINISH_STYLUS_HANDWRITING: {
-                inputMethod.finishStylusHandwriting();
+                if (isValid(inputMethod, target, "DO_FINISH_STYLUS_HANDWRITING")) {
+                    inputMethod.finishStylusHandwriting();
+                }
                 return;
             }
             case DO_REMOVE_STYLUS_HANDWRITING_WINDOW: {
-                inputMethod.removeStylusHandwritingWindow();
+                if (isValid(inputMethod, target, "DO_REMOVE_STYLUS_HANDWRITING_WINDOW")) {
+                    inputMethod.removeStylusHandwritingWindow();
+                }
                 return;
             }
-
         }
         Log.w(TAG, "Unhandled message code: " + msg.what);
     }
@@ -445,4 +472,15 @@
     public void removeStylusHandwritingWindow() {
         mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_STYLUS_HANDWRITING_WINDOW));
     }
+
+    private static boolean isValid(InputMethod inputMethod, InputMethodServiceInternal target,
+            String msg) {
+        if (inputMethod != null && target != null && !target.isServiceDestroyed()) {
+            return true;
+        } else {
+            Log.w(TAG, "Ignoring " + msg + ", InputMethod:" + inputMethod
+                    + ", InputMethodServiceInternal:" + target);
+            return false;
+        }
+    }
 }
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 7436601..8b3451e 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -700,11 +700,6 @@
         @MainThread
         @Override
         public final void initializeInternal(@NonNull IInputMethod.InitParams params) {
-            if (mDestroyed) {
-                Log.i(TAG, "The InputMethodService has already onDestroyed()."
-                    + "Ignore the initialization.");
-                return;
-            }
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMS.initializeInternal");
             mConfigTracker.onInitialize(params.configChanges);
             mPrivOps.set(params.privilegedOperations);
@@ -3938,6 +3933,14 @@
             public void triggerServiceDump(String where, @Nullable byte[] icProto) {
                 ImeTracing.getInstance().triggerServiceDump(where, mDumper, icProto);
             }
+
+            /**
+             * {@inheritDoc}
+             */
+            @Override
+            public boolean isServiceDestroyed() {
+                return mDestroyed;
+            }
         };
     }
 
diff --git a/core/java/android/inputmethodservice/InputMethodServiceInternal.java b/core/java/android/inputmethodservice/InputMethodServiceInternal.java
index f44f49d..c6612f6 100644
--- a/core/java/android/inputmethodservice/InputMethodServiceInternal.java
+++ b/core/java/android/inputmethodservice/InputMethodServiceInternal.java
@@ -85,4 +85,11 @@
      */
     default void triggerServiceDump(@NonNull String where, @Nullable byte[] icProto) {
     }
+
+    /**
+     * @return {@code true} if {@link InputMethodService} is destroyed.
+     */
+    default boolean isServiceDestroyed() {
+        return false;
+    };
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 41c0367..d67f94f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -168,6 +168,17 @@
         return new ShelfState();
     }
 
+    @Override
+    public String toString() {
+        return "NotificationShelf("
+                + "hideBackground=" + mHideBackground + " notGoneIndex=" + mNotGoneIndex
+                + " hasItemsInStableShelf=" + mHasItemsInStableShelf
+                + " statusBarState=" + mStatusBarState + " interactive=" + mInteractive
+                + " animationsEnabled=" + mAnimationsEnabled
+                + " showNotificationShelf=" + mShowNotificationShelf
+                + " indexOfFirstViewInShelf=" + mIndexOfFirstViewInShelf + ')';
+    }
+
     /** Update the state of the shelf. */
     public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState,
             AmbientState ambientState) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index 039a362..827d0d0f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -36,7 +36,6 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
-import android.os.Parcelable;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
@@ -551,9 +550,12 @@
         }
     }
 
+    @Override
     public String toString() {
-        return "StatusBarIconView(slot=" + mSlot + " icon=" + mIcon
-            + " notification=" + mNotification + ")";
+        return "StatusBarIconView("
+                + "slot='" + mSlot + " alpha=" + getAlpha() + " icon=" + mIcon
+                + " iconColor=#" + Integer.toHexString(mIconColor)
+                + " notification=" + mNotification + ')';
     }
 
     public StatusBarNotification getNotification() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 7b8c5fc..5a70d89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -278,6 +278,15 @@
         }
     }
 
+    @Override
+    public String toString() {
+        return "NotificationIconContainer("
+                + "dozing=" + mDozing + " onLockScreen=" + mOnLockScreen
+                + " inNotificationIconShelf=" + mInNotificationIconShelf
+                + " speedBumpIndex=" + mSpeedBumpIndex
+                + " themedTextColorPrimary=#" + Integer.toHexString(mThemedTextColorPrimary) + ')';
+    }
+
     @VisibleForTesting
     public void setIconSize(int size) {
         mIconSize = size;
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 0000000..f7ccd34
--- /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 0000000..adfd2af
--- /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<Object, SparseBooleanArray> mGlobalRestrictions = new ArrayMap<>();
+
+    // Map from (Object token) to (int userId) to (int code) to (boolean restricted)
+    private final ArrayMap<Object, SparseArray<SparseBooleanArray>> mUserRestrictions =
+            new ArrayMap<>();
+
+    // Map from (Object token) to (int userId) to (PackageTagsList packageTagsList)
+    private final ArrayMap<Object, SparseArray<PackageTagsList>>
+            mUserRestrictionExcludedPackageTags = new ArrayMap<>();
+
+    public AppOpsRestrictionsImpl(Context context, Handler handler,
+            AppOpsServiceInterface appOpsServiceInterface) {
+        mContext = context;
+        mHandler = handler;
+        mAppOpsServiceInterface = appOpsServiceInterface;
+    }
+
+    @Override
+    public boolean setGlobalRestriction(Object clientToken, int code, boolean restricted) {
+        if (restricted) {
+            if (!mGlobalRestrictions.containsKey(clientToken)) {
+                mGlobalRestrictions.put(clientToken, new SparseBooleanArray());
+            }
+            SparseBooleanArray restrictedCodes = mGlobalRestrictions.get(clientToken);
+            Objects.requireNonNull(restrictedCodes);
+            boolean changed = !restrictedCodes.get(code);
+            restrictedCodes.put(code, true);
+            return changed;
+        } else {
+            SparseBooleanArray restrictedCodes = mGlobalRestrictions.get(clientToken);
+            if (restrictedCodes == null) {
+                return false;
+            }
+            boolean changed = restrictedCodes.get(code);
+            restrictedCodes.delete(code);
+            if (restrictedCodes.size() == 0) {
+                mGlobalRestrictions.remove(clientToken);
+            }
+            return changed;
+        }
+    }
+
+    @Override
+    public boolean getGlobalRestriction(Object clientToken, int code) {
+        SparseBooleanArray restrictedCodes = mGlobalRestrictions.get(clientToken);
+        if (restrictedCodes == null) {
+            return false;
+        }
+        return restrictedCodes.get(code);
+    }
+
+    @Override
+    public boolean hasGlobalRestrictions(Object clientToken) {
+        return mGlobalRestrictions.containsKey(clientToken);
+    }
+
+    @Override
+    public boolean clearGlobalRestrictions(Object clientToken) {
+        return mGlobalRestrictions.remove(clientToken) != null;
+    }
+
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.CREATE_USERS
+    })
+    @Override
+    public boolean setUserRestriction(Object clientToken, int userId, int code,
+            boolean restricted,
+            PackageTagsList excludedPackageTags) {
+        int[] userIds = resolveUserId(userId);
+        boolean changed = false;
+        for (int i = 0; i < userIds.length; i++) {
+            changed |= putUserRestriction(clientToken, userIds[i], code, restricted);
+            changed |= putUserRestrictionExclusions(clientToken, userIds[i],
+                    excludedPackageTags);
+        }
+        return changed;
+    }
+
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_USERS,
+            android.Manifest.permission.CREATE_USERS
+    })
+    private int[] resolveUserId(int userId) {
+        int[] userIds;
+        if (userId == UserHandle.USER_ALL) {
+            // TODO(b/162888972): this call is returning all users, not just live ones - we
+            // need to either fix the method called, or rename the variable
+            List<UserInfo> liveUsers = UserManager.get(mContext).getUsers();
+
+            userIds = new int[liveUsers.size()];
+            for (int i = 0; i < liveUsers.size(); i++) {
+                userIds[i] = liveUsers.get(i).id;
+            }
+        } else {
+            userIds = new int[]{userId};
+        }
+        return userIds;
+    }
+
+    @Override
+    public boolean hasUserRestrictions(Object clientToken) {
+        return mUserRestrictions.containsKey(clientToken);
+    }
+
+    private boolean getUserRestriction(Object clientToken, int userId, int code) {
+        SparseArray<SparseBooleanArray> userIdRestrictedCodes =
+                mUserRestrictions.get(clientToken);
+        if (userIdRestrictedCodes == null) {
+            return false;
+        }
+        SparseBooleanArray restrictedCodes = userIdRestrictedCodes.get(userId);
+        if (restrictedCodes == null) {
+            return false;
+        }
+        return restrictedCodes.get(code);
+    }
+
+    @Override
+    public boolean getUserRestriction(Object clientToken, int userId, int code, String packageName,
+            String attributionTag, boolean isCheckOp) {
+        boolean restricted = getUserRestriction(clientToken, userId, code);
+        if (!restricted) {
+            return false;
+        }
+
+        PackageTagsList perUserExclusions = getUserRestrictionExclusions(clientToken, userId);
+        if (perUserExclusions == null) {
+            return true;
+        }
+
+        // TODO (b/240617242) add overload for checkOp to support attribution tags
+        if (isCheckOp) {
+            return !perUserExclusions.includes(packageName);
+        }
+        return !perUserExclusions.contains(packageName, attributionTag);
+    }
+
+    @Override
+    public boolean clearUserRestrictions(Object clientToken) {
+        boolean changed = false;
+        SparseBooleanArray allUserRestrictedCodes = collectAllUserRestrictedCodes(clientToken);
+        changed |= mUserRestrictions.remove(clientToken) != null;
+        changed |= mUserRestrictionExcludedPackageTags.remove(clientToken) != null;
+        notifyAllUserRestrictions(allUserRestrictedCodes);
+        return changed;
+    }
+
+    private SparseBooleanArray collectAllUserRestrictedCodes(Object clientToken) {
+        SparseBooleanArray allRestrictedCodes = new SparseBooleanArray();
+        SparseArray<SparseBooleanArray> userIdRestrictedCodes = mUserRestrictions.get(clientToken);
+        if (userIdRestrictedCodes == null) {
+            return allRestrictedCodes;
+        }
+        int userIdRestrictedCodesSize = userIdRestrictedCodes.size();
+        for (int i = 0; i < userIdRestrictedCodesSize; i++) {
+            SparseBooleanArray restrictedCodes = userIdRestrictedCodes.valueAt(i);
+            int restrictedCodesSize = restrictedCodes.size();
+            for (int j = 0; j < restrictedCodesSize; j++) {
+                int code = restrictedCodes.keyAt(j);
+                allRestrictedCodes.put(code, true);
+            }
+        }
+        return allRestrictedCodes;
+    }
+
+    // TODO: For clearUserRestrictions, we are calling notifyOpChanged from within the
+    //  LegacyAppOpsServiceInterfaceImpl class. But, for all other changes to restrictions, we're
+    //  calling it from within AppOpsService. This is awkward, and we should probably do it one
+    //  way or the other.
+    private void notifyAllUserRestrictions(SparseBooleanArray allUserRestrictedCodes) {
+        int restrictedCodesSize = allUserRestrictedCodes.size();
+        for (int j = 0; j < restrictedCodesSize; j++) {
+            int code = allUserRestrictedCodes.keyAt(j);
+            mHandler.post(() -> mAppOpsServiceInterface.notifyWatchersOfChange(code, UID_ANY));
+        }
+    }
+
+    @Override
+    public boolean clearUserRestrictions(Object clientToken, Integer userId) {
+        boolean changed = false;
+
+        SparseArray<SparseBooleanArray> userIdRestrictedCodes =
+                mUserRestrictions.get(clientToken);
+        if (userIdRestrictedCodes != null) {
+            changed |= userIdRestrictedCodes.contains(userId);
+            userIdRestrictedCodes.remove(userId);
+            if (userIdRestrictedCodes.size() == 0) {
+                mUserRestrictions.remove(clientToken);
+            }
+        }
+
+        SparseArray<PackageTagsList> userIdPackageTags =
+                mUserRestrictionExcludedPackageTags.get(clientToken);
+        if (userIdPackageTags != null) {
+            changed |= userIdPackageTags.contains(userId);
+            userIdPackageTags.remove(userId);
+            if (userIdPackageTags.size() == 0) {
+                mUserRestrictionExcludedPackageTags.remove(clientToken);
+            }
+        }
+
+        return changed;
+    }
+
+    private boolean putUserRestriction(Object token, int userId, int code, boolean restricted) {
+        boolean changed = false;
+        if (restricted) {
+            if (!mUserRestrictions.containsKey(token)) {
+                mUserRestrictions.put(token, new SparseArray<>());
+            }
+            SparseArray<SparseBooleanArray> userIdRestrictedCodes = mUserRestrictions.get(token);
+            Objects.requireNonNull(userIdRestrictedCodes);
+
+            if (!userIdRestrictedCodes.contains(userId)) {
+                userIdRestrictedCodes.put(userId, new SparseBooleanArray());
+            }
+            SparseBooleanArray restrictedCodes = userIdRestrictedCodes.get(userId);
+
+            changed = !restrictedCodes.get(code);
+            restrictedCodes.put(code, restricted);
+        } else {
+            SparseArray<SparseBooleanArray> userIdRestrictedCodes = mUserRestrictions.get(token);
+            if (userIdRestrictedCodes == null) {
+                return false;
+            }
+            SparseBooleanArray restrictedCodes = userIdRestrictedCodes.get(userId);
+            if (restrictedCodes == null) {
+                return false;
+            }
+            changed = restrictedCodes.get(code);
+            restrictedCodes.delete(code);
+            if (restrictedCodes.size() == 0) {
+                userIdRestrictedCodes.remove(userId);
+            }
+            if (userIdRestrictedCodes.size() == 0) {
+                mUserRestrictions.remove(token);
+            }
+        }
+        return changed;
+    }
+
+    @Override
+    public PackageTagsList getUserRestrictionExclusions(Object clientToken, int userId) {
+        SparseArray<PackageTagsList> userIdPackageTags =
+                mUserRestrictionExcludedPackageTags.get(clientToken);
+        if (userIdPackageTags == null) {
+            return null;
+        }
+        return userIdPackageTags.get(userId);
+    }
+
+    private boolean putUserRestrictionExclusions(Object token, int userId,
+            PackageTagsList excludedPackageTags) {
+        boolean addingExclusions = excludedPackageTags != null && !excludedPackageTags.isEmpty();
+        if (addingExclusions) {
+            if (!mUserRestrictionExcludedPackageTags.containsKey(token)) {
+                mUserRestrictionExcludedPackageTags.put(token, new SparseArray<>());
+            }
+            SparseArray<PackageTagsList> userIdExcludedPackageTags =
+                    mUserRestrictionExcludedPackageTags.get(token);
+            Objects.requireNonNull(userIdExcludedPackageTags);
+
+            userIdExcludedPackageTags.put(userId, excludedPackageTags);
+            return true;
+        } else {
+            SparseArray<PackageTagsList> userIdExclusions =
+                    mUserRestrictionExcludedPackageTags.get(token);
+            if (userIdExclusions == null) {
+                return false;
+            }
+            boolean changed = userIdExclusions.get(userId) != null;
+            userIdExclusions.remove(userId);
+            if (userIdExclusions.size() == 0) {
+                mUserRestrictionExcludedPackageTags.remove(token);
+            }
+            return changed;
+        }
+    }
+
+    @Override
+    public void dumpRestrictions(PrintWriter pw, int code, String dumpPackage,
+            boolean showUserRestrictions) {
+        final int globalRestrictionCount = mGlobalRestrictions.size();
+        for (int i = 0; i < globalRestrictionCount; i++) {
+            Object token = mGlobalRestrictions.keyAt(i);
+            SparseBooleanArray restrictedOps = mGlobalRestrictions.valueAt(i);
+
+            pw.println("  Global restrictions for token " + token + ":");
+            StringBuilder restrictedOpsValue = new StringBuilder();
+            restrictedOpsValue.append("[");
+            final int restrictedOpCount = restrictedOps.size();
+            for (int j = 0; j < restrictedOpCount; j++) {
+                if (restrictedOpsValue.length() > 1) {
+                    restrictedOpsValue.append(", ");
+                }
+                restrictedOpsValue.append(AppOpsManager.opToName(restrictedOps.keyAt(j)));
+            }
+            restrictedOpsValue.append("]");
+            pw.println("      Restricted ops: " + restrictedOpsValue);
+        }
+
+        if (!showUserRestrictions) {
+            return;
+        }
+
+        final int userRestrictionCount = mUserRestrictions.size();
+        for (int i = 0; i < userRestrictionCount; i++) {
+            Object token = mUserRestrictions.keyAt(i);
+            SparseArray<SparseBooleanArray> perUserRestrictions = mUserRestrictions.get(token);
+            SparseArray<PackageTagsList> perUserExcludedPackageTags =
+                    mUserRestrictionExcludedPackageTags.get(token);
+
+            boolean printedTokenHeader = false;
+
+            final int restrictionCount = perUserRestrictions != null
+                    ? perUserRestrictions.size() : 0;
+            if (restrictionCount > 0 && dumpPackage == null) {
+                boolean printedOpsHeader = false;
+                for (int j = 0; j < restrictionCount; j++) {
+                    int userId = perUserRestrictions.keyAt(j);
+                    SparseBooleanArray restrictedOps = perUserRestrictions.valueAt(j);
+                    if (restrictedOps == null) {
+                        continue;
+                    }
+                    if (code >= 0 && !restrictedOps.get(code)) {
+                        continue;
+                    }
+                    if (!printedTokenHeader) {
+                        pw.println("  User restrictions for token " + token + ":");
+                        printedTokenHeader = true;
+                    }
+                    if (!printedOpsHeader) {
+                        pw.println("      Restricted ops:");
+                        printedOpsHeader = true;
+                    }
+                    StringBuilder restrictedOpsValue = new StringBuilder();
+                    restrictedOpsValue.append("[");
+                    final int restrictedOpCount = restrictedOps.size();
+                    for (int k = 0; k < restrictedOpCount; k++) {
+                        int restrictedOp = restrictedOps.keyAt(k);
+                        if (restrictedOpsValue.length() > 1) {
+                            restrictedOpsValue.append(", ");
+                        }
+                        restrictedOpsValue.append(AppOpsManager.opToName(restrictedOp));
+                    }
+                    restrictedOpsValue.append("]");
+                    pw.print("        ");
+                    pw.print("user: ");
+                    pw.print(userId);
+                    pw.print(" restricted ops: ");
+                    pw.println(restrictedOpsValue);
+                }
+            }
+
+            final int excludedPackageCount = perUserExcludedPackageTags != null
+                    ? perUserExcludedPackageTags.size() : 0;
+            if (excludedPackageCount > 0 && code < 0) {
+                IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+                ipw.increaseIndent();
+                boolean printedPackagesHeader = false;
+                for (int j = 0; j < excludedPackageCount; j++) {
+                    int userId = perUserExcludedPackageTags.keyAt(j);
+                    PackageTagsList packageNames =
+                            perUserExcludedPackageTags.valueAt(j);
+                    if (packageNames == null) {
+                        continue;
+                    }
+                    boolean hasPackage;
+                    if (dumpPackage != null) {
+                        hasPackage = packageNames.includes(dumpPackage);
+                    } else {
+                        hasPackage = true;
+                    }
+                    if (!hasPackage) {
+                        continue;
+                    }
+                    if (!printedTokenHeader) {
+                        ipw.println("User restrictions for token " + token + ":");
+                        printedTokenHeader = true;
+                    }
+
+                    ipw.increaseIndent();
+                    if (!printedPackagesHeader) {
+                        ipw.println("Excluded packages:");
+                        printedPackagesHeader = true;
+                    }
+
+                    ipw.increaseIndent();
+                    ipw.print("user: ");
+                    ipw.print(userId);
+                    ipw.println(" packages: ");
+
+                    ipw.increaseIndent();
+                    packageNames.dump(ipw);
+
+                    ipw.decreaseIndent();
+                    ipw.decreaseIndent();
+                    ipw.decreaseIndent();
+                }
+                ipw.decreaseIndent();
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index b844fc6..072d17f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -97,7 +97,6 @@
 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;
@@ -118,14 +117,12 @@
 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.Pair;
 import android.util.Slog;
@@ -367,6 +364,9 @@
     /** 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 */
@@ -926,6 +926,8 @@
         }
         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");
@@ -5348,124 +5350,8 @@
                 pw.println();
             }
 
-            final int globalRestrictionCount = mOpGlobalRestrictions.size();
-            for (int i = 0; i < globalRestrictionCount; i++) {
-                IBinder token = mOpGlobalRestrictions.keyAt(i);
-                ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.valueAt(i);
-                ArraySet<Integer> restrictedOps = restrictionState.mRestrictedOps;
-
-                pw.println("  Global restrictions for token " + token + ":");
-                StringBuilder restrictedOpsValue = new StringBuilder();
-                restrictedOpsValue.append("[");
-                final int restrictedOpCount = restrictedOps.size();
-                for (int j = 0; j < restrictedOpCount; j++) {
-                    if (restrictedOpsValue.length() > 1) {
-                        restrictedOpsValue.append(", ");
-                    }
-                    restrictedOpsValue.append(AppOpsManager.opToName(restrictedOps.valueAt(j)));
-                }
-                restrictedOpsValue.append("]");
-                pw.println("      Restricted ops: " + restrictedOpsValue);
-
-            }
-
-            final int userRestrictionCount = mOpUserRestrictions.size();
-            for (int i = 0; i < userRestrictionCount; i++) {
-                IBinder token = mOpUserRestrictions.keyAt(i);
-                ClientUserRestrictionState restrictionState = mOpUserRestrictions.valueAt(i);
-                boolean printedTokenHeader = false;
-
-                if (dumpMode >= 0 || dumpWatchers || dumpHistory) {
-                    continue;
-                }
-
-                final int restrictionCount = restrictionState.perUserRestrictions != null
-                        ? restrictionState.perUserRestrictions.size() : 0;
-                if (restrictionCount > 0 && dumpPackage == null) {
-                    boolean printedOpsHeader = false;
-                    for (int j = 0; j < restrictionCount; j++) {
-                        int userId = restrictionState.perUserRestrictions.keyAt(j);
-                        boolean[] restrictedOps = restrictionState.perUserRestrictions.valueAt(j);
-                        if (restrictedOps == null) {
-                            continue;
-                        }
-                        if (dumpOp >= 0 && (dumpOp >= restrictedOps.length
-                                || !restrictedOps[dumpOp])) {
-                            continue;
-                        }
-                        if (!printedTokenHeader) {
-                            pw.println("  User restrictions for token " + token + ":");
-                            printedTokenHeader = true;
-                        }
-                        if (!printedOpsHeader) {
-                            pw.println("      Restricted ops:");
-                            printedOpsHeader = true;
-                        }
-                        StringBuilder restrictedOpsValue = new StringBuilder();
-                        restrictedOpsValue.append("[");
-                        final int restrictedOpCount = restrictedOps.length;
-                        for (int k = 0; k < restrictedOpCount; k++) {
-                            if (restrictedOps[k]) {
-                                if (restrictedOpsValue.length() > 1) {
-                                    restrictedOpsValue.append(", ");
-                                }
-                                restrictedOpsValue.append(AppOpsManager.opToName(k));
-                            }
-                        }
-                        restrictedOpsValue.append("]");
-                        pw.print("        "); pw.print("user: "); pw.print(userId);
-                                pw.print(" restricted ops: "); pw.println(restrictedOpsValue);
-                    }
-                }
-
-                final int excludedPackageCount = restrictionState.perUserExcludedPackageTags != null
-                        ? restrictionState.perUserExcludedPackageTags.size() : 0;
-                if (excludedPackageCount > 0 && dumpOp < 0) {
-                    IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
-                    ipw.increaseIndent();
-                    boolean printedPackagesHeader = false;
-                    for (int j = 0; j < excludedPackageCount; j++) {
-                        int userId = restrictionState.perUserExcludedPackageTags.keyAt(j);
-                        PackageTagsList packageNames =
-                                restrictionState.perUserExcludedPackageTags.valueAt(j);
-                        if (packageNames == null) {
-                            continue;
-                        }
-                        boolean hasPackage;
-                        if (dumpPackage != null) {
-                            hasPackage = packageNames.includes(dumpPackage);
-                        } else {
-                            hasPackage = true;
-                        }
-                        if (!hasPackage) {
-                            continue;
-                        }
-                        if (!printedTokenHeader) {
-                            ipw.println("User restrictions for token " + token + ":");
-                            printedTokenHeader = true;
-                        }
-
-                        ipw.increaseIndent();
-                        if (!printedPackagesHeader) {
-                            ipw.println("Excluded packages:");
-                            printedPackagesHeader = true;
-                        }
-
-                        ipw.increaseIndent();
-                        ipw.print("user: ");
-                        ipw.print(userId);
-                        ipw.println(" packages: ");
-
-                        ipw.increaseIndent();
-                        packageNames.dump(ipw);
-
-                        ipw.decreaseIndent();
-                        ipw.decreaseIndent();
-                        ipw.decreaseIndent();
-                    }
-                    ipw.decreaseIndent();
-                }
-            }
+            boolean showUserRestrictions = !(dumpMode < 0 && !dumpWatchers && !dumpHistory);
+            mAppOpsRestrictions.dumpRestrictions(pw, dumpOp, dumpPackage, showUserRestrictions);
 
             if (!dumpHistory && !dumpWatchers) {
                 pw.println();
@@ -6085,8 +5971,6 @@
 
     private final class ClientUserRestrictionState implements DeathRecipient {
         private final IBinder token;
-        SparseArray<boolean[]> perUserRestrictions;
-        SparseArray<PackageTagsList> perUserExcludedPackageTags;
 
         ClientUserRestrictionState(IBinder token)
                 throws RemoteException {
@@ -6096,134 +5980,29 @@
 
         public boolean setRestriction(int code, boolean restricted,
                 PackageTagsList excludedPackageTags, int userId) {
-            boolean changed = false;
-
-            if (perUserRestrictions == null && restricted) {
-                perUserRestrictions = new SparseArray<>();
-            }
-
-            int[] users;
-            if (userId == UserHandle.USER_ALL) {
-                // TODO(b/162888972): this call is returning all users, not just live ones - we
-                // need to either fix the method called, or rename the variable
-                List<UserInfo> liveUsers = UserManager.get(mContext).getUsers();
-
-                users = new int[liveUsers.size()];
-                for (int i = 0; i < liveUsers.size(); i++) {
-                    users[i] = liveUsers.get(i).id;
-                }
-            } else {
-                users = new int[]{userId};
-            }
-
-            if (perUserRestrictions != null) {
-                int numUsers = users.length;
-
-                for (int i = 0; i < numUsers; i++) {
-                    int thisUserId = users[i];
-
-                    boolean[] userRestrictions = perUserRestrictions.get(thisUserId);
-                    if (userRestrictions == null && restricted) {
-                        userRestrictions = new boolean[AppOpsManager._NUM_OP];
-                        perUserRestrictions.put(thisUserId, userRestrictions);
-                    }
-                    if (userRestrictions != null && userRestrictions[code] != restricted) {
-                        userRestrictions[code] = restricted;
-                        if (!restricted && isDefault(userRestrictions)) {
-                            perUserRestrictions.remove(thisUserId);
-                            userRestrictions = null;
-                        }
-                        changed = true;
-                    }
-
-                    if (userRestrictions != null) {
-                        final boolean noExcludedPackages =
-                                excludedPackageTags == null || excludedPackageTags.isEmpty();
-                        if (perUserExcludedPackageTags == null && !noExcludedPackages) {
-                            perUserExcludedPackageTags = new SparseArray<>();
-                        }
-                        if (perUserExcludedPackageTags != null) {
-                            if (noExcludedPackages) {
-                                perUserExcludedPackageTags.remove(thisUserId);
-                                if (perUserExcludedPackageTags.size() <= 0) {
-                                    perUserExcludedPackageTags = null;
-                                }
-                            } else {
-                                perUserExcludedPackageTags.put(thisUserId, excludedPackageTags);
-                            }
-                            changed = true;
-                        }
-                    }
-                }
-            }
-
-            return changed;
+            return mAppOpsRestrictions.setUserRestriction(token, userId, code,
+                    restricted, excludedPackageTags);
         }
 
-        public boolean hasRestriction(int restriction, String packageName, String attributionTag,
+        public boolean hasRestriction(int code, String packageName, String attributionTag,
                 int userId, boolean isCheckOp) {
-            if (perUserRestrictions == null) {
-                return false;
-            }
-            boolean[] restrictions = perUserRestrictions.get(userId);
-            if (restrictions == null) {
-                return false;
-            }
-            if (!restrictions[restriction]) {
-                return false;
-            }
-            if (perUserExcludedPackageTags == null) {
-                return true;
-            }
-            PackageTagsList perUserExclusions = perUserExcludedPackageTags.get(userId);
-            if (perUserExclusions == null) {
-                return true;
-            }
-
-            // TODO (b/240617242) add overload for checkOp to support attribution tags
-            if (isCheckOp) {
-                return !perUserExclusions.includes(packageName);
-            }
-            return !perUserExclusions.contains(packageName, attributionTag);
+            return mAppOpsRestrictions.getUserRestriction(token, userId, code, packageName,
+                    attributionTag, isCheckOp);
         }
 
         public void removeUser(int userId) {
-            if (perUserExcludedPackageTags != null) {
-                perUserExcludedPackageTags.remove(userId);
-                if (perUserExcludedPackageTags.size() <= 0) {
-                    perUserExcludedPackageTags = null;
-                }
-            }
-            if (perUserRestrictions != null) {
-                perUserRestrictions.remove(userId);
-                if (perUserRestrictions.size() <= 0) {
-                    perUserRestrictions = null;
-                }
-            }
+            mAppOpsRestrictions.clearUserRestrictions(token, userId);
         }
 
         public boolean isDefault() {
-            return perUserRestrictions == null || perUserRestrictions.size() <= 0;
+            return !mAppOpsRestrictions.hasUserRestrictions(token);
         }
 
         @Override
         public void binderDied() {
             synchronized (AppOpsService.this) {
+                mAppOpsRestrictions.clearUserRestrictions(token);
                 mOpUserRestrictions.remove(token);
-                if (perUserRestrictions == null) {
-                    return;
-                }
-                final int userCount = perUserRestrictions.size();
-                for (int i = 0; i < userCount; i++) {
-                    final boolean[] restrictions = perUserRestrictions.valueAt(i);
-                    final int restrictionCount = restrictions.length;
-                    for (int j = 0; j < restrictionCount; j++) {
-                        if (restrictions[j]) {
-                            final int changedCode = j;
-                            mHandler.post(() -> notifyWatchersOfChange(changedCode, UID_ANY));
-                        }
-                    }
-                }
                 destroy();
             }
         }
@@ -6231,23 +6010,10 @@
         public void destroy() {
             token.unlinkToDeath(this, 0);
         }
-
-        private boolean isDefault(boolean[] array) {
-            if (ArrayUtils.isEmpty(array)) {
-                return true;
-            }
-            for (boolean value : array) {
-                if (value) {
-                    return false;
-                }
-            }
-            return true;
-        }
     }
 
     private final class ClientGlobalRestrictionState implements DeathRecipient {
         final IBinder mToken;
-        final ArraySet<Integer> mRestrictedOps = new ArraySet<>();
 
         ClientGlobalRestrictionState(IBinder token)
                 throws RemoteException {
@@ -6256,23 +6022,21 @@
         }
 
         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 c4a9a4b..18f659e 100644
--- a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java
+++ b/services/core/java/com/android/server/appop/AppOpsServiceInterface.java
@@ -148,6 +148,14 @@
     /**
      * 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.
      * @param changedListener the change listener.
      * @param op App-op whose mode has changed
      * @param uid user id associated with the app-op
@@ -198,5 +206,4 @@
      * @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 80266972..f6fff35 100644
--- a/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java
+++ b/services/core/java/com/android/server/appop/LegacyAppOpsServiceInterfaceImpl.java
@@ -333,6 +333,18 @@
     }
 
     @Override
+    public void notifyWatchersOfChange(int code, int uid) {
+        ArraySet<OnOpModeChangedListener> listenerSet = getOpModeChangedListeners(code);
+        if (listenerSet == null) {
+            return;
+        }
+        for (int i = 0; i < listenerSet.size(); i++) {
+            final OnOpModeChangedListener listener = listenerSet.valueAt(i);
+            notifyOpChanged(listener, code, uid, null);
+        }
+    }
+
+    @Override
     public void notifyOpChanged(@NonNull OnOpModeChangedListener onModeChangedListener, int code,
             int uid, @Nullable String packageName) {
         Objects.requireNonNull(onModeChangedListener);
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 0000000..5dc1251
--- /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);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
new file mode 100644
index 0000000..ad47773
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -0,0 +1,143 @@
+/*
+ * 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.policy;
+
+import static android.view.KeyEvent.KEYCODE_A;
+import static android.view.KeyEvent.KEYCODE_ALT_LEFT;
+import static android.view.KeyEvent.KEYCODE_B;
+import static android.view.KeyEvent.KEYCODE_C;
+import static android.view.KeyEvent.KEYCODE_CTRL_LEFT;
+import static android.view.KeyEvent.KEYCODE_E;
+import static android.view.KeyEvent.KEYCODE_L;
+import static android.view.KeyEvent.KEYCODE_M;
+import static android.view.KeyEvent.KEYCODE_META_LEFT;
+import static android.view.KeyEvent.KEYCODE_N;
+import static android.view.KeyEvent.KEYCODE_P;
+import static android.view.KeyEvent.KEYCODE_S;
+import static android.view.KeyEvent.KEYCODE_SLASH;
+import static android.view.KeyEvent.KEYCODE_SPACE;
+import static android.view.KeyEvent.KEYCODE_TAB;
+import static android.view.KeyEvent.KEYCODE_Z;
+
+import android.content.Intent;
+import android.os.RemoteException;
+import android.util.SparseArray;
+
+import org.junit.Test;
+
+public class ModifierShortcutTests extends ShortcutKeyTestBase {
+    private static final SparseArray<String> META_SHORTCUTS =  new SparseArray<>();
+    static {
+        META_SHORTCUTS.append(KEYCODE_A, Intent.CATEGORY_APP_CALCULATOR);
+        META_SHORTCUTS.append(KEYCODE_B, Intent.CATEGORY_APP_BROWSER);
+        META_SHORTCUTS.append(KEYCODE_C, Intent.CATEGORY_APP_CONTACTS);
+        META_SHORTCUTS.append(KEYCODE_E, Intent.CATEGORY_APP_EMAIL);
+        META_SHORTCUTS.append(KEYCODE_L, Intent.CATEGORY_APP_CALENDAR);
+        META_SHORTCUTS.append(KEYCODE_M, Intent.CATEGORY_APP_MAPS);
+        META_SHORTCUTS.append(KEYCODE_P, Intent.CATEGORY_APP_MUSIC);
+        META_SHORTCUTS.append(KEYCODE_S, Intent.CATEGORY_APP_MESSAGING);
+    }
+
+    /**
+     * Test meta+ shortcuts defined in bookmarks.xml.
+     */
+    @Test
+    public void testMetaShortcuts() {
+        for (int i = 0; i < META_SHORTCUTS.size(); i++) {
+            final int keyCode = META_SHORTCUTS.keyAt(i);
+            final String category = META_SHORTCUTS.valueAt(i);
+
+            sendKeyCombination(new int[]{KEYCODE_META_LEFT, keyCode}, 0);
+            mPhoneWindowManager.assertLaunchCategory(category);
+        }
+    }
+
+    /**
+     * ALT + TAB to show recent apps.
+     */
+    @Test
+    public void testAltTab() {
+        mPhoneWindowManager.overrideStatusBarManagerInternal();
+        sendKeyCombination(new int[]{KEYCODE_ALT_LEFT, KEYCODE_TAB}, 0);
+        mPhoneWindowManager.assertShowRecentApps();
+    }
+
+    /**
+     * CTRL + SPACE to switch keyboard layout.
+     */
+    @Test
+    public void testCtrlSpace() {
+        sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SPACE}, 0);
+        mPhoneWindowManager.assertSwitchKeyboardLayout();
+    }
+
+    /**
+     * META + SPACE to switch keyboard layout.
+     */
+    @Test
+    public void testMetaSpace() {
+        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_SPACE}, 0);
+        mPhoneWindowManager.assertSwitchKeyboardLayout();
+    }
+
+    /**
+     * CTRL + ALT + Z to enable accessibility service.
+     */
+    @Test
+    public void testCtrlAltZ() {
+        sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_ALT_LEFT, KEYCODE_Z}, 0);
+        mPhoneWindowManager.assertAccessibilityKeychordCalled();
+    }
+
+    /**
+     * META + CTRL+ S to take screenshot.
+     */
+    @Test
+    public void testMetaCtrlS() {
+        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_S}, 0);
+        mPhoneWindowManager.assertTakeScreenshotCalled();
+    }
+
+    /**
+     * META + N to expand notification panel.
+     */
+    @Test
+    public void testMetaN() throws RemoteException {
+        mPhoneWindowManager.overrideExpandNotificationsPanel();
+        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_N}, 0);
+        mPhoneWindowManager.assertExpandNotification();
+    }
+
+    /**
+     * META + SLASH to toggle shortcuts menu.
+     */
+    @Test
+    public void testMetaSlash() {
+        mPhoneWindowManager.overrideStatusBarManagerInternal();
+        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_SLASH}, 0);
+        mPhoneWindowManager.assertToggleShortcutsMenu();
+    }
+
+    /**
+     * META  + ALT to toggle Cap Lock.
+     */
+    @Test
+    public void testMetaAlt() {
+        sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ALT_LEFT}, 0);
+        mPhoneWindowManager.assertToggleCapsLock();
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index ee11ac8..fe46c14 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -32,6 +32,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT;
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS;
@@ -53,6 +54,7 @@
 import android.app.NotificationManager;
 import android.app.SearchManager;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayManagerInternal;
@@ -62,6 +64,7 @@
 import android.os.HandlerThread;
 import android.os.PowerManager;
 import android.os.PowerManagerInternal;
+import android.os.RemoteException;
 import android.os.Vibrator;
 import android.service.dreams.DreamManagerInternal;
 import android.telecom.TelecomManager;
@@ -73,12 +76,16 @@
 import com.android.internal.accessibility.AccessibilityShortcutController;
 import com.android.server.GestureLauncherService;
 import com.android.server.LocalServices;
+import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.vr.VrManagerInternal;
 import com.android.server.wm.ActivityTaskManagerInternal;
 import com.android.server.wm.DisplayPolicy;
 import com.android.server.wm.DisplayRotation;
 import com.android.server.wm.WindowManagerInternal;
 
+import junit.framework.Assert;
+
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockSettings;
 import org.mockito.Mockito;
@@ -118,6 +125,8 @@
     @Mock private GlobalActions mGlobalActions;
     @Mock private AccessibilityShortcutController mAccessibilityShortcutController;
 
+    @Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+
     private StaticMockitoSession mMockitoSession;
     private HandlerThread mHandlerThread;
     private Handler mHandler;
@@ -226,6 +235,8 @@
         mPhoneWindowManager.systemBooted();
 
         overrideLaunchAccessibility();
+        doReturn(false).when(mPhoneWindowManager).keyguardOn();
+        doNothing().when(mContext).startActivityAsUser(any(), any());
     }
 
     void tearDown() {
@@ -310,6 +321,22 @@
         doReturn(true).when(mTelecomManager).endCall();
     }
 
+    void overrideExpandNotificationsPanel() {
+        // Can't directly mock on IStatusbarService, use spyOn and override the specific api.
+        mPhoneWindowManager.getStatusBarService();
+        spyOn(mPhoneWindowManager.mStatusBarService);
+        try {
+            doNothing().when(mPhoneWindowManager.mStatusBarService).expandNotificationsPanel();
+        } catch (RemoteException e) {
+            e.printStackTrace();
+        }
+    }
+
+    void overrideStatusBarManagerInternal() {
+        doReturn(mStatusBarManagerInternal).when(
+                () -> LocalServices.getService(eq(StatusBarManagerInternal.class)));
+    }
+
     /**
      * Below functions will check the policy behavior could be invoked.
      */
@@ -368,4 +395,46 @@
         waitForIdle();
         verify(mSearchManager, timeout(SHORTCUT_KEY_DELAY_MILLIS)).launchAssist(any());
     }
+
+    void assertLaunchCategory(String category) {
+        waitForIdle();
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).startActivityAsUser(intentCaptor.capture(), any());
+        Assert.assertTrue(intentCaptor.getValue().getSelector().hasCategory(category));
+        // Reset verifier for next call.
+        Mockito.reset(mContext);
+    }
+
+    void assertShowRecentApps() {
+        waitForIdle();
+        verify(mStatusBarManagerInternal).showRecentApps(anyBoolean());
+    }
+
+    void assertSwitchKeyboardLayout() {
+        waitForIdle();
+        verify(mWindowManagerFuncsImpl).switchKeyboardLayout(anyInt(), anyInt());
+    }
+
+    void assertTakeBugreport() {
+        waitForIdle();
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).sendOrderedBroadcastAsUser(intentCaptor.capture(), any(), any(), any(),
+                any(), anyInt(), any(), any());
+        Assert.assertTrue(intentCaptor.getValue().getAction() == Intent.ACTION_BUG_REPORT);
+    }
+
+    void assertExpandNotification() throws RemoteException {
+        waitForIdle();
+        verify(mPhoneWindowManager.mStatusBarService).expandNotificationsPanel();
+    }
+
+    void assertToggleShortcutsMenu() {
+        waitForIdle();
+        verify(mStatusBarManagerInternal).toggleKeyboardShortcutsMenu(anyInt());
+    }
+
+    void assertToggleCapsLock() {
+        waitForIdle();
+        verify(mInputManagerInternal).toggleCapsLock(anyInt());
+    }
 }