Merge "Add credential manager FEATURE_ constant to Package Manager Test: Manual. Built locally Bug: 248609653"
diff --git a/ProtoLibraries.bp b/ProtoLibraries.bp
index c12f5b4..56d91b2 100644
--- a/ProtoLibraries.bp
+++ b/ProtoLibraries.bp
@@ -41,7 +41,6 @@
         ":libtombstone_proto-src",
         "core/proto/**/*.proto",
         "libs/incident/**/*.proto",
-        ":service-permission-streaming-proto-sources",
     ],
     output_extension: "srcjar",
 }
@@ -72,7 +71,6 @@
         ":libstats_atom_message_protos",
         "core/proto/**/*.proto",
         "libs/incident/**/*.proto",
-        ":service-permission-streaming-proto-sources",
     ],
 
     output_extension: "proto.h",
@@ -91,7 +89,6 @@
         "cmds/statsd/src/**/*.proto",
         "core/proto/**/*.proto",
         "libs/incident/proto/**/*.proto",
-        ":service-permission-streaming-proto-sources",
     ],
     proto: {
         include_dirs: [
@@ -126,7 +123,6 @@
         ":libstats_atom_message_protos",
         "core/proto/**/*.proto",
         "libs/incident/proto/android/os/**/*.proto",
-        ":service-permission-streaming-proto-sources",
     ],
     // Protos have lots of MissingOverride and similar.
     errorprone: {
@@ -148,7 +144,6 @@
         ":libstats_atom_message_protos",
         "core/proto/**/*.proto",
         "libs/incident/proto/android/os/**/*.proto",
-        ":service-permission-streaming-proto-sources",
     ],
     exclude_srcs: [
         "core/proto/android/privacy.proto",
@@ -184,7 +179,6 @@
         ":libstats_atom_enum_protos",
         ":libstats_atom_message_protos",
         "core/proto/**/*.proto",
-        ":service-permission-streaming-proto-sources",
     ],
 }
 
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 5b61f9a..22f70ce 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -57,6 +57,8 @@
 
 import android.Manifest;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.Activity;
 import android.app.ActivityManagerInternal;
@@ -289,6 +291,7 @@
     final DeliveryTracker mDeliveryTracker = new DeliveryTracker();
     IBinder.DeathRecipient mListenerDeathRecipient;
     Intent mTimeTickIntent;
+    Bundle mTimeTickOptions;
     IAlarmListener mTimeTickTrigger;
     PendingIntent mDateChangeSender;
     boolean mInteractive = true;
@@ -1909,7 +1912,9 @@
                     Intent.FLAG_RECEIVER_REGISTERED_ONLY
                             | Intent.FLAG_RECEIVER_FOREGROUND
                             | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
-
+            mTimeTickOptions = BroadcastOptions
+                    .makeRemovingMatchingFilter(new IntentFilter(Intent.ACTION_TIME_TICK))
+                    .toBundle();
             mTimeTickTrigger = new IAlarmListener.Stub() {
                 @Override
                 public void doAlarm(final IAlarmCompleteListener callback) throws RemoteException {
@@ -1921,8 +1926,8 @@
                     // takes care of this automatically, but we're using the direct internal
                     // interface here rather than that client-side wrapper infrastructure.
                     mHandler.post(() -> {
-                        getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL);
-
+                        getContext().sendBroadcastAsUser(mTimeTickIntent, UserHandle.ALL, null,
+                                mTimeTickOptions);
                         try {
                             callback.alarmComplete(this);
                         } catch (RemoteException e) { /* local method call */ }
diff --git a/core/api/current.txt b/core/api/current.txt
index 83ada14..65c1f46 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -45195,6 +45195,7 @@
     method public final int getLineAscent(int);
     method public final int getLineBaseline(int);
     method public final int getLineBottom(int);
+    method public int getLineBottom(int, boolean);
     method public int getLineBounds(int, android.graphics.Rect);
     method public abstract boolean getLineContainsTab(int);
     method public abstract int getLineCount();
@@ -53317,6 +53318,7 @@
     method public default void performHandwritingGesture(@NonNull android.view.inputmethod.HandwritingGesture, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.IntConsumer);
     method public boolean performPrivateCommand(String, android.os.Bundle);
     method public default boolean performSpellCheck();
+    method public default boolean replaceText(@IntRange(from=0) int, @IntRange(from=0) int, @NonNull CharSequence, int, @Nullable android.view.inputmethod.TextAttribute);
     method public boolean reportFullscreenMode(boolean);
     method public boolean requestCursorUpdates(int);
     method public default boolean requestCursorUpdates(int, int);
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index dfef279..8ae16df6 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -4667,7 +4667,7 @@
      * @hide
      */
     public static void broadcastStickyIntent(Intent intent, int userId) {
-        broadcastStickyIntent(intent, AppOpsManager.OP_NONE, userId);
+        broadcastStickyIntent(intent, AppOpsManager.OP_NONE, null, userId);
     }
 
     /**
@@ -4676,11 +4676,20 @@
      * @hide
      */
     public static void broadcastStickyIntent(Intent intent, int appOp, int userId) {
+        broadcastStickyIntent(intent, appOp, null, userId);
+    }
+
+    /**
+     * Convenience for sending a sticky broadcast.  For internal use only.
+     *
+     * @hide
+     */
+    public static void broadcastStickyIntent(Intent intent, int appOp, Bundle options, int userId) {
         try {
             getService().broadcastIntentWithFeature(
                     null, null, intent, null, null, Activity.RESULT_OK, null, null,
                     null /*requiredPermissions*/, null /*excludedPermissions*/,
-                    null /*excludedPackages*/, appOp, null, false, true, userId);
+                    null /*excludedPackages*/, appOp, options, false, true, userId);
         } catch (RemoteException ex) {
         }
     }
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 419b8e1..626b7d3 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -30,6 +30,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.pm.ActivityPresentationInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PermissionMethod;
 import android.content.pm.UserInfo;
 import android.net.Uri;
 import android.os.Bundle;
@@ -292,6 +293,7 @@
             boolean allowAll, int allowMode, String name, String callerPackage);
 
     /** Checks if the calling binder pid as the permission. */
+    @PermissionMethod
     public abstract void enforceCallingPermission(String permission, String func);
 
     /** Returns the current user id. */
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index aa5fa5b..c2df802 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -27,12 +27,15 @@
 import android.compat.annotation.Disabled;
 import android.compat.annotation.EnabledSince;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.PowerExemptionManager;
 import android.os.PowerExemptionManager.ReasonCode;
 import android.os.PowerExemptionManager.TempAllowListType;
 
+import java.util.Objects;
+
 /**
  * Helper class for building an options Bundle that can be used with
  * {@link android.content.Context#sendBroadcast(android.content.Intent)
@@ -55,6 +58,7 @@
     private boolean mRequireCompatChangeEnabled = true;
     private boolean mIsAlarmBroadcast = false;
     private long mIdForResponseEvent;
+    private @Nullable IntentFilter mRemoveMatchingFilter;
 
     /**
      * Change ID which is invalid.
@@ -180,11 +184,25 @@
     private static final String KEY_ID_FOR_RESPONSE_EVENT =
             "android:broadcast.idForResponseEvent";
 
+    /**
+     * Corresponds to {@link #setRemoveMatchingFilter}.
+     */
+    private static final String KEY_REMOVE_MATCHING_FILTER =
+            "android:broadcast.removeMatchingFilter";
+
     public static BroadcastOptions makeBasic() {
         BroadcastOptions opts = new BroadcastOptions();
         return opts;
     }
 
+    /** {@hide} */
+    public static @NonNull BroadcastOptions makeRemovingMatchingFilter(
+            @NonNull IntentFilter removeMatchingFilter) {
+        BroadcastOptions opts = new BroadcastOptions();
+        opts.setRemoveMatchingFilter(removeMatchingFilter);
+        return opts;
+    }
+
     private BroadcastOptions() {
         super();
         resetTemporaryAppAllowlist();
@@ -216,6 +234,8 @@
         mRequireCompatChangeEnabled = opts.getBoolean(KEY_REQUIRE_COMPAT_CHANGE_ENABLED, true);
         mIdForResponseEvent = opts.getLong(KEY_ID_FOR_RESPONSE_EVENT);
         mIsAlarmBroadcast = opts.getBoolean(KEY_ALARM_BROADCAST, false);
+        mRemoveMatchingFilter = opts.getParcelable(KEY_REMOVE_MATCHING_FILTER,
+                IntentFilter.class);
     }
 
     /**
@@ -596,6 +616,29 @@
     }
 
     /**
+     * When enqueuing this broadcast, remove all pending broadcasts previously
+     * sent by this app which match the given filter.
+     * <p>
+     * For example, sending {@link Intent#ACTION_SCREEN_ON} would typically want
+     * to remove any pending {@link Intent#ACTION_SCREEN_OFF} broadcasts.
+     *
+     * @hide
+     */
+    public void setRemoveMatchingFilter(@NonNull IntentFilter removeMatchingFilter) {
+        mRemoveMatchingFilter = Objects.requireNonNull(removeMatchingFilter);
+    }
+
+    /** @hide */
+    public void clearRemoveMatchingFilter() {
+        mRemoveMatchingFilter = null;
+    }
+
+    /** @hide */
+    public @Nullable IntentFilter getRemoveMatchingFilter() {
+        return mRemoveMatchingFilter;
+    }
+
+    /**
      * Returns the created options as a Bundle, which can be passed to
      * {@link android.content.Context#sendBroadcast(android.content.Intent)
      * Context.sendBroadcast(Intent)} and related methods.
@@ -640,6 +683,9 @@
         if (mIdForResponseEvent != 0) {
             b.putLong(KEY_ID_FOR_RESPONSE_EVENT, mIdForResponseEvent);
         }
+        if (mRemoveMatchingFilter != null) {
+            b.putParcelable(KEY_REMOVE_MATCHING_FILTER, mRemoveMatchingFilter);
+        }
         return b.isEmpty() ? null : b;
     }
 }
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 97da2da..430b52c 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -51,6 +51,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.pm.PermissionMethod;
 import android.content.res.AssetManager;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
@@ -6066,6 +6067,7 @@
      */
     @CheckResult(suggest="#enforcePermission(String,int,int,String)")
     @PackageManager.PermissionResult
+    @PermissionMethod
     public abstract int checkPermission(@NonNull String permission, int pid, int uid);
 
     /** @hide */
@@ -6098,6 +6100,7 @@
      */
     @CheckResult(suggest="#enforceCallingPermission(String,String)")
     @PackageManager.PermissionResult
+    @PermissionMethod
     public abstract int checkCallingPermission(@NonNull String permission);
 
     /**
@@ -6118,6 +6121,7 @@
      */
     @CheckResult(suggest="#enforceCallingOrSelfPermission(String,String)")
     @PackageManager.PermissionResult
+    @PermissionMethod
     public abstract int checkCallingOrSelfPermission(@NonNull String permission);
 
     /**
@@ -6146,6 +6150,7 @@
      *
      * @see #checkPermission(String, int, int)
      */
+    @PermissionMethod
     public abstract void enforcePermission(
             @NonNull String permission, int pid, int uid, @Nullable String message);
 
@@ -6167,6 +6172,7 @@
      *
      * @see #checkCallingPermission(String)
      */
+    @PermissionMethod
     public abstract void enforceCallingPermission(
             @NonNull String permission, @Nullable String message);
 
@@ -6183,6 +6189,7 @@
      *
      * @see #checkCallingOrSelfPermission(String)
      */
+    @PermissionMethod
     public abstract void enforceCallingOrSelfPermission(
             @NonNull String permission, @Nullable String message);
 
diff --git a/core/java/android/content/pm/PermissionMethod.java b/core/java/android/content/pm/PermissionMethod.java
new file mode 100644
index 0000000..021b2e1
--- /dev/null
+++ b/core/java/android/content/pm/PermissionMethod.java
@@ -0,0 +1,36 @@
+/*
+ * 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 android.content.pm;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Documents that the subject method's job is to look
+ * up whether the provided or calling uid/pid has the requested permission.
+ *
+ * Methods should either return `void`, but potentially throw {@link SecurityException},
+ * or return {@link android.content.pm.PackageManager.PermissionResult} `int`.
+ *
+ * @hide
+ */
+@Retention(CLASS)
+@Target({METHOD})
+public @interface PermissionMethod {}
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/IRemoteInputConnectionInvoker.java b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
index 6f758de..891da24 100644
--- a/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
+++ b/core/java/android/inputmethodservice/IRemoteInputConnectionInvoker.java
@@ -775,4 +775,33 @@
             return false;
         }
     }
+
+    /**
+     * Invokes {@link IRemoteInputConnection#replaceText(InputConnectionCommandHeader, int, int,
+     * CharSequence, TextAttribute)}.
+     *
+     * @param start the character index where the replacement should start.
+     * @param end the character index where the replacement should end.
+     * @param newCursorPosition the new cursor position around the text. If > 0, this is relative to
+     *     the end of the text - 1; if <= 0, this is relative to the start of the text. So a value
+     *     of 1 will always advance you to the position after the full text being inserted. Note
+     *     that this means you can't position the cursor within the text.
+     * @param text the text to replace. This may include styles.
+     * @param textAttribute The extra information about the text. This value may be null.
+     */
+    @AnyThread
+    public boolean replaceText(
+            int start,
+            int end,
+            @NonNull CharSequence text,
+            int newCursorPosition,
+            @Nullable TextAttribute textAttribute) {
+        try {
+            mConnection.replaceText(
+                    createHeader(), start, end, text, newCursorPosition, textAttribute);
+            return true;
+        } catch (RemoteException e) {
+            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/core/java/android/inputmethodservice/RemoteInputConnection.java b/core/java/android/inputmethodservice/RemoteInputConnection.java
index 694293c..2b5f14d 100644
--- a/core/java/android/inputmethodservice/RemoteInputConnection.java
+++ b/core/java/android/inputmethodservice/RemoteInputConnection.java
@@ -498,6 +498,17 @@
         return mInvoker.setImeConsumesInput(imeConsumesInput);
     }
 
+    /** See {@link InputConnection#replaceText(int, int, CharSequence, int, TextAttribute)}. */
+    @AnyThread
+    public boolean replaceText(
+            int start,
+            int end,
+            @NonNull CharSequence text,
+            int newCursorPosition,
+            @Nullable TextAttribute textAttribute) {
+        return mInvoker.replaceText(start, end, text, newCursorPosition, textAttribute);
+    }
+
     @AnyThread
     @Override
     public String toString() {
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 5c809a1..607d1e1 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -2869,10 +2869,10 @@
      * It includes:
      *
      * <ol>
-     *   <li>The current foreground user in the main display.
-     *   <li>Current background users in secondary displays (for example, passenger users on
-     *   automotive, using the display associated with their seats).
-     *   <li>Profile users (in the running / started state) of other visible users.
+     *   <li>The current foreground user.
+     *   <li>(Running) profiles of the current foreground user.
+     *   <li>Background users assigned to secondary displays (for example, passenger users on
+     *   automotive builds, using the display associated with their seats).
      * </ol>
      *
      * @return whether the user is visible at the moment, as defined above.
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index b5f7c54..dbb41f4 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -1841,7 +1841,7 @@
         // Find the first line whose vertical center is below the top of the area.
         int startLine = getLineForVertical((int) area.top);
         int startLineTop = getLineTop(startLine);
-        int startLineBottom = getLineBottomWithoutSpacing(startLine);
+        int startLineBottom = getLineBottom(startLine, /* includeLineSpacing= */ false);
         if (area.top > (startLineTop + startLineBottom) / 2f) {
             startLine++;
             if (startLine >= getLineCount()) {
@@ -1854,7 +1854,7 @@
         // Find the last line whose vertical center is above the bottom of the area.
         int endLine = getLineForVertical((int) area.bottom);
         int endLineTop = getLineTop(endLine);
-        int endLineBottom = getLineBottomWithoutSpacing(endLine);
+        int endLineBottom = getLineBottom(endLine, /* includeLineSpacing= */ false);
         if (area.bottom < (endLineTop + endLineBottom) / 2f) {
             endLine--;
         }
@@ -2229,17 +2229,21 @@
      * Return the vertical position of the bottom of the specified line.
      */
     public final int getLineBottom(int line) {
-        return getLineTop(line + 1);
+        return getLineBottom(line, /* includeLineSpacing= */ true);
     }
 
     /**
-     * Return the vertical position of the bottom of the specified line without the line spacing
-     * added.
+     * Return the vertical position of the bottom of the specified line.
      *
-     * @hide
+     * @param line index of the line
+     * @param includeLineSpacing whether to include the line spacing
      */
-    public final int getLineBottomWithoutSpacing(int line) {
-        return getLineTop(line + 1) - getLineExtra(line);
+    public int getLineBottom(int line, boolean includeLineSpacing) {
+        if (includeLineSpacing) {
+            return getLineTop(line + 1);
+        } else {
+            return getLineTop(line + 1) - getLineExtra(line);
+        }
     }
 
     /**
@@ -2394,7 +2398,7 @@
 
         int line = getLineForOffset(point);
         int top = getLineTop(line);
-        int bottom = getLineBottomWithoutSpacing(line);
+        int bottom = getLineBottom(line, /* includeLineSpacing= */ false);
 
         boolean clamped = shouldClampCursor(line);
         float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
@@ -2530,7 +2534,7 @@
         final int endline = getLineForOffset(end);
 
         int top = getLineTop(startline);
-        int bottom = getLineBottomWithoutSpacing(endline);
+        int bottom = getLineBottom(endline, /* includeLineSpacing= */ false);
 
         if (startline == endline) {
             addSelection(startline, start, end, top, bottom, consumer);
@@ -2559,7 +2563,7 @@
             }
 
             top = getLineTop(endline);
-            bottom = getLineBottomWithoutSpacing(endline);
+            bottom = getLineBottom(endline, /* includeLineSpacing= */ false);
 
             addSelection(endline, getLineStart(endline), end, top, bottom, consumer);
 
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 51e3665..596e491 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -93,7 +93,9 @@
     private static final String ELLIPSIS_NORMAL = "\u2026"; // HORIZONTAL ELLIPSIS (…)
     private static final String ELLIPSIS_TWO_DOTS = "\u2025"; // TWO DOT LEADER (‥)
 
-    private static final int LINE_FEED_CODE_POINT = 10;
+    /** @hide */
+    public static final int LINE_FEED_CODE_POINT = 10;
+
     private static final int NBSP_CODE_POINT = 160;
 
     /**
@@ -2335,11 +2337,29 @@
                 || codePoint == LINE_FEED_CODE_POINT;
     }
 
-    private static boolean isWhiteSpace(int codePoint) {
+    /** @hide */
+    public static boolean isWhitespace(int codePoint) {
         return Character.isWhitespace(codePoint) || codePoint == NBSP_CODE_POINT;
     }
 
     /** @hide */
+    public static boolean isWhitespaceExceptNewline(int codePoint) {
+        return isWhitespace(codePoint) && !isNewline(codePoint);
+    }
+
+    /** @hide */
+    public static boolean isPunctuation(int codePoint) {
+        int type = Character.getType(codePoint);
+        return type == Character.CONNECTOR_PUNCTUATION
+                || type == Character.DASH_PUNCTUATION
+                || type == Character.END_PUNCTUATION
+                || type == Character.FINAL_QUOTE_PUNCTUATION
+                || type == Character.INITIAL_QUOTE_PUNCTUATION
+                || type == Character.OTHER_PUNCTUATION
+                || type == Character.START_PUNCTUATION;
+    }
+
+    /** @hide */
     @Nullable
     public static String withoutPrefix(@Nullable String prefix, @Nullable String str) {
         if (prefix == null || str == null) return str;
@@ -2430,7 +2450,7 @@
                 gettingCleaned.removeRange(offset, offset + codePointLen);
             } else if (type == Character.CONTROL && !isNewline) {
                 gettingCleaned.removeRange(offset, offset + codePointLen);
-            } else if (trim && !isWhiteSpace(codePoint)) {
+            } else if (trim && !isWhitespace(codePoint)) {
                 // This is only executed if the code point is not removed
                 if (firstNonWhiteSpace == -1) {
                     firstNonWhiteSpace = offset;
diff --git a/core/java/android/text/method/WordIterator.java b/core/java/android/text/method/WordIterator.java
index f427e1b..6d18d2c 100644
--- a/core/java/android/text/method/WordIterator.java
+++ b/core/java/android/text/method/WordIterator.java
@@ -24,6 +24,7 @@
 import android.os.Build;
 import android.text.CharSequenceCharacterIterator;
 import android.text.Selection;
+import android.text.TextUtils;
 
 import java.util.Locale;
 
@@ -275,9 +276,9 @@
     }
 
     /**
-     * If <code>offset</code> is within a group of punctuation as defined
-     * by {@link #isPunctuation(int)}, returns the index of the first character
-     * of that group, otherwise returns BreakIterator.DONE.
+     * If <code>offset</code> is within a group of punctuation as defined by {@link
+     * TextUtils#isPunctuation(int)}, returns the index of the first character of that group,
+     * otherwise returns BreakIterator.DONE.
      *
      * @param offset the offset to search from.
      */
@@ -292,9 +293,9 @@
     }
 
     /**
-     * If <code>offset</code> is within a group of punctuation as defined
-     * by {@link #isPunctuation(int)}, returns the index of the last character
-     * of that group plus one, otherwise returns BreakIterator.DONE.
+     * If <code>offset</code> is within a group of punctuation as defined by {@link
+     * TextUtils#isPunctuation(int)}, returns the index of the last character of that group plus
+     * one, otherwise returns BreakIterator.DONE.
      *
      * @param offset the offset to search from.
      */
@@ -309,8 +310,8 @@
     }
 
     /**
-     * Indicates if the provided offset is after a punctuation character
-     * as defined by {@link #isPunctuation(int)}.
+     * Indicates if the provided offset is after a punctuation character as defined by {@link
+     * TextUtils#isPunctuation(int)}.
      *
      * @param offset the offset to check from.
      * @return Whether the offset is after a punctuation character.
@@ -319,14 +320,14 @@
     public boolean isAfterPunctuation(int offset) {
         if (mStart < offset && offset <= mEnd) {
             final int codePoint = Character.codePointBefore(mCharSeq, offset);
-            return isPunctuation(codePoint);
+            return TextUtils.isPunctuation(codePoint);
         }
         return false;
     }
 
     /**
-     * Indicates if the provided offset is at a punctuation character
-     * as defined by {@link #isPunctuation(int)}.
+     * Indicates if the provided offset is at a punctuation character as defined by {@link
+     * TextUtils#isPunctuation(int)}.
      *
      * @param offset the offset to check from.
      * @return Whether the offset is at a punctuation character.
@@ -335,7 +336,7 @@
     public boolean isOnPunctuation(int offset) {
         if (mStart <= offset && offset < mEnd) {
             final int codePoint = Character.codePointAt(mCharSeq, offset);
-            return isPunctuation(codePoint);
+            return TextUtils.isPunctuation(codePoint);
         }
         return false;
     }
@@ -369,17 +370,6 @@
         return !isOnPunctuation(offset) && isAfterPunctuation(offset);
     }
 
-    private static boolean isPunctuation(int cp) {
-        final int type = Character.getType(cp);
-        return (type == Character.CONNECTOR_PUNCTUATION
-                || type == Character.DASH_PUNCTUATION
-                || type == Character.END_PUNCTUATION
-                || type == Character.FINAL_QUOTE_PUNCTUATION
-                || type == Character.INITIAL_QUOTE_PUNCTUATION
-                || type == Character.OTHER_PUNCTUATION
-                || type == Character.START_PUNCTUATION);
-    }
-
     private boolean isAfterLetterOrDigit(int offset) {
         if (mStart < offset && offset <= mEnd) {
             final int codePoint = Character.codePointBefore(mCharSeq, offset);
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index 5832527..c8c941a 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -22,6 +22,7 @@
 import static android.view.InsetsSourceProto.VISIBLE_FRAME;
 import static android.view.InsetsState.ITYPE_CAPTION_BAR;
 import static android.view.InsetsState.ITYPE_IME;
+import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -148,7 +149,7 @@
         // During drag-move and drag-resizing, the caption insets position may not get updated
         // before the app frame get updated. To layout the app content correctly during drag events,
         // we always return the insets with the corresponding height covering the top.
-        if (getType() == ITYPE_CAPTION_BAR) {
+        if (!CAPTION_ON_SHELL && getType() == ITYPE_CAPTION_BAR) {
             return Insets.of(0, frame.height(), 0, 0);
         }
         // Checks for whether there is shared edge with insets for 0-width/height window.
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index 90384b5..c32ca9e 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -528,7 +528,12 @@
     @Override
     @UiThread
     void flush(@FlushReason int reason) {
-        if (mEvents == null) return;
+        if (mEvents == null || mEvents.size() == 0) {
+            if (sVerbose) {
+                Log.v(TAG, "Don't flush for empty event buffer.");
+            }
+            return;
+        }
 
         if (mDisabled.get()) {
             Log.e(TAG, "handleForceFlush(" + getDebugState(reason) + "): should not be when "
diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java
index a72f0d5..3733c3f 100644
--- a/core/java/android/view/inputmethod/BaseInputConnection.java
+++ b/core/java/android/view/inputmethod/BaseInputConnection.java
@@ -897,8 +897,43 @@
         }
     }
 
-    private void replaceText(CharSequence text, int newCursorPosition,
-            boolean composing) {
+    @Override
+    public boolean replaceText(
+            @IntRange(from = 0) int start,
+            @IntRange(from = 0) int end,
+            @NonNull CharSequence text,
+            int newCursorPosition,
+            @Nullable TextAttribute textAttribute) {
+        Preconditions.checkArgumentNonnegative(start);
+        Preconditions.checkArgumentNonnegative(end);
+
+        if (DEBUG) {
+            Log.v(
+                    TAG,
+                    "replaceText " + start + ", " + end + ", " + text + ", " + newCursorPosition);
+        }
+
+        final Editable content = getEditable();
+        if (content == null) {
+            return false;
+        }
+        beginBatchEdit();
+        removeComposingSpans(content);
+
+        int len = content.length();
+        start = Math.min(start, len);
+        end = Math.min(end, len);
+        if (end < start) {
+            int tmp = start;
+            start = end;
+            end = tmp;
+        }
+        replaceTextInternal(start, end, text, newCursorPosition, /*composing=*/ false);
+        endBatchEdit();
+        return true;
+    }
+
+    private void replaceText(CharSequence text, int newCursorPosition, boolean composing) {
         final Editable content = getEditable();
         if (content == null) {
             return;
@@ -931,6 +966,16 @@
                 b = tmp;
             }
         }
+        replaceTextInternal(a, b, text, newCursorPosition, composing);
+        endBatchEdit();
+    }
+
+    private void replaceTextInternal(
+            int a, int b, CharSequence text, int newCursorPosition, boolean composing) {
+        final Editable content = getEditable();
+        if (content == null) {
+            return;
+        }
 
         if (composing) {
             Spannable sp = null;
@@ -974,7 +1019,6 @@
         if (newCursorPosition < 0) newCursorPosition = 0;
         if (newCursorPosition > content.length()) newCursorPosition = content.length();
         Selection.setSelection(content, newCursorPosition);
-
         content.replace(a, b, text);
 
         if (DEBUG) {
@@ -982,8 +1026,6 @@
             lp.println("Final text:");
             TextUtils.dumpSpans(content, lp, "  ");
         }
-
-        endBatchEdit();
     }
 
     /**
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index 2f834c9..7d268a9 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -1329,4 +1329,44 @@
         // existing APIs.
         return null;
     }
+
+    /**
+     * Replace the specific range in the editor with suggested text.
+     *
+     * <p>This method finishes whatever composing text is currently active and leaves the text
+     * as-it, replaces the specific range of text with the passed CharSequence, and then moves the
+     * cursor according to {@code newCursorPosition}. This behaves like calling {@link
+     * #finishComposingText()}, {@link #setSelection(int, int) setSelection(start, end)}, and then
+     * {@link #commitText(CharSequence, int, TextAttribute) commitText(text, newCursorPosition,
+     * textAttribute)}.
+     *
+     * <p>Similar to {@link #setSelection(int, int)}, the order of start and end is not important.
+     * In effect, the region from start to end and the region from end to start is the same. Editor
+     * authors, be ready to accept a start that is greater than end.
+     *
+     * @param start the character index where the replacement should start.
+     * @param end the character index where the replacement should end.
+     * @param newCursorPosition the new cursor position around the text. If > 0, this is relative to
+     *     the end of the text - 1; if <= 0, this is relative to the start of the text. So a value
+     *     of 1 will always advance you to the position after the full text being inserted. Note
+     *     that this means you can't position the cursor within the text.
+     * @param text the text to replace. This may include styles.
+     * @param textAttribute The extra information about the text. This value may be null.
+     */
+    default boolean replaceText(
+            @IntRange(from = 0) int start,
+            @IntRange(from = 0) int end,
+            @NonNull CharSequence text,
+            int newCursorPosition,
+            @Nullable TextAttribute textAttribute) {
+        Preconditions.checkArgumentNonnegative(start);
+        Preconditions.checkArgumentNonnegative(end);
+
+        beginBatchEdit();
+        finishComposingText();
+        setSelection(start, end);
+        commitText(text, newCursorPosition, textAttribute);
+        endBatchEdit();
+        return true;
+    }
 }
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 424b8ae..8f590f8 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -572,8 +572,8 @@
 
         final Layout layout = mTextView.getLayout();
         final int line = layout.getLineForOffset(mTextView.getSelectionStart());
-        final int sourceHeight =
-            layout.getLineBottomWithoutSpacing(line) - layout.getLineTop(line);
+        final int sourceHeight = layout.getLineBottom(line, /* includeLineSpacing= */ false)
+                - layout.getLineTop(line);
         final int height = (int)(sourceHeight * zoom);
         final int width = (int)(aspectRatio * Math.max(sourceHeight, mMinLineHeightForMagnifier));
 
@@ -2340,7 +2340,7 @@
         final int offset = mTextView.getSelectionStart();
         final int line = layout.getLineForOffset(offset);
         final int top = layout.getLineTop(line);
-        final int bottom = layout.getLineBottomWithoutSpacing(line);
+        final int bottom = layout.getLineBottom(line, /* includeLineSpacing= */ false);
 
         final boolean clamped = layout.shouldClampCursor(line);
         updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
@@ -3443,7 +3443,7 @@
         @Override
         protected int getVerticalLocalPosition(int line) {
             final Layout layout = mTextView.getLayout();
-            return layout.getLineBottomWithoutSpacing(line);
+            return layout.getLineBottom(line, /* includeLineSpacing= */ false);
         }
 
         @Override
@@ -4109,7 +4109,8 @@
         @Override
         protected int getVerticalLocalPosition(int line) {
             final Layout layout = mTextView.getLayout();
-            return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
+            return layout.getLineBottom(line, /* includeLineSpacing= */ false)
+                    - mContainerMarginTop;
         }
 
         @Override
@@ -4706,8 +4707,9 @@
                                 + viewportToContentVerticalOffset;
                         final float insertionMarkerBaseline = layout.getLineBaseline(line)
                                 + viewportToContentVerticalOffset;
-                        final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
-                                + viewportToContentVerticalOffset;
+                        final float insertionMarkerBottom =
+                                layout.getLineBottom(line, /* includeLineSpacing= */ false)
+                                        + viewportToContentVerticalOffset;
                         final boolean isTopVisible = mTextView
                                 .isPositionVisible(insertionMarkerX, insertionMarkerTop);
                         final boolean isBottomVisible = mTextView
@@ -5137,7 +5139,7 @@
 
                 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
                         - getHorizontalOffset() + getCursorOffset();
-                mPositionY = layout.getLineBottomWithoutSpacing(line);
+                mPositionY = layout.getLineBottom(line, /* includeLineSpacing= */ false);
 
                 // Take TextView's padding and scroll into account.
                 mPositionX += mTextView.viewportToContentHorizontalOffset();
@@ -5233,8 +5235,8 @@
             if (mNewMagnifierEnabled) {
                 Layout layout = mTextView.getLayout();
                 final int line = layout.getLineForOffset(getCurrentCursorOffset());
-                return layout.getLineBottomWithoutSpacing(line) - layout.getLineTop(line)
-                        >= mMaxLineHeightForMagnifier;
+                return layout.getLineBottom(line, /* includeLineSpacing= */ false)
+                        - layout.getLineTop(line) >= mMaxLineHeightForMagnifier;
             }
             final float magnifierContentHeight = Math.round(
                     mMagnifierAnimator.mMagnifier.getHeight()
@@ -5389,7 +5391,8 @@
 
             // Vertically snap to middle of current line.
             showPosInView.y = ((mTextView.getLayout().getLineTop(lineNumber)
-                    + mTextView.getLayout().getLineBottomWithoutSpacing(lineNumber)) / 2.0f
+                    + mTextView.getLayout()
+                            .getLineBottom(lineNumber, /* includeLineSpacing= */ false)) / 2.0f
                     + mTextView.getTotalPaddingTop() - mTextView.getScrollY()) * mTextViewScaleY;
             return true;
         }
@@ -5473,7 +5476,8 @@
                         updateCursorPosition();
                     }
                     final int lineHeight =
-                            layout.getLineBottomWithoutSpacing(line) - layout.getLineTop(line);
+                            layout.getLineBottom(line, /* includeLineSpacing= */ false)
+                                    - layout.getLineTop(line);
                     float zoom = mInitialZoom;
                     if (lineHeight < mMinLineHeightForMagnifier) {
                         zoom = zoom * mMinLineHeightForMagnifier / lineHeight;
@@ -5823,8 +5827,8 @@
         private MotionEvent transformEventForTouchThrough(MotionEvent ev) {
             final Layout layout = mTextView.getLayout();
             final int line = layout.getLineForOffset(getCurrentCursorOffset());
-            final int textHeight =
-                    layout.getLineBottomWithoutSpacing(line) - layout.getLineTop(line);
+            final int textHeight = layout.getLineBottom(line, /* includeLineSpacing= */ false)
+                    - layout.getLineTop(line);
             // Transforms the touch events to screen coordinates.
             // And also shift up to make the hit point is on the text.
             // Note:
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 41d00a2..752d02c 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -9330,9 +9330,58 @@
         if (range == null) {
             return handleGestureFailure(gesture);
         }
-        getEditableText().delete(range[0], range[1]);
-        Selection.setSelection(getEditableText(), range[0]);
-        // TODO(b/243983058): Delete extra spaces.
+        int start = range[0];
+        int end = range[1];
+
+        // For word granularity, adjust the start and end offsets to remove extra whitespace around
+        // the deleted text.
+        if (gesture.getGranularity() == HandwritingGesture.GRANULARITY_WORD) {
+            // If the deleted text is at the start of the text, the behavior is the same as the case
+            // where the deleted text follows a new line character.
+            int codePointBeforeStart = start > 0
+                    ? Character.codePointBefore(mText, start) : TextUtils.LINE_FEED_CODE_POINT;
+            // If the deleted text is at the end of the text, the behavior is the same as the case
+            // where the deleted text precedes a new line character.
+            int codePointAtEnd = end < mText.length()
+                    ? Character.codePointAt(mText, end) : TextUtils.LINE_FEED_CODE_POINT;
+            if (TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)
+                    && (TextUtils.isWhitespace(codePointAtEnd)
+                            || TextUtils.isPunctuation(codePointAtEnd))) {
+                // Remove whitespace (except new lines) before the deleted text, in these cases:
+                // - There is whitespace following the deleted text
+                //     e.g. "one [deleted] three" -> "one | three" -> "one| three"
+                // - There is punctuation following the deleted text
+                //     e.g. "one [deleted]!" -> "one |!" -> "one|!"
+                // - There is a new line following the deleted text
+                //     e.g. "one [deleted]\n" -> "one |\n" -> "one|\n"
+                // - The deleted text is at the end of the text
+                //     e.g. "one [deleted]" -> "one |" -> "one|"
+                // (The pipe | indicates the cursor position.)
+                while (start > 0 && TextUtils.isWhitespaceExceptNewline(codePointBeforeStart)) {
+                    start -= Character.charCount(codePointBeforeStart);
+                    codePointBeforeStart = Character.codePointBefore(mText, start);
+                }
+            } else if (TextUtils.isWhitespaceExceptNewline(codePointAtEnd)
+                    && (TextUtils.isWhitespace(codePointBeforeStart)
+                            || TextUtils.isPunctuation(codePointBeforeStart))) {
+                // Remove whitespace (except new lines) after the deleted text, in these cases:
+                // - There is punctuation preceding the deleted text
+                //     e.g. "([deleted] two)" -> "(| two)" -> "(|two)"
+                // - There is a new line preceding the deleted text
+                //     e.g. "\n[deleted] two" -> "\n| two" -> "\n|two"
+                // - The deleted text is at the start of the text
+                //     e.g. "[deleted] two" -> "| two" -> "|two"
+                // (The pipe | indicates the cursor position.)
+                while (end < mText.length()
+                        && TextUtils.isWhitespaceExceptNewline(codePointAtEnd)) {
+                    end += Character.charCount(codePointAtEnd);
+                    codePointAtEnd = Character.codePointAt(mText, end);
+                }
+            }
+        }
+
+        getEditableText().delete(start, end);
+        Selection.setSelection(getEditableText(), start);
         return InputConnection.HANDWRITING_GESTURE_RESULT_SUCCESS;
     }
 
@@ -9341,7 +9390,7 @@
         PointF point = convertFromScreenToContentCoordinates(gesture.getInsertionPoint());
         int line = mLayout.getLineForVertical((int) point.y);
         if (point.y < mLayout.getLineTop(line)
-                || point.y > mLayout.getLineBottomWithoutSpacing(line)) {
+                || point.y > mLayout.getLineBottom(line, /* includeLineSpacing= */ false)) {
             return handleGestureFailure(gesture);
         }
         if (point.x < mLayout.getLineLeft(line) || point.x > mLayout.getLineRight(line)) {
@@ -9369,7 +9418,7 @@
             // Both points are above the top of the first line.
             return handleGestureFailure(gesture);
         }
-        if (yMin > mLayout.getLineBottomWithoutSpacing(line)) {
+        if (yMin > mLayout.getLineBottom(line, /* includeLineSpacing= */ false)) {
             if (line == mLayout.getLineCount() - 1 || yMax < mLayout.getLineTop(line + 1)) {
                 // The points are below the last line, or they are between two lines.
                 return handleGestureFailure(gesture);
@@ -9423,7 +9472,7 @@
 
         int line = mLayout.getLineForVertical((int) point.y);
         if (point.y < mLayout.getLineTop(line)
-                || point.y > mLayout.getLineBottomWithoutSpacing(line)) {
+                || point.y > mLayout.getLineBottom(line, /* includeLineSpacing= */ false)) {
             return handleGestureFailure(gesture);
         }
         if (point.x < mLayout.getLineLeft(line) || point.x > mLayout.getLineRight(line)) {
diff --git a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
index 17f9b7d..ea5c9a3 100644
--- a/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
+++ b/core/java/com/android/internal/inputmethod/IRemoteInputConnection.aidl
@@ -137,4 +137,7 @@
             int afterLength, int flags, in AndroidFuture future /* T=SurroundingText */);
 
     void setImeConsumesInput(in InputConnectionCommandHeader header, boolean imeConsumesInput);
+
+    void replaceText(in InputConnectionCommandHeader header, int start, int end, CharSequence text,
+            int newCursorPosition,in TextAttribute textAttribute);
 }
diff --git a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
index 713e913..fcaa1e1 100644
--- a/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/com/android/internal/inputmethod/RemoteInputConnectionImpl.java
@@ -1185,6 +1185,30 @@
         });
     }
 
+    @Dispatching(cancellable = true)
+    @Override
+    public void replaceText(
+            InputConnectionCommandHeader header,
+            int start,
+            int end,
+            @NonNull CharSequence text,
+            int newCursorPosition,
+            @Nullable TextAttribute textAttribute) {
+        dispatchWithTracing(
+                "replaceText",
+                () -> {
+                    if (header.mSessionId != mCurrentSessionId.get()) {
+                        return; // cancelled
+                    }
+                    InputConnection ic = getInputConnection();
+                    if (ic == null || !isActive()) {
+                        Log.w(TAG, "replaceText on inactive InputConnection");
+                        return;
+                    }
+                    ic.replaceText(start, end, text, newCursorPosition, textAttribute);
+                });
+    }
+
     private final IRemoteAccessibilityInputConnection mAccessibilityInputConnection =
             new IRemoteAccessibilityInputConnection.Stub() {
         @Dispatching(cancellable = true)
diff --git a/core/java/com/android/internal/util/function/pooled/PooledLambda.java b/core/java/com/android/internal/util/function/pooled/PooledLambda.java
index f073c1c0..2bfde24 100755
--- a/core/java/com/android/internal/util/function/pooled/PooledLambda.java
+++ b/core/java/com/android/internal/util/function/pooled/PooledLambda.java
@@ -22,36 +22,24 @@
 import android.os.Message;
 
 import com.android.internal.util.function.DecConsumer;
-import com.android.internal.util.function.DecFunction;
 import com.android.internal.util.function.DodecConsumer;
-import com.android.internal.util.function.DodecFunction;
 import com.android.internal.util.function.HeptConsumer;
-import com.android.internal.util.function.HeptFunction;
 import com.android.internal.util.function.HexConsumer;
-import com.android.internal.util.function.HexFunction;
 import com.android.internal.util.function.NonaConsumer;
-import com.android.internal.util.function.NonaFunction;
 import com.android.internal.util.function.OctConsumer;
-import com.android.internal.util.function.OctFunction;
 import com.android.internal.util.function.QuadConsumer;
-import com.android.internal.util.function.QuadFunction;
 import com.android.internal.util.function.QuadPredicate;
 import com.android.internal.util.function.QuintConsumer;
-import com.android.internal.util.function.QuintFunction;
 import com.android.internal.util.function.QuintPredicate;
 import com.android.internal.util.function.TriConsumer;
-import com.android.internal.util.function.TriFunction;
 import com.android.internal.util.function.TriPredicate;
 import com.android.internal.util.function.UndecConsumer;
-import com.android.internal.util.function.UndecFunction;
 import com.android.internal.util.function.pooled.PooledLambdaImpl.LambdaType.ReturnType;
 
 import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Function;
-import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 /**
@@ -194,40 +182,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1) }
-     */
-    static <A> PooledSupplier<Boolean> obtainSupplier(
-            Predicate<? super A> function,
-            A arg1) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 1, 0, ReturnType.BOOLEAN, arg1, null, null, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1) }
-     */
-    static <A, R> PooledSupplier<R> obtainSupplier(
-            Function<? super A, ? extends R> function,
-            A arg1) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 1, 0, ReturnType.OBJECT, arg1, null, null, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -279,42 +233,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2) }
-     */
-    static <A, B> PooledSupplier<Boolean> obtainSupplier(
-            BiPredicate<? super A, ? super B> function,
-            A arg1, B arg2) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 2, 0, ReturnType.BOOLEAN, arg1, arg2, null, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2) }
-     */
-    static <A, B, R> PooledSupplier<R> obtainSupplier(
-            BiFunction<? super A, ? super B, ? extends R> function,
-            A arg1, B arg2) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 2, 0, ReturnType.OBJECT, arg1, arg2, null, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -411,24 +329,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
-     * @param arg2 parameter supplied to {@code function} on call
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg1) -> function(arg1, arg2) }
-     */
-    static <A, B, R> PooledFunction<A, R> obtainFunction(
-            BiFunction<? super A, ? super B, ? extends R> function,
-            ArgumentPlaceholder<A> arg1, B arg2) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 2, 1, ReturnType.OBJECT, arg1, arg2, null, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -465,24 +365,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg2) -> function(arg1, arg2) }
-     */
-    static <A, B, R> PooledFunction<B, R> obtainFunction(
-            BiFunction<? super A, ? super B, ? extends R> function,
-            A arg1, ArgumentPlaceholder<B> arg2) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 2, 1, ReturnType.OBJECT, arg1, arg2, null, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -536,25 +418,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3) }
-     */
-    static <A, B, C, R> PooledSupplier<R> obtainSupplier(
-            TriFunction<? super A, ? super B, ? super C, ? extends R> function,
-            A arg1, B arg2, C arg3) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 3, 0, ReturnType.OBJECT, arg1, arg2, arg3, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -574,25 +437,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg1) -> function(arg1, arg2, arg3) }
-     */
-    static <A, B, C, R> PooledFunction<A, R> obtainFunction(
-            TriFunction<? super A, ? super B, ? super C, ? extends R> function,
-            ArgumentPlaceholder<A> arg1, B arg2, C arg3) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -612,25 +456,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
-     * @param arg3 parameter supplied to {@code function} on call
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg2) -> function(arg1, arg2, arg3) }
-     */
-    static <A, B, C, R> PooledFunction<B, R> obtainFunction(
-            TriFunction<? super A, ? super B, ? super C, ? extends R> function,
-            A arg1, ArgumentPlaceholder<B> arg2, C arg3) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -650,25 +475,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 placeholder for a missing argument. Use {@link #__} to get one
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg3) -> function(arg1, arg2, arg3) }
-     */
-    static <A, B, C, R> PooledFunction<C, R> obtainFunction(
-            TriFunction<? super A, ? super B, ? super C, ? extends R> function,
-            A arg1, B arg2, ArgumentPlaceholder<C> arg3) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 3, 1, ReturnType.OBJECT, arg1, arg2, arg3, null, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -724,26 +530,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4) }
-     */
-    static <A, B, C, D, R> PooledSupplier<R> obtainSupplier(
-            QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
-            A arg1, B arg2, C arg3, D arg4) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 4, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -764,26 +550,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 placeholder for a missing argument. Use {@link #__} to get one
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg1) -> function(arg1, arg2, arg3, arg4) }
-     */
-    static <A, B, C, D, R> PooledFunction<A, R> obtainFunction(
-            QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
-            ArgumentPlaceholder<A> arg1, B arg2, C arg3, D arg4) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -804,26 +570,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 placeholder for a missing argument. Use {@link #__} to get one
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg2) -> function(arg1, arg2, arg3, arg4) }
-     */
-    static <A, B, C, D, R> PooledFunction<B, R> obtainFunction(
-            QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
-            A arg1, ArgumentPlaceholder<B> arg2, C arg3, D arg4) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -844,26 +590,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 placeholder for a missing argument. Use {@link #__} to get one
-     * @param arg4 parameter supplied to {@code function} on call
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg3) -> function(arg1, arg2, arg3, arg4) }
-     */
-    static <A, B, C, D, R> PooledFunction<C, R> obtainFunction(
-            QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
-            A arg1, B arg2, ArgumentPlaceholder<C> arg3, D arg4) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * {@link PooledConsumer} factory
      *
      * @param function non-capturing lambda(typically an unbounded method reference)
@@ -884,26 +610,6 @@
     }
 
     /**
-     * {@link PooledFunction} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 placeholder for a missing argument. Use {@link #__} to get one
-     * @return a {@link PooledFunction}, equivalent to lambda:
-     *         {@code (arg4) -> function(arg1, arg2, arg3, arg4) }
-     */
-    static <A, B, C, D, R> PooledFunction<D, R> obtainFunction(
-            QuadFunction<? super A, ? super B, ? super C, ? super D, ? extends R> function,
-            A arg1, B arg2, C arg3, ArgumentPlaceholder<D> arg4) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 4, 1, ReturnType.OBJECT, arg1, arg2, arg3, arg4, null, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -961,27 +667,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @param arg5 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4, arg5) }
-     */
-    static <A, B, C, D, E, R> PooledSupplier<R> obtainSupplier(
-            QuintFunction<? super A, ? super B, ? super C, ? super D, ? super E, ? extends R>
-                    function, A arg1, B arg2, C arg3, D arg4, E arg5) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 5, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, arg5, null, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -1042,28 +727,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @param arg5 parameter supplied to {@code function} on call
-     * @param arg6 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4, arg5, arg6) }
-     */
-    static <A, B, C, D, E, F, R> PooledSupplier<R> obtainSupplier(
-            HexFunction<? super A, ? super B, ? super C, ? super D, ? super E, ? super F,
-                    ? extends R> function, A arg1, B arg2, C arg3, D arg4, E arg5, F arg6) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 6, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, arg5, arg6, null, null,
-                null, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -1126,30 +789,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @param arg5 parameter supplied to {@code function} on call
-     * @param arg6 parameter supplied to {@code function} on call
-     * @param arg7 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4, arg5, arg6, arg7) }
-     */
-    static <A, B, C, D, E, F, G, R> PooledSupplier<R> obtainSupplier(
-            HeptFunction<? super A, ? super B, ? super C, ? super D, ? super E, ? super F,
-                    ? super G, ? extends R> function,
-            A arg1, B arg2, C arg3, D arg4, E arg5, F arg6, G arg7) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 7, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, arg5, arg6, arg7, null,
-                null, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -1215,31 +854,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @param arg5 parameter supplied to {@code function} on call
-     * @param arg6 parameter supplied to {@code function} on call
-     * @param arg7 parameter supplied to {@code function} on call
-     * @param arg8 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) }
-     */
-    static <A, B, C, D, E, F, G, H, R> PooledSupplier<R> obtainSupplier(
-            OctFunction<? super A, ? super B, ? super C, ? super D, ? super E, ? super F,
-                                ? super G, ? super H, ? extends R> function,
-            A arg1, B arg2, C arg3, D arg4, E arg5, F arg6, G arg7, H arg8) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 8, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8,
-                null, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -1308,32 +922,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @param arg5 parameter supplied to {@code function} on call
-     * @param arg6 parameter supplied to {@code function} on call
-     * @param arg7 parameter supplied to {@code function} on call
-     * @param arg8 parameter supplied to {@code function} on call
-     * @param arg9 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) }
-     */
-    static <A, B, C, D, E, F, G, H, I, R> PooledSupplier<R> obtainSupplier(
-            NonaFunction<? super A, ? super B, ? super C, ? super D, ? super E, ? super F,
-                                ? super G, ? super H, ? super I, ? extends R> function,
-            A arg1, B arg2, C arg3, D arg4, E arg5, F arg6, G arg7, H arg8, I arg9) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 9, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8,
-                arg9, null, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -1404,33 +992,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @param arg5 parameter supplied to {@code function} on call
-     * @param arg6 parameter supplied to {@code function} on call
-     * @param arg7 parameter supplied to {@code function} on call
-     * @param arg8 parameter supplied to {@code function} on call
-     * @param arg9 parameter supplied to {@code function} on call
-     * @param arg10 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) }
-     */
-    static <A, B, C, D, E, F, G, H, I, J, R> PooledSupplier<R> obtainSupplier(
-            DecFunction<? super A, ? super B, ? super C, ? super D, ? super E, ? super F,
-                                ? super G, ? super H, ? super I, ? super J, ? extends R> function,
-            A arg1, B arg2, C arg3, D arg4, E arg5, F arg6, G arg7, H arg8, I arg9, J arg10) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 10, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8,
-                arg9, arg10, null, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -1504,36 +1065,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @param arg5 parameter supplied to {@code function} on call
-     * @param arg6 parameter supplied to {@code function} on call
-     * @param arg7 parameter supplied to {@code function} on call
-     * @param arg8 parameter supplied to {@code function} on call
-     * @param arg9 parameter supplied to {@code function} on call
-     * @param arg10 parameter supplied to {@code function} on call
-     * @param arg11 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10,
-     *         arg11) }
-     */
-    static <A, B, C, D, E, F, G, H, I, J, K, R> PooledSupplier<R> obtainSupplier(
-            UndecFunction<? super A, ? super B, ? super C, ? super D, ? super E, ? super F,
-                    ? super G, ? super H, ? super I, ? super J, ? super K, ? extends R> function,
-            A arg1, B arg2, C arg3, D arg4, E arg5, F arg6, G arg7, H arg8, I arg9, J arg10,
-            K arg11) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 11, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8,
-                arg9, arg10, arg11, null);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
@@ -1611,38 +1142,6 @@
     }
 
     /**
-     * {@link PooledSupplier} factory
-     *
-     * @param function non-capturing lambda(typically an unbounded method reference)
-     *                 to be invoked on call
-     * @param arg1 parameter supplied to {@code function} on call
-     * @param arg2 parameter supplied to {@code function} on call
-     * @param arg3 parameter supplied to {@code function} on call
-     * @param arg4 parameter supplied to {@code function} on call
-     * @param arg5 parameter supplied to {@code function} on call
-     * @param arg6 parameter supplied to {@code function} on call
-     * @param arg7 parameter supplied to {@code function} on call
-     * @param arg8 parameter supplied to {@code function} on call
-     * @param arg9 parameter supplied to {@code function} on call
-     * @param arg10 parameter supplied to {@code function} on call
-     * @param arg11 parameter supplied to {@code function} on call
-     * @param arg12 parameter supplied to {@code function} on call
-     * @return a {@link PooledSupplier}, equivalent to lambda:
-     *         {@code () -> function(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10,
-     *         arg11) }
-     */
-    static <A, B, C, D, E, F, G, H, I, J, K, L, R> PooledSupplier<R> obtainSupplier(
-            DodecFunction<? super A, ? super B, ? super C, ? super D, ? super E, ? super F,
-                                ? super G, ? super H, ? super I, ? super J, ? super K, ? extends L,
-                                ? extends R> function,
-            A arg1, B arg2, C arg3, D arg4, E arg5, F arg6, G arg7, H arg8, I arg9, J arg10,
-            K arg11, L arg12) {
-        return acquire(PooledLambdaImpl.sPool,
-                function, 11, 0, ReturnType.OBJECT, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8,
-                arg9, arg10, arg11, arg12);
-    }
-
-    /**
      * Factory of {@link Message}s that contain an
      * ({@link PooledLambda#recycleOnUse auto-recycling}) {@link PooledRunnable} as its
      * {@link Message#getCallback internal callback}.
diff --git a/core/proto/android/os/incident.proto b/core/proto/android/os/incident.proto
index 4bbfee2..59e01bf 100644
--- a/core/proto/android/os/incident.proto
+++ b/core/proto/android/os/incident.proto
@@ -61,7 +61,6 @@
 import "frameworks/base/core/proto/android/privacy.proto";
 import "frameworks/base/core/proto/android/section.proto";
 import "frameworks/base/proto/src/ipconnectivity.proto";
-import "packages/modules/Permission/service/proto/role_service.proto";
 
 package android.os;
 
@@ -358,10 +357,7 @@
         (section).userdebug_and_eng_only = true
     ];
 
-    optional com.android.role.RoleServiceDumpProto role = 3024 [
-        (section).type = SECTION_DUMPSYS,
-        (section).args = "role --proto"
-    ];
+    reserved 3024;
 
     optional android.service.restricted_image.RestrictedImagesDumpProto restricted_images = 3025 [
         (section).type = SECTION_DUMPSYS,
diff --git a/core/tests/coretests/src/android/text/LayoutGetRangeForRectTest.java b/core/tests/coretests/src/android/text/LayoutGetRangeForRectTest.java
index 32fdb5e..787a405 100644
--- a/core/tests/coretests/src/android/text/LayoutGetRangeForRectTest.java
+++ b/core/tests/coretests/src/android/text/LayoutGetRangeForRectTest.java
@@ -90,7 +90,8 @@
 
         mLineCenters = new float[mLayout.getLineCount()];
         for (int i = 0; i < mLayout.getLineCount(); ++i) {
-            mLineCenters[i] = (mLayout.getLineTop(i) + mLayout.getLineBottomWithoutSpacing(i)) / 2f;
+            mLineCenters[i] = (mLayout.getLineTop(i)
+                    + mLayout.getLineBottom(i, /* includeLineSpacing= */ false)) / 2f;
         }
 
         mGraphemeClusterSegmentIterator =
diff --git a/core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java b/core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java
index 2bb5abe..4007c43 100644
--- a/core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java
+++ b/core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java
@@ -619,6 +619,74 @@
         verifyTextSnapshotContentEquals(mBaseInputConnection.takeSnapshot(), expectedTextSnapshot);
     }
 
+    @Test
+    public void testReplaceText_toEditorWithoutSelectionAndComposing() {
+        // before replace: "|"
+        // after replace: "text1|"
+        assertThat(mBaseInputConnection.replaceText(0, 0, "text1", 1, null)).isTrue();
+        verifyContent("text1", 5, 5, -1, -1);
+
+        // before replace: "text1|"
+        // after replace: "text2|"
+        assertThat(mBaseInputConnection.replaceText(0, 5, "text2", 1, null)).isTrue();
+        verifyContent("text2", 5, 5, -1, -1);
+
+        // before replace: "text1|"
+        // after replace: "|text3"
+        assertThat(mBaseInputConnection.replaceText(0, 5, "text3", -1, null)).isTrue();
+        verifyContent("text3", 0, 0, -1, -1);
+
+        // before replace: "|text3"
+        // after replace: "ttext4|t3"
+        // BUG(b/21476564): this behavior is inconsistent with API description.
+        assertThat(mBaseInputConnection.replaceText(1, 3, "text4", 1, null)).isTrue();
+        verifyContent("ttext4t3", 6, 6, -1, -1);
+
+        // before replace: "ttext4|t3"
+        // after replace: "|text5t3"
+        assertThat(mBaseInputConnection.replaceText(0, 6, "text5", -1, null)).isTrue();
+        verifyContent("text5t3", 0, 0, -1, -1);
+    }
+
+    @Test
+    public void testReplaceText_toEditorWithSelection() {
+        // before replace: "123|456|789"
+        // before replace: "123text|6789"
+        prepareContent("123456789", 3, 6, -1, -1);
+        assertThat(mBaseInputConnection.replaceText(3, 5, "text", 1, null)).isTrue();
+        verifyContent("123text6789", 7, 7, -1, -1);
+
+        // before replace: "|123|"
+        // before replace: "|text23"
+        prepareContent("123", 0, 3, -1, -1);
+        assertThat(mBaseInputConnection.replaceText(0, 1, "text", 0, null)).isTrue();
+        verifyContent("text23", 0, 0, -1, -1);
+    }
+
+    @Test
+    public void testReplaceText_toEditorWithComposing() {
+        // before replace: "123456|789"
+        //                     ---
+        // before replace: "123456text|"
+        prepareContent("123456789", 6, 6, 3, 6);
+        assertThat(mBaseInputConnection.replaceText(6, 9, "text", 1, null)).isTrue();
+        verifyContent("123456text", 10, 10, -1, -1);
+
+        // before replace: "123456789|"
+        //                     ---
+        // before replace: "text|123456789"
+        prepareContent("123456789", 9, 9, 3, 6);
+        assertThat(mBaseInputConnection.replaceText(0, 0, "text", 1, null)).isTrue();
+        verifyContent("text123456789", 4, 4, -1, -1);
+
+        // before replace: "|123456789|"
+        //                      ---
+        // before replace: "12text|9"
+        prepareContent("123456789", 0, 9, 3, 6);
+        assertThat(mBaseInputConnection.replaceText(2, 8, "text", 1, null)).isTrue();
+        verifyContent("12text9", 6, 6, -1, -1);
+    }
+
     private void prepareContent(
             CharSequence text,
             int selectionStart,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 33074de..f4dda4c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -57,6 +57,8 @@
 import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
 import com.android.wm.shell.compatui.CompatUIController;
 import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeController;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelperController;
@@ -716,15 +718,36 @@
     // Desktop mode (optional feature)
     //
 
+    @WMSingleton
+    @Provides
+    static Optional<DesktopMode> provideDesktopMode(
+            Optional<DesktopModeController> desktopModeController) {
+        return desktopModeController.map(DesktopModeController::asDesktopMode);
+    }
+
+    @BindsOptionalOf
+    @DynamicOverride
+    abstract DesktopModeController optionalDesktopModeController();
+
+    @WMSingleton
+    @Provides
+    static Optional<DesktopModeController> providesDesktopModeController(
+            @DynamicOverride Optional<DesktopModeController> desktopModeController) {
+        if (DesktopModeStatus.IS_SUPPORTED) {
+            return desktopModeController;
+        }
+        return Optional.empty();
+    }
+
     @BindsOptionalOf
     @DynamicOverride
     abstract DesktopModeTaskRepository optionalDesktopModeTaskRepository();
 
     @WMSingleton
     @Provides
-    static Optional<DesktopModeTaskRepository> providesDesktopModeTaskRepository(
+    static Optional<DesktopModeTaskRepository> providesDesktopTaskRepository(
             @DynamicOverride Optional<DesktopModeTaskRepository> desktopModeTaskRepository) {
-        if (DesktopMode.IS_SUPPORTED) {
+        if (DesktopModeStatus.IS_SUPPORTED) {
             return desktopModeTaskRepository;
         }
         return Optional.empty();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 35e88e9..e784261 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -48,7 +48,6 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.desktopmode.DesktopModeController;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.draganddrop.DragAndDropController;
@@ -595,19 +594,18 @@
 
     @WMSingleton
     @Provides
-    static Optional<DesktopModeController> provideDesktopModeController(
-            Context context, ShellInit shellInit,
+    @DynamicOverride
+    static DesktopModeController provideDesktopModeController(Context context, ShellInit shellInit,
             ShellTaskOrganizer shellTaskOrganizer,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            Transitions transitions,
+            @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
             @ShellMainThread Handler mainHandler,
-            Transitions transitions
+            @ShellMainThread ShellExecutor mainExecutor
     ) {
-        if (DesktopMode.IS_SUPPORTED) {
-            return Optional.of(new DesktopModeController(context, shellInit, shellTaskOrganizer,
-                    rootTaskDisplayAreaOrganizer, mainHandler, transitions));
-        } else {
-            return Optional.empty();
-        }
+        return new DesktopModeController(context, shellInit, shellTaskOrganizer,
+                rootTaskDisplayAreaOrganizer, transitions, desktopModeTaskRepository, mainHandler,
+                mainExecutor);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index 8993d54..ff3be38 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -16,43 +16,16 @@
 
 package com.android.wm.shell.desktopmode;
 
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
-
-import android.content.Context;
-import android.os.SystemProperties;
-import android.os.UserHandle;
-import android.provider.Settings;
-
-import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.annotations.ExternalThread;
 
 /**
- * Constants for desktop mode feature
+ * Interface to interact with desktop mode feature in shell.
  */
-public class DesktopMode {
+@ExternalThread
+public interface DesktopMode {
 
-    /**
-     * Flag to indicate whether desktop mode is available on the device
-     */
-    public static final boolean IS_SUPPORTED = SystemProperties.getBoolean(
-            "persist.wm.debug.desktop_mode", false);
-
-    /**
-     * Check if desktop mode is active
-     *
-     * @return {@code true} if active
-     */
-    public static boolean isActive(Context context) {
-        if (!IS_SUPPORTED) {
-            return false;
-        }
-        try {
-            int result = Settings.System.getIntForUser(context.getContentResolver(),
-                    Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT);
-            ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result);
-            return result != 0;
-        } catch (Exception e) {
-            ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e);
-            return false;
-        }
+    /** Returns a binder that can be passed to an external process to manipulate DesktopMode. */
+    default IDesktopMode createExternalInterface() {
+        return null;
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index 6e44d58..9474cfe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -20,8 +20,10 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 
+import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
 
+import android.app.ActivityManager.RunningTaskInfo;
 import android.app.WindowConfiguration;
 import android.content.Context;
 import android.database.ContentObserver;
@@ -29,51 +31,83 @@
 import android.os.Handler;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.ArraySet;
 import android.window.DisplayAreaInfo;
 import android.window.WindowContainerTransaction;
 
+import androidx.annotation.BinderThread;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.RemoteCallable;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
+import java.util.ArrayList;
+import java.util.Comparator;
+
 /**
  * Handles windowing changes when desktop mode system setting changes
  */
-public class DesktopModeController {
+public class DesktopModeController implements RemoteCallable<DesktopModeController> {
 
     private final Context mContext;
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
-    private final SettingsObserver mSettingsObserver;
     private final Transitions mTransitions;
+    private final DesktopModeTaskRepository mDesktopModeTaskRepository;
+    private final ShellExecutor mMainExecutor;
+    private final DesktopMode mDesktopModeImpl = new DesktopModeImpl();
+    private final SettingsObserver mSettingsObserver;
 
     public DesktopModeController(Context context, ShellInit shellInit,
             ShellTaskOrganizer shellTaskOrganizer,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+            Transitions transitions,
+            DesktopModeTaskRepository desktopModeTaskRepository,
             @ShellMainThread Handler mainHandler,
-            Transitions transitions) {
+            @ShellMainThread ShellExecutor mainExecutor) {
         mContext = context;
         mShellTaskOrganizer = shellTaskOrganizer;
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
-        mSettingsObserver = new SettingsObserver(mContext, mainHandler);
         mTransitions = transitions;
+        mDesktopModeTaskRepository = desktopModeTaskRepository;
+        mMainExecutor = mainExecutor;
+        mSettingsObserver = new SettingsObserver(mContext, mainHandler);
         shellInit.addInitCallback(this::onInit, this);
     }
 
     private void onInit() {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopModeController");
         mSettingsObserver.observe();
-        if (DesktopMode.isActive(mContext)) {
+        if (DesktopModeStatus.isActive(mContext)) {
             updateDesktopModeActive(true);
         }
     }
 
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public ShellExecutor getRemoteCallExecutor() {
+        return mMainExecutor;
+    }
+
+    /**
+     * Get connection interface between sysui and shell
+     */
+    public DesktopMode asDesktopMode() {
+        return mDesktopModeImpl;
+    }
+
     @VisibleForTesting
     void updateDesktopModeActive(boolean active) {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
@@ -121,6 +155,28 @@
     }
 
     /**
+     * Show apps on desktop
+     */
+    public void showDesktopApps() {
+        ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
+        ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
+        for (Integer taskId : activeTasks) {
+            RunningTaskInfo taskInfo = mShellTaskOrganizer.getRunningTaskInfo(taskId);
+            if (taskInfo != null) {
+                taskInfos.add(taskInfo);
+            }
+        }
+        // Order by lastActiveTime, descending
+        taskInfos.sort(Comparator.comparingLong(task -> -task.lastActiveTime));
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        for (RunningTaskInfo task : taskInfos) {
+            wct.reorder(task.token, true);
+        }
+        mShellTaskOrganizer.applyTransaction(wct);
+    }
+
+    /**
      * A {@link ContentObserver} for listening to changes to {@link Settings.System#DESKTOP_MODE}
      */
     private final class SettingsObserver extends ContentObserver {
@@ -150,8 +206,51 @@
         }
 
         private void desktopModeSettingChanged() {
-            boolean enabled = DesktopMode.isActive(mContext);
+            boolean enabled = DesktopModeStatus.isActive(mContext);
             updateDesktopModeActive(enabled);
         }
     }
+
+    /**
+     * The interface for calls from outside the shell, within the host process.
+     */
+    @ExternalThread
+    private final class DesktopModeImpl implements DesktopMode {
+
+        private IDesktopModeImpl mIDesktopMode;
+
+        @Override
+        public IDesktopMode createExternalInterface() {
+            if (mIDesktopMode != null) {
+                mIDesktopMode.invalidate();
+            }
+            mIDesktopMode = new IDesktopModeImpl(DesktopModeController.this);
+            return mIDesktopMode;
+        }
+    }
+
+    /**
+     * The interface for calls from outside the host process.
+     */
+    @BinderThread
+    private static class IDesktopModeImpl extends IDesktopMode.Stub {
+
+        private DesktopModeController mController;
+
+        IDesktopModeImpl(DesktopModeController controller) {
+            mController = controller;
+        }
+
+        /**
+         * Invalidates this instance, preventing future calls from updating the controller.
+         */
+        void invalidate() {
+            mController = null;
+        }
+
+        public void showDesktopApps() {
+            executeRemoteCallWithTaskPermission(mController, "showDesktopApps",
+                    DesktopModeController::showDesktopApps);
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
new file mode 100644
index 0000000..195ff50
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -0,0 +1,58 @@
+/*
+ * 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.wm.shell.desktopmode;
+
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+
+import android.content.Context;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import com.android.internal.protolog.common.ProtoLog;
+
+/**
+ * Constants for desktop mode feature
+ */
+public class DesktopModeStatus {
+
+    /**
+     * Flag to indicate whether desktop mode is available on the device
+     */
+    public static final boolean IS_SUPPORTED = SystemProperties.getBoolean(
+            "persist.wm.debug.desktop_mode", false);
+
+    /**
+     * Check if desktop mode is active
+     *
+     * @return {@code true} if active
+     */
+    public static boolean isActive(Context context) {
+        if (!IS_SUPPORTED) {
+            return false;
+        }
+        try {
+            int result = Settings.System.getIntForUser(context.getContentResolver(),
+                    Settings.System.DESKTOP_MODE, UserHandle.USER_CURRENT);
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE, "isDesktopModeEnabled=%s", result);
+            return result != 0;
+        } catch (Exception e) {
+            ProtoLog.e(WM_SHELL_DESKTOP_MODE, "Failed to read DESKTOP_MODE setting %s", e);
+            return false;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
new file mode 100644
index 0000000..5042bd6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -0,0 +1,26 @@
+/*
+ * 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.wm.shell.desktopmode;
+
+/**
+ * Interface that is exposed to remote callers to manipulate desktop mode features.
+ */
+interface IDesktopMode {
+
+    /** Show apps on the desktop */
+    void showDesktopApps();
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index f58719b..e2d5a49 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -28,7 +28,7 @@
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ShellInit;
@@ -90,7 +90,7 @@
             t.apply();
         }
 
-        if (DesktopMode.IS_SUPPORTED && taskInfo.isVisible) {
+        if (DesktopModeStatus.IS_SUPPORTED && taskInfo.isVisible) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                     "Adding active freeform task: #%d", taskInfo.taskId);
             mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
@@ -123,7 +123,7 @@
                 taskInfo.taskId);
         mTasks.remove(taskInfo.taskId);
 
-        if (DesktopMode.IS_SUPPORTED) {
+        if (DesktopModeStatus.IS_SUPPORTED) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                     "Removing active freeform task: #%d", taskInfo.taskId);
             mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId));
@@ -150,7 +150,7 @@
             mWindowDecorationViewModel.onTaskInfoChanged(state.mTaskInfo, state.mWindowDecoration);
         }
 
-        if (DesktopMode.IS_SUPPORTED) {
+        if (DesktopModeStatus.IS_SUPPORTED) {
             if (taskInfo.isVisible) {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                         "Adding active freeform task: #%d", taskInfo.taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index f879994..6409e70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -44,7 +44,7 @@
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -287,7 +287,7 @@
             rawMapping.put(taskInfo.taskId, taskInfo);
         }
 
-        boolean desktopModeActive = DesktopMode.isActive(mContext);
+        boolean desktopModeActive = DesktopModeStatus.isActive(mContext);
         ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>();
 
         // Pull out the pairs as we iterate back in the list
@@ -320,7 +320,6 @@
 
         // Add a special entry for freeform tasks
         if (!freeformTasks.isEmpty()) {
-            // First task is added separately
             recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks(
                     freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0])));
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index e8a2cb160..9c7131a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -36,7 +36,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
 import com.android.wm.shell.transition.Transitions;
 
@@ -239,7 +239,7 @@
 
     private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
         if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
-        return DesktopMode.IS_SUPPORTED
+        return DesktopModeStatus.IS_SUPPORTED
                 && mDisplayController.getDisplayContext(taskInfo.displayId)
                 .getResources().getConfiguration().smallestScreenWidthDp >= 600;
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 5040bc3..733f6b7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -34,7 +34,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
 
 /**
  * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with
@@ -164,7 +164,7 @@
         View caption = mResult.mRootView.findViewById(R.id.caption);
         caption.setOnTouchListener(mOnCaptionTouchListener);
         View maximize = caption.findViewById(R.id.maximize_window);
-        if (DesktopMode.IS_SUPPORTED) {
+        if (DesktopModeStatus.IS_SUPPORTED) {
             // Hide maximize button when desktop mode is available
             maximize.setVisibility(View.GONE);
         } else {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
index c0720cf..3672ae3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
@@ -44,6 +44,7 @@
     private ActivityManager.TaskDescription.Builder mTaskDescriptionBuilder = null;
     private final Point mPositionInParent = new Point();
     private boolean mIsVisible = false;
+    private long mLastActiveTime;
 
     public static WindowContainerToken createMockWCToken() {
         final IWindowContainerToken itoken = mock(IWindowContainerToken.class);
@@ -52,6 +53,11 @@
         return new WindowContainerToken(itoken);
     }
 
+    public TestRunningTaskInfoBuilder setToken(WindowContainerToken token) {
+        mToken = token;
+        return this;
+    }
+
     public TestRunningTaskInfoBuilder setBounds(Rect bounds) {
         mBounds.set(bounds);
         return this;
@@ -95,6 +101,11 @@
         return this;
     }
 
+    public TestRunningTaskInfoBuilder setLastActiveTime(long lastActiveTime) {
+        mLastActiveTime = lastActiveTime;
+        return this;
+    }
+
     public ActivityManager.RunningTaskInfo build() {
         final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
         info.taskId = sNextTaskId++;
@@ -110,6 +121,7 @@
                 mTaskDescriptionBuilder != null ? mTaskDescriptionBuilder.build() : null;
         info.positionInParent = mPositionInParent;
         info.isVisible = mIsVisible;
+        info.lastActiveTime = mLastActiveTime;
         return info;
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index c628f399..dd23d97 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -19,16 +19,22 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.WindowConfiguration;
+import android.app.ActivityManager;
 import android.os.Handler;
 import android.os.IBinder;
 import android.testing.AndroidTestingRunner;
@@ -39,13 +45,17 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -67,18 +77,38 @@
     private Handler mMockHandler;
     @Mock
     private Transitions mMockTransitions;
+    private TestShellExecutor mExecutor;
 
     private DesktopModeController mController;
+    private DesktopModeTaskRepository mDesktopModeTaskRepository;
     private ShellInit mShellInit;
+    private StaticMockitoSession mMockitoSession;
 
     @Before
     public void setUp() {
+        mMockitoSession = mockitoSession().mockStatic(DesktopModeStatus.class).startMocking();
+        when(DesktopModeStatus.isActive(any())).thenReturn(true);
+
         mShellInit = Mockito.spy(new ShellInit(mTestExecutor));
+        mExecutor = new TestShellExecutor();
+
+        mDesktopModeTaskRepository = new DesktopModeTaskRepository();
 
         mController = new DesktopModeController(mContext, mShellInit, mShellTaskOrganizer,
-                mRootTaskDisplayAreaOrganizer, mMockHandler, mMockTransitions);
+                mRootTaskDisplayAreaOrganizer, mMockTransitions,
+                mDesktopModeTaskRepository, mMockHandler, mExecutor);
+
+        when(mShellTaskOrganizer.prepareClearFreeformForStandardTasks(anyInt())).thenReturn(
+                new WindowContainerTransaction());
 
         mShellInit.init();
+        clearInvocations(mShellTaskOrganizer);
+        clearInvocations(mRootTaskDisplayAreaOrganizer);
+    }
+
+    @After
+    public void tearDown() {
+        mMockitoSession.finishMocking();
     }
 
     @Test
@@ -159,17 +189,15 @@
         assertThat(wct.getChanges()).hasSize(3);
 
         // Verify executed WCT has a change for setting task windowing mode to undefined
-        Change taskWmModeChange = wct.getChanges().get(taskWmMockToken.binder());
-        assertThat(taskWmModeChange).isNotNull();
-        assertThat(taskWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
+        Change taskWmMode = wct.getChanges().get(taskWmMockToken.binder());
+        assertThat(taskWmMode).isNotNull();
+        assertThat(taskWmMode.getWindowingMode()).isEqualTo(WINDOWING_MODE_UNDEFINED);
 
         // Verify executed WCT has a change for clearing task bounds
-        Change taskBoundsChange = wct.getChanges().get(taskBoundsMockToken.binder());
-        assertThat(taskBoundsChange).isNotNull();
-        assertThat(taskBoundsChange.getWindowSetMask()
-                & WindowConfiguration.WINDOW_CONFIG_BOUNDS).isNotEqualTo(0);
-        assertThat(taskBoundsChange.getConfiguration().windowConfiguration.getBounds().isEmpty())
-                .isTrue();
+        Change bounds = wct.getChanges().get(taskBoundsMockToken.binder());
+        assertThat(bounds).isNotNull();
+        assertThat(bounds.getWindowSetMask() & WINDOW_CONFIG_BOUNDS).isNotEqualTo(0);
+        assertThat(bounds.getConfiguration().windowConfiguration.getBounds().isEmpty()).isTrue();
 
         // Verify executed WCT has a change for setting display windowing mode to fullscreen
         Change displayWmModeChange = wct.getChanges().get(displayAreaInfo.token.asBinder());
@@ -177,6 +205,41 @@
         assertThat(displayWmModeChange.getWindowingMode()).isEqualTo(WINDOWING_MODE_FULLSCREEN);
     }
 
+    @Test
+    public void testShowDesktopApps() {
+        // Set up two active tasks on desktop
+        mDesktopModeTaskRepository.addActiveTask(1);
+        mDesktopModeTaskRepository.addActiveTask(2);
+        MockToken token1 = new MockToken();
+        MockToken token2 = new MockToken();
+        ActivityManager.RunningTaskInfo taskInfo1 = new TestRunningTaskInfoBuilder().setToken(
+                token1.token()).setLastActiveTime(100).build();
+        ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder().setToken(
+                token2.token()).setLastActiveTime(200).build();
+        when(mShellTaskOrganizer.getRunningTaskInfo(1)).thenReturn(taskInfo1);
+        when(mShellTaskOrganizer.getRunningTaskInfo(2)).thenReturn(taskInfo2);
+
+        // Run show desktop apps logic
+        mController.showDesktopApps();
+        ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass(
+                WindowContainerTransaction.class);
+        verify(mShellTaskOrganizer).applyTransaction(wctCaptor.capture());
+        WindowContainerTransaction wct = wctCaptor.getValue();
+
+        // Check wct has reorder calls
+        assertThat(wct.getHierarchyOps()).hasSize(2);
+
+        // Task 2 has activity later, must be first
+        WindowContainerTransaction.HierarchyOp op1 = wct.getHierarchyOps().get(0);
+        assertThat(op1.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+        assertThat(op1.getContainer()).isEqualTo(token2.binder());
+
+        // Task 1 should be second
+        WindowContainerTransaction.HierarchyOp op2 = wct.getHierarchyOps().get(0);
+        assertThat(op2.getType()).isEqualTo(HIERARCHY_OP_TYPE_REORDER);
+        assertThat(op2.getContainer()).isEqualTo(token2.binder());
+    }
+
     private static class MockToken {
         private final WindowContainerToken mToken;
         private final IBinder mBinder;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index cadfeb0..70fee2b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -54,7 +54,7 @@
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.desktopmode.DesktopMode;
+import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
@@ -190,8 +190,8 @@
     @Test
     public void testGetRecentTasks_groupActiveFreeformTasks() {
         StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
-                DesktopMode.class).startMocking();
-        when(DesktopMode.isActive(any())).thenReturn(true);
+                DesktopModeStatus.class).startMocking();
+        when(DesktopModeStatus.isActive(any())).thenReturn(true);
 
         ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
         ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
similarity index 96%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DisposableBroadcastReceiverAsUser.kt
rename to packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
index 1589b04..a7de4ce 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DisposableBroadcastReceiverAsUser.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.framework.compose
+package com.android.settingslib.spaprivileged.framework.compose
 
 import android.content.BroadcastReceiver
 import android.content.Context
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index 4222744..2111df5 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -237,6 +237,13 @@
     public ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData =
             new ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData();
 
+    /**
+     * Indicates that this task for the desktop tile in recents.
+     *
+     * Used when desktop mode feature is enabled.
+     */
+    public boolean desktopTile;
+
     public Task() {
         // Do nothing
     }
@@ -267,6 +274,7 @@
         this(other.key, other.colorPrimary, other.colorBackground, other.isDockable,
                 other.isLocked, other.taskDescription, other.topActivity);
         lastSnapshotData.set(other.lastSnapshotData);
+        desktopTile = other.desktopTile;
     }
 
     /**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index 6d12485..85278dd 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -62,6 +62,8 @@
     // See IRecentTasks.aidl
     public static final String KEY_EXTRA_RECENT_TASKS = "recent_tasks";
     public static final String KEY_EXTRA_SHELL_BACK_ANIMATION = "extra_shell_back_animation";
+    // See IDesktopMode.aidl
+    public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode";
 
     public static final String NAV_BAR_MODE_3BUTTON_OVERLAY =
             WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
index b78fa9a..71470e8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
@@ -57,11 +57,10 @@
     val faceLockedOut: Boolean,
     val fpLockedOut: Boolean,
     val goingToSleep: Boolean,
-    val keyguardAwakeExcludingBouncerShowing: Boolean,
+    val keyguardAwake: Boolean,
     val keyguardGoingAway: Boolean,
     val listeningForFaceAssistant: Boolean,
     val occludingAppRequestingFaceAuth: Boolean,
-    val onlyFaceEnrolled: Boolean,
     val primaryUser: Boolean,
     val scanningAllowedByStrongAuth: Boolean,
     val secureCameraLaunched: Boolean,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 4b6177a..f259a54 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2579,11 +2579,8 @@
         }
 
         final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED;
-        // mKeyguardIsVisible is true even when the bouncer is shown, we don't want to run face auth
-        // on bouncer if both fp and fingerprint are enrolled.
-        final boolean awakeKeyguardExcludingBouncerShowing = mKeyguardIsVisible
-                && mDeviceInteractive && !mGoingToSleep
-                && !statusBarShadeLocked && !mBouncerFullyShown;
+        final boolean awakeKeyguard = mKeyguardIsVisible && mDeviceInteractive && !mGoingToSleep
+                && !statusBarShadeLocked;
         final int user = getCurrentUser();
         final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(user);
         final boolean isLockDown =
@@ -2623,16 +2620,15 @@
         final boolean faceDisabledForUser = isFaceDisabled(user);
         final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user);
         final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant();
-        final boolean onlyFaceEnrolled = isOnlyFaceEnrolled();
         final boolean fpOrFaceIsLockedOut = isFaceLockedOut() || fpLockedout;
 
         // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an
         // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
         final boolean shouldListen =
-                ((mBouncerFullyShown && !mGoingToSleep && onlyFaceEnrolled)
+                (mBouncerFullyShown && !mGoingToSleep
                         || mAuthInterruptActive
                         || mOccludingAppRequestingFace
-                        || awakeKeyguardExcludingBouncerShowing
+                        || awakeKeyguard
                         || shouldListenForFaceAssistant
                         || mAuthController.isUdfpsFingerDown()
                         || mUdfpsBouncerShowing)
@@ -2658,11 +2654,10 @@
                     isFaceLockedOut(),
                     fpLockedout,
                     mGoingToSleep,
-                    awakeKeyguardExcludingBouncerShowing,
+                    awakeKeyguard,
                     mKeyguardGoingAway,
                     shouldListenForFaceAssistant,
                     mOccludingAppRequestingFace,
-                    onlyFaceEnrolled,
                     mIsPrimaryUser,
                     strongAuthAllowsScanning,
                     mSecureCameraLaunched,
@@ -2672,11 +2667,6 @@
         return shouldListen;
     }
 
-    private boolean isOnlyFaceEnrolled() {
-        return isFaceEnrolled()
-                && !getCachedIsUnlockWithFingerprintPossible(sCurrentUser);
-    }
-
     private void maybeLogListenerModelData(KeyguardListenModel model) {
         mLogger.logKeyguardListenerModel(model);
 
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 50c38e5..a21f45f 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -97,7 +97,8 @@
                     .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
                     .setRecentTasks(mWMComponent.getRecentTasks())
                     .setBackAnimation(mWMComponent.getBackAnimation())
-                    .setFloatingTasks(mWMComponent.getFloatingTasks());
+                    .setFloatingTasks(mWMComponent.getFloatingTasks())
+                    .setDesktopMode(mWMComponent.getDesktopMode());
 
             // Only initialize when not starting from tests since this currently initializes some
             // components that shouldn't be run in the test environment
@@ -117,7 +118,8 @@
                     .setStartingSurface(Optional.ofNullable(null))
                     .setRecentTasks(Optional.ofNullable(null))
                     .setBackAnimation(Optional.ofNullable(null))
-                    .setFloatingTasks(Optional.ofNullable(null));
+                    .setFloatingTasks(Optional.ofNullable(null))
+                    .setDesktopMode(Optional.ofNullable(null));
         }
         mSysUIComponent = builder.build();
         if (initializeComponents) {
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
index b8a0013..1f7021e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt
@@ -18,6 +18,7 @@
 
 import android.annotation.AnyThread
 import android.annotation.MainThread
+import android.app.Activity
 import android.app.AlertDialog
 import android.app.Dialog
 import android.app.PendingIntent
@@ -119,8 +120,16 @@
     }
 
     override fun closeDialogs() {
-        dialog?.dismiss()
-        dialog = null
+        val isActivityFinishing =
+            (activityContext as? Activity)?.let { it.isFinishing || it.isDestroyed }
+        if (isActivityFinishing == true) {
+            dialog = null
+            return
+        }
+        if (dialog?.isShowing == true) {
+            dialog?.dismiss()
+            dialog = null
+        }
     }
 
     override fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 7e30431..0d06c51 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -40,6 +40,7 @@
 import com.android.wm.shell.TaskViewFactory;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
+import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.onehanded.OneHanded;
@@ -113,6 +114,9 @@
         @BindsInstance
         Builder setFloatingTasks(Optional<FloatingTasks> f);
 
+        @BindsInstance
+        Builder setDesktopMode(Optional<DesktopMode> d);
+
         SysUIComponent build();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
index dd11549..096f969 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
@@ -30,6 +30,7 @@
 import com.android.wm.shell.dagger.TvWMShellModule;
 import com.android.wm.shell.dagger.WMShellModule;
 import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
 import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.onehanded.OneHanded;
@@ -112,4 +113,10 @@
 
     @WMSingleton
     Optional<FloatingTasks> getFloatingTasks();
+
+    /**
+     * Optional {@link DesktopMode} component for interacting with desktop mode.
+     */
+    @WMSingleton
+    Optional<DesktopMode> getDesktopMode();
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
index 21a51d1..c07d402 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
@@ -18,13 +18,21 @@
 
 import static com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent.DreamMediaEntryModule.DREAM_MEDIA_ENTRY_VIEW;
 import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_MEDIA_ENTRY_LAYOUT_PARAMS;
+import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN;
 
+import android.app.PendingIntent;
 import android.util.Log;
 import android.view.View;
 
+import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.media.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.ViewController;
 
 import javax.inject.Inject;
@@ -87,6 +95,15 @@
 
         private final DreamOverlayStateController mDreamOverlayStateController;
         private final MediaDreamComplication mMediaComplication;
+        private final MediaCarouselController mMediaCarouselController;
+
+        private final ActivityStarter mActivityStarter;
+        private final ActivityIntentHelper mActivityIntentHelper;
+        private final KeyguardStateController mKeyguardStateController;
+        private final NotificationLockscreenUserManager mLockscreenUserManager;
+
+        private final FeatureFlags mFeatureFlags;
+        private boolean mIsTapToOpenEnabled;
 
         private boolean mMediaComplicationAdded;
 
@@ -94,15 +111,28 @@
         DreamMediaEntryViewController(
                 @Named(DREAM_MEDIA_ENTRY_VIEW) View view,
                 DreamOverlayStateController dreamOverlayStateController,
-                MediaDreamComplication mediaComplication) {
+                MediaDreamComplication mediaComplication,
+                MediaCarouselController mediaCarouselController,
+                ActivityStarter activityStarter,
+                ActivityIntentHelper activityIntentHelper,
+                KeyguardStateController keyguardStateController,
+                NotificationLockscreenUserManager lockscreenUserManager,
+                FeatureFlags featureFlags) {
             super(view);
             mDreamOverlayStateController = dreamOverlayStateController;
             mMediaComplication = mediaComplication;
+            mMediaCarouselController = mediaCarouselController;
+            mActivityStarter = activityStarter;
+            mActivityIntentHelper = activityIntentHelper;
+            mKeyguardStateController = keyguardStateController;
+            mLockscreenUserManager = lockscreenUserManager;
+            mFeatureFlags = featureFlags;
             mView.setOnClickListener(this::onClickMediaEntry);
         }
 
         @Override
         protected void onViewAttached() {
+            mIsTapToOpenEnabled = mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN);
         }
 
         @Override
@@ -113,6 +143,31 @@
         private void onClickMediaEntry(View v) {
             if (DEBUG) Log.d(TAG, "media entry complication tapped");
 
+            if (mIsTapToOpenEnabled) {
+                final PendingIntent clickIntent =
+                        mMediaCarouselController.getCurrentVisibleMediaContentIntent();
+
+                if (clickIntent == null) {
+                    return;
+                }
+
+                // See StatusBarNotificationActivityStarter#onNotificationClicked
+                final boolean showOverLockscreen = mKeyguardStateController.isShowing()
+                        && mActivityIntentHelper.wouldShowOverLockscreen(clickIntent.getIntent(),
+                        mLockscreenUserManager.getCurrentUserId());
+
+                if (showOverLockscreen) {
+                    mActivityStarter.startActivity(clickIntent.getIntent(),
+                            /* dismissShade */ true,
+                            /* animationController */ null,
+                            /* showOverLockscreenWhenLocked */ true);
+                } else {
+                    mActivityStarter.postStartActivityDismissingKeyguard(clickIntent, null);
+                }
+
+                return;
+            }
+
             if (!mMediaComplicationAdded) {
                 addMediaComplication();
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 223d79a..54c1b28 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -200,7 +200,8 @@
     public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901);
     public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903);
     public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904);
-    public static final UnreleasedFlag MEDIA_DREAM_COMPLICATION = new UnreleasedFlag(905);
+    public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905);
+    public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906);
 
     // 1000 - dock
     public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING =
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index b36f33b..f1e54e0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.media
 
+import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
 import android.content.res.ColorStateList
@@ -945,6 +946,11 @@
         mediaManager.onSwipeToDismiss()
     }
 
+    fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
+        return MediaPlayerData.playerKeys()
+                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)?.data?.clickIntent
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.apply {
             println("keysNeedRemoval: $keysNeedRemoval")
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index b516689..a9e1a4d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -516,7 +516,7 @@
     abstract int getStopButtonVisibility();
 
     public CharSequence getStopButtonText() {
-        return mContext.getText(R.string.media_output_dialog_button_stop_casting);
+        return mContext.getText(R.string.keyboard_key_media_stop);
     }
 
     public void onStopButtonClick() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index cb6f5a7..fbd0079 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -108,7 +108,7 @@
 
     @Override
     public CharSequence getStopButtonText() {
-        int resId = R.string.media_output_dialog_button_stop_casting;
+        int resId = R.string.keyboard_key_media_stop;
         if (isBroadcastSupported() && mMediaOutputController.isPlaying()
                 && !mMediaOutputController.isBluetoothLeBroadcastEnabled()) {
             resId = R.string.media_output_broadcast;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
index dc1488e..53b4d43 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.media.dream;
 
-import static com.android.systemui.flags.Flags.MEDIA_DREAM_COMPLICATION;
+import static com.android.systemui.flags.Flags.DREAM_MEDIA_COMPLICATION;
 
 import android.content.Context;
 import android.util.Log;
@@ -77,7 +77,7 @@
         public void onMediaDataLoaded(@NonNull String key, @Nullable String oldKey,
                 @NonNull MediaData data, boolean immediately, int receivedSmartspaceCardLatency,
                 boolean isSsReactivated) {
-            if (!mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)) {
+            if (!mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)) {
                 return;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 95edb35..7e2a5c5 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -27,6 +27,7 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_RECENT_TASKS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_BACK_ANIMATION;
+import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_DESKTOP_MODE;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_FLOATING_TASKS;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_ONE_HANDED;
 import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SHELL_PIP;
@@ -110,6 +111,7 @@
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
 import com.android.systemui.statusbar.policy.CallbackController;
 import com.android.wm.shell.back.BackAnimation;
+import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.pip.Pip;
@@ -169,6 +171,7 @@
     private final KeyguardUnlockAnimationController mSysuiUnlockAnimationController;
     private final Optional<RecentTasks> mRecentTasks;
     private final Optional<BackAnimation> mBackAnimation;
+    private final Optional<DesktopMode> mDesktopModeOptional;
     private final UiEventLogger mUiEventLogger;
 
     private Region mActiveNavBarRegion;
@@ -488,6 +491,9 @@
             mBackAnimation.ifPresent((backAnimation) -> params.putBinder(
                     KEY_EXTRA_SHELL_BACK_ANIMATION,
                     backAnimation.createExternalInterface().asBinder()));
+            mDesktopModeOptional.ifPresent((desktopMode -> params.putBinder(
+                    KEY_EXTRA_SHELL_DESKTOP_MODE,
+                    desktopMode.createExternalInterface().asBinder())));
 
             try {
                 Log.d(TAG_OPS, "OverviewProxyService connected, initializing overview proxy");
@@ -573,6 +579,7 @@
             Optional<RecentTasks> recentTasks,
             Optional<BackAnimation> backAnimation,
             Optional<StartingSurface> startingSurface,
+            Optional<DesktopMode> desktopModeOptional,
             BroadcastDispatcher broadcastDispatcher,
             ShellTransitions shellTransitions,
             ScreenLifecycle screenLifecycle,
@@ -607,6 +614,7 @@
         mShellTransitions = shellTransitions;
         mRecentTasks = recentTasks;
         mBackAnimation = backAnimation;
+        mDesktopModeOptional = desktopModeOptional;
         mUiEventLogger = uiEventLogger;
 
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
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/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 1eafaf0..f7ce43b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -3348,7 +3348,8 @@
                 // lock screen where users can use the UDFPS affordance to enter the device
                 mStatusBarKeyguardViewManager.reset(true);
             } else if (mState == StatusBarState.KEYGUARD
-                    && !mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()) {
+                    && !mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()
+                    && isKeyguardSecure()) {
                 mStatusBarKeyguardViewManager.showGenericBouncer(true /* scrimmed */);
             }
         }
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/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
index 485a7e5..aca60c0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
@@ -86,13 +86,12 @@
     becauseCannotSkipBouncer = false,
     biometricSettingEnabledForUser = false,
     bouncerFullyShown = false,
-    onlyFaceEnrolled = false,
     faceAuthenticated = false,
     faceDisabled = false,
     faceLockedOut = false,
     fpLockedOut = false,
     goingToSleep = false,
-    keyguardAwakeExcludingBouncerShowing = false,
+    keyguardAwake = false,
     keyguardGoingAway = false,
     listeningForFaceAssistant = false,
     occludingAppRequestingFaceAuth = false,
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index e7e3f34..12d3d42 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -213,8 +213,6 @@
             mBiometricEnabledCallbackArgCaptor;
     @Captor
     private ArgumentCaptor<FaceManager.AuthenticationCallback> mAuthenticationCallbackCaptor;
-    @Captor
-    private ArgumentCaptor<CancellationSignal> mCancellationSignalCaptor;
 
     // Direct executor
     private final Executor mBackgroundExecutor = Runnable::run;
@@ -597,13 +595,11 @@
 
     @Test
     public void testTriesToAuthenticate_whenBouncer() {
-        fingerprintIsNotEnrolled();
-        faceAuthEnabled();
         setKeyguardBouncerVisibility(true);
 
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
-        verify(mFaceManager, atLeastOnce()).isHardwareDetected();
-        verify(mFaceManager, atLeastOnce()).hasEnrolledTemplates(anyInt());
+        verify(mFaceManager).isHardwareDetected();
+        verify(mFaceManager).hasEnrolledTemplates(anyInt());
     }
 
     @Test
@@ -1238,9 +1234,7 @@
     public void testShouldListenForFace_whenFaceIsAlreadyAuthenticated_returnsFalse()
             throws RemoteException {
         // Face auth should run when the following is true.
-        faceAuthEnabled();
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         keyguardNotGoingAway();
         currentUserIsPrimary();
         strongAuthNotRequired();
@@ -1267,7 +1261,7 @@
         mKeyguardUpdateMonitor =
                 new TestableKeyguardUpdateMonitor(mSpiedContext);
 
-        // Preconditions for face auth to run
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
         strongAuthNotRequired();
@@ -1284,7 +1278,7 @@
     @Test
     public void testShouldListenForFace_whenStrongAuthDoesNotAllowScanning_returnsFalse()
             throws RemoteException {
-        // Preconditions for face auth to run
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
         currentUserIsPrimary();
@@ -1305,11 +1299,8 @@
     @Test
     public void testShouldListenForFace_whenBiometricsDisabledForUser_returnsFalse()
             throws RemoteException {
-        // Preconditions for face auth to run
-        faceAuthEnabled();
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
         biometricsNotDisabledThroughDevicePolicyManager();
@@ -1329,11 +1320,9 @@
     @Test
     public void testShouldListenForFace_whenUserCurrentlySwitching_returnsFalse()
             throws RemoteException {
-        // Preconditions for face auth to run
-        faceAuthEnabled();
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
         biometricsNotDisabledThroughDevicePolicyManager();
@@ -1352,11 +1341,8 @@
     @Test
     public void testShouldListenForFace_whenSecureCameraLaunched_returnsFalse()
             throws RemoteException {
-        // Preconditions for face auth to run
-        faceAuthEnabled();
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
         biometricsNotDisabledThroughDevicePolicyManager();
@@ -1375,7 +1361,7 @@
     @Test
     public void testShouldListenForFace_whenOccludingAppRequestsFaceAuth_returnsTrue()
             throws RemoteException {
-        // Preconditions for face auth to run
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
         currentUserIsPrimary();
@@ -1398,8 +1384,7 @@
     @Test
     public void testShouldListenForFace_whenBouncerShowingAndDeviceIsAwake_returnsTrue()
             throws RemoteException {
-        // Preconditions for face auth to run
-        faceAuthEnabled();
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
@@ -1411,7 +1396,6 @@
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
 
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         mTestableLooper.processAllMessages();
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
@@ -1420,7 +1404,7 @@
     @Test
     public void testShouldListenForFace_whenAuthInterruptIsActive_returnsTrue()
             throws RemoteException {
-        // Preconditions for face auth to run
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
@@ -1446,7 +1430,6 @@
         biometricsNotDisabledThroughDevicePolicyManager();
         biometricsEnabledForCurrentUser();
         userNotCurrentlySwitching();
-        bouncerFullyVisible();
 
         statusBarShadeIsLocked();
         mTestableLooper.processAllMessages();
@@ -1460,9 +1443,6 @@
         keyguardIsVisible();
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
         statusBarShadeIsNotLocked();
-        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
-        bouncerNotFullyVisible();
-
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
     }
 
@@ -1524,44 +1504,6 @@
     }
 
     @Test
-    public void testBouncerVisibility_whenBothFingerprintAndFaceIsEnrolled_stopsFaceAuth()
-            throws RemoteException {
-        // Both fingerprint and face are enrolled by default
-        // Preconditions for face auth to run
-        keyguardNotGoingAway();
-        currentUserIsPrimary();
-        currentUserDoesNotHaveTrust();
-        biometricsNotDisabledThroughDevicePolicyManager();
-        biometricsEnabledForCurrentUser();
-        userNotCurrentlySwitching();
-        deviceNotGoingToSleep();
-        deviceIsInteractive();
-        statusBarShadeIsNotLocked();
-        keyguardIsVisible();
-
-        mTestableLooper.processAllMessages();
-        clearInvocations(mUiEventLogger);
-
-        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
-
-        mKeyguardUpdateMonitor.requestFaceAuth(true,
-                FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
-
-        verify(mFaceManager).authenticate(any(),
-                mCancellationSignalCaptor.capture(),
-                mAuthenticationCallbackCaptor.capture(),
-                any(),
-                anyInt(),
-                anyBoolean());
-        CancellationSignal cancelSignal = mCancellationSignalCaptor.getValue();
-
-        bouncerFullyVisible();
-        mTestableLooper.processAllMessages();
-
-        assertThat(cancelSignal.isCanceled()).isTrue();
-    }
-
-    @Test
     public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() {
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
@@ -1624,21 +1566,6 @@
                 .onAuthenticationError(FaceManager.FACE_ERROR_LOCKOUT_PERMANENT, "");
     }
 
-    private void faceAuthEnabled() {
-        // this ensures KeyguardUpdateMonitor updates the cached mIsFaceEnrolled flag using the
-        // face manager mock wire-up in setup()
-        mKeyguardUpdateMonitor.isFaceAuthEnabledForUser(mCurrentUserId);
-    }
-
-    private void fingerprintIsNotEnrolled() {
-        when(mFingerprintManager.hasEnrolledTemplates(mCurrentUserId)).thenReturn(false);
-        // This updates the cached fingerprint state.
-        // There is no straightforward API to update the fingerprint state.
-        // It currently works updates after enrollment changes because something else invokes
-        // startListeningForFingerprint(), which internally calls this method.
-        mKeyguardUpdateMonitor.isUnlockWithFingerprintPossible(mCurrentUserId);
-    }
-
     private void statusBarShadeIsNotLocked() {
         mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD);
     }
@@ -1745,10 +1672,6 @@
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
     }
 
-    private void bouncerNotFullyVisible() {
-        setKeyguardBouncerVisibility(false);
-    }
-
     private void bouncerFullyVisible() {
         setKeyguardBouncerVisibility(true);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
index bc94440..522b5b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
@@ -16,17 +16,28 @@
 
 package com.android.systemui.dreams.complication;
 
-import static org.mockito.Mockito.verify;
+import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN;
 
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.content.Intent;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.media.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -48,21 +59,52 @@
     @Mock
     private MediaDreamComplication mMediaComplication;
 
+    @Mock
+    private MediaCarouselController mMediaCarouselController;
+
+    @Mock
+    private ActivityStarter mActivityStarter;
+
+    @Mock
+    private ActivityIntentHelper mActivityIntentHelper;
+
+    @Mock
+    private KeyguardStateController mKeyguardStateController;
+
+    @Mock
+    private NotificationLockscreenUserManager mLockscreenUserManager;
+
+    @Mock
+    private FeatureFlags mFeatureFlags;
+
+    @Mock
+    private PendingIntent mPendingIntent;
+
+    private final Intent mIntent = new Intent("android.test.TEST_ACTION");
+    private final Integer mCurrentUserId = 99;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(false);
     }
 
     /**
      * Ensures clicking media entry chip adds/removes media complication.
      */
     @Test
-    public void testClick() {
+    public void testClickToOpenUMO() {
         final DreamMediaEntryComplication.DreamMediaEntryViewController viewController =
                 new DreamMediaEntryComplication.DreamMediaEntryViewController(
                         mView,
                         mDreamOverlayStateController,
-                        mMediaComplication);
+                        mMediaComplication,
+                        mMediaCarouselController,
+                        mActivityStarter,
+                        mActivityIntentHelper,
+                        mKeyguardStateController,
+                        mLockscreenUserManager,
+                        mFeatureFlags);
 
         final ArgumentCaptor<View.OnClickListener> clickListenerCaptor =
                 ArgumentCaptor.forClass(View.OnClickListener.class);
@@ -85,10 +127,90 @@
                 new DreamMediaEntryComplication.DreamMediaEntryViewController(
                         mView,
                         mDreamOverlayStateController,
-                        mMediaComplication);
+                        mMediaComplication,
+                        mMediaCarouselController,
+                        mActivityStarter,
+                        mActivityIntentHelper,
+                        mKeyguardStateController,
+                        mLockscreenUserManager,
+                        mFeatureFlags);
 
         viewController.onViewDetached();
         verify(mView).setSelected(false);
         verify(mDreamOverlayStateController).removeComplication(mMediaComplication);
     }
+
+    /**
+     * Ensures clicking media entry chip opens media when flag is set.
+     */
+    @Test
+    public void testClickToOpenMediaOverLockscreen() {
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(true);
+
+        when(mMediaCarouselController.getCurrentVisibleMediaContentIntent()).thenReturn(
+                mPendingIntent);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mPendingIntent.getIntent()).thenReturn(mIntent);
+        when(mLockscreenUserManager.getCurrentUserId()).thenReturn(mCurrentUserId);
+
+        final DreamMediaEntryComplication.DreamMediaEntryViewController viewController =
+                new DreamMediaEntryComplication.DreamMediaEntryViewController(
+                        mView,
+                        mDreamOverlayStateController,
+                        mMediaComplication,
+                        mMediaCarouselController,
+                        mActivityStarter,
+                        mActivityIntentHelper,
+                        mKeyguardStateController,
+                        mLockscreenUserManager,
+                        mFeatureFlags);
+        viewController.onViewAttached();
+
+        final ArgumentCaptor<View.OnClickListener> clickListenerCaptor =
+                ArgumentCaptor.forClass(View.OnClickListener.class);
+        verify(mView).setOnClickListener(clickListenerCaptor.capture());
+
+        when(mActivityIntentHelper.wouldShowOverLockscreen(mIntent, mCurrentUserId)).thenReturn(
+                true);
+
+        clickListenerCaptor.getValue().onClick(mView);
+        verify(mActivityStarter).startActivity(mIntent, true, null, true);
+    }
+
+    /**
+     * Ensures clicking media entry chip opens media when flag is set.
+     */
+    @Test
+    public void testClickToOpenMediaDismissingLockscreen() {
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(true);
+
+        when(mMediaCarouselController.getCurrentVisibleMediaContentIntent()).thenReturn(
+                mPendingIntent);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mPendingIntent.getIntent()).thenReturn(mIntent);
+        when(mLockscreenUserManager.getCurrentUserId()).thenReturn(mCurrentUserId);
+
+        final DreamMediaEntryComplication.DreamMediaEntryViewController viewController =
+                new DreamMediaEntryComplication.DreamMediaEntryViewController(
+                        mView,
+                        mDreamOverlayStateController,
+                        mMediaComplication,
+                        mMediaCarouselController,
+                        mActivityStarter,
+                        mActivityIntentHelper,
+                        mKeyguardStateController,
+                        mLockscreenUserManager,
+                        mFeatureFlags);
+        viewController.onViewAttached();
+
+        final ArgumentCaptor<View.OnClickListener> clickListenerCaptor =
+                ArgumentCaptor.forClass(View.OnClickListener.class);
+        verify(mView).setOnClickListener(clickListenerCaptor.capture());
+
+        when(mActivityIntentHelper.wouldShowOverLockscreen(mIntent, mCurrentUserId)).thenReturn(
+                false);
+
+        clickListenerCaptor.getValue().onClick(mView);
+        verify(mActivityStarter).postStartActivityDismissingKeyguard(mPendingIntent, null);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
index 5dd1cfc..e3e3b74 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media
 
+import android.app.PendingIntent
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
@@ -43,6 +44,7 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.Mockito.`when` as whenever
@@ -366,7 +368,7 @@
                 playerIndex,
                 mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
         )
-        assertEquals( playerIndex, 0)
+        assertEquals(playerIndex, 0)
 
         // Replaying the same media player one more time.
         // And check that the card stays in its position.
@@ -402,4 +404,44 @@
         visualStabilityCallback.value.onReorderingAllowed()
         assertEquals(true, result)
     }
+
+    @Test
+    fun testGetCurrentVisibleMediaContentIntent() {
+        val clickIntent1 = mock(PendingIntent::class.java)
+        val player1 = Triple("player1",
+                DATA.copy(clickIntent = clickIntent1),
+                1000L)
+        clock.setCurrentTimeMillis(player1.third)
+        MediaPlayerData.addMediaPlayer(player1.first,
+                player1.second.copy(notificationKey = player1.first),
+                panel, clock, isSsReactivated = false)
+
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
+
+        val clickIntent2 = mock(PendingIntent::class.java)
+        val player2 = Triple("player2",
+                DATA.copy(clickIntent = clickIntent2),
+                2000L)
+        clock.setCurrentTimeMillis(player2.third)
+        MediaPlayerData.addMediaPlayer(player2.first,
+                player2.second.copy(notificationKey = player2.first),
+                panel, clock, isSsReactivated = false)
+
+        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
+        // added to the front because it was active more recently.
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
+
+        val clickIntent3 = mock(PendingIntent::class.java)
+        val player3 = Triple("player3",
+                DATA.copy(clickIntent = clickIntent3),
+                500L)
+        clock.setCurrentTimeMillis(player3.third)
+        MediaPlayerData.addMediaPlayer(player3.first,
+                player3.second.copy(notificationKey = player3.first),
+                panel, clock, isSsReactivated = false)
+
+        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
+        // added to the end because it was active less recently.
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index 0bfc034..2f52950 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.media.dream;
 
-import static com.android.systemui.flags.Flags.MEDIA_DREAM_COMPLICATION;
+import static com.android.systemui.flags.Flags.DREAM_MEDIA_COMPLICATION;
 
 import static org.mockito.AdditionalMatchers.not;
 import static org.mockito.ArgumentMatchers.any;
@@ -68,7 +68,7 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
-        when(mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)).thenReturn(true);
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)).thenReturn(true);
     }
 
     @Test
@@ -137,7 +137,7 @@
 
     @Test
     public void testOnMediaDataLoaded_mediaComplicationDisabled_doesNotAddComplication() {
-        when(mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)).thenReturn(false);
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)).thenReturn(false);
 
         final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
                 mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index a4fc160..3aed167 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -147,7 +147,8 @@
         "android.hardware.boot-V1.0-java",
         "android.hardware.boot-V1.1-java",
         "android.hardware.boot-V1.2-java",
-        "android.hardware.broadcastradio-V2.0-java",
+        "android.hardware.broadcastradio-V2.0-java", // HIDL
+        "android.hardware.broadcastradio-V1-java", // AIDL
         "android.hardware.health-V1.0-java", // HIDL
         "android.hardware.health-V2.0-java", // HIDL
         "android.hardware.health-V2.1-java", // HIDL
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index b96d33c..4278b3e 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -22,9 +22,12 @@
 import android.annotation.Nullable;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.database.ContentObserver;
 import android.hardware.health.HealthInfo;
 import android.hardware.health.V2_1.BatteryCapacityLevel;
@@ -185,6 +188,17 @@
     private ArrayDeque<Bundle> mBatteryLevelsEventQueue;
     private long mLastBatteryLevelChangedSentMs;
 
+    private Bundle mBatteryChangedOptions = BroadcastOptions.makeRemovingMatchingFilter(
+            new IntentFilter(Intent.ACTION_BATTERY_CHANGED)).toBundle();
+    private Bundle mPowerConnectedOptions = BroadcastOptions.makeRemovingMatchingFilter(
+            new IntentFilter(Intent.ACTION_POWER_DISCONNECTED)).toBundle();
+    private Bundle mPowerDisconnectedOptions = BroadcastOptions.makeRemovingMatchingFilter(
+            new IntentFilter(Intent.ACTION_POWER_CONNECTED)).toBundle();
+    private Bundle mBatteryLowOptions = BroadcastOptions.makeRemovingMatchingFilter(
+            new IntentFilter(Intent.ACTION_BATTERY_OKAY)).toBundle();
+    private Bundle mBatteryOkayOptions = BroadcastOptions.makeRemovingMatchingFilter(
+            new IntentFilter(Intent.ACTION_BATTERY_LOW)).toBundle();
+
     private MetricsLogger mMetricsLogger;
 
     public BatteryService(Context context) {
@@ -606,7 +620,8 @@
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
+                        mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
+                                mPowerConnectedOptions);
                     }
                 });
             }
@@ -617,7 +632,8 @@
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
+                        mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
+                                mPowerDisconnectedOptions);
                     }
                 });
             }
@@ -630,7 +646,8 @@
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
+                        mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
+                                mBatteryLowOptions);
                     }
                 });
             } else if (mSentLowBatteryBroadcast &&
@@ -642,7 +659,8 @@
                 mHandler.post(new Runnable() {
                     @Override
                     public void run() {
-                        mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL);
+                        mContext.sendBroadcastAsUser(statusIntent, UserHandle.ALL, null,
+                                mBatteryOkayOptions);
                     }
                 });
             }
@@ -712,7 +730,8 @@
                     + ", info:" + mHealthInfo.toString());
         }
 
-        mHandler.post(() -> ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL));
+        mHandler.post(() -> ActivityManager.broadcastStickyIntent(intent, AppOpsManager.OP_NONE,
+                mBatteryChangedOptions, UserHandle.USER_ALL));
     }
 
     private void sendBatteryLevelChangedIntentLocked() {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 9d24e8e..50b47a6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -245,6 +245,7 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.PermissionInfo;
+import android.content.pm.PermissionMethod;
 import android.content.pm.ProcessInfo;
 import android.content.pm.ProviderInfo;
 import android.content.pm.ProviderInfoList;
@@ -4515,7 +4516,7 @@
         // Clean-up disabled broadcast receivers.
         for (int i = mBroadcastQueues.length - 1; i >= 0; i--) {
             mBroadcastQueues[i].cleanupDisabledPackageReceiversLocked(
-                    packageName, disabledClasses, userId, true);
+                    packageName, disabledClasses, userId);
         }
 
     }
@@ -4524,7 +4525,7 @@
         boolean didSomething = false;
         for (int i = mBroadcastQueues.length - 1; i >= 0; i--) {
             didSomething |= mBroadcastQueues[i].cleanupDisabledPackageReceiversLocked(
-                    null, null, userId, true);
+                    null, null, userId);
         }
         return didSomething;
     }
@@ -4660,7 +4661,7 @@
         if (doit) {
             for (i = mBroadcastQueues.length - 1; i >= 0; i--) {
                 didSomething |= mBroadcastQueues[i].cleanupDisabledPackageReceiversLocked(
-                        packageName, null, userId, doit);
+                        packageName, null, userId);
             }
         }
 
@@ -5963,6 +5964,12 @@
         }
     }
 
+    /**
+     * Allows if {@code pid} is {@link #MY_PID}, then denies if the {@code pid} has been denied
+     * provided non-{@code null} {@code permission} before. Otherwise calls into
+     * {@link ActivityManager#checkComponentPermission(String, int, int, boolean)}.
+     */
+    @PermissionMethod
     public static int checkComponentPermission(String permission, int pid, int uid,
             int owningUid, boolean exported) {
         if (pid == MY_PID) {
@@ -6009,6 +6016,7 @@
      * This can be called with or without the global lock held.
      */
     @Override
+    @PermissionMethod
     public int checkPermission(String permission, int pid, int uid) {
         if (permission == null) {
             return PackageManager.PERMISSION_DENIED;
@@ -6020,6 +6028,7 @@
      * Binder IPC calls go through the public entry point.
      * This can be called with or without the global lock held.
      */
+    @PermissionMethod
     int checkCallingPermission(String permission) {
         return checkPermission(permission,
                 Binder.getCallingPid(),
@@ -6029,6 +6038,7 @@
     /**
      * This can be called with or without the global lock held.
      */
+    @PermissionMethod
     void enforceCallingPermission(String permission, String func) {
         if (checkCallingPermission(permission)
                 == PackageManager.PERMISSION_GRANTED) {
@@ -6046,6 +6056,7 @@
     /**
      * This can be called with or without the global lock held.
      */
+    @PermissionMethod
     void enforcePermission(String permission, int pid, int uid, String func) {
         if (checkPermission(permission, pid, uid) == PackageManager.PERMISSION_GRANTED) {
             return;
@@ -17355,6 +17366,8 @@
                     bOptions.setTemporaryAppAllowlist(mInternal.getBootTimeTempAllowListDuration(),
                             TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
                             PowerExemptionManager.REASON_LOCALE_CHANGED, "");
+                    bOptions.setRemoveMatchingFilter(
+                            new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
                     broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, null,
                             null, null, OP_NONE, bOptions.toBundle(), false, false, MY_PID,
                             SYSTEM_UID, Binder.getCallingUid(), Binder.getCallingPid(),
diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java
index 2ebe0b4..3efb628 100644
--- a/services/core/java/com/android/server/am/BroadcastConstants.java
+++ b/services/core/java/com/android/server/am/BroadcastConstants.java
@@ -17,6 +17,7 @@
 package com.android.server.am;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
 import android.compat.annotation.Overridable;
@@ -24,6 +25,8 @@
 import android.database.ContentObserver;
 import android.os.Build;
 import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.util.KeyValueListParser;
 import android.util.Slog;
@@ -39,6 +42,9 @@
 public class BroadcastConstants {
     private static final String TAG = "BroadcastConstants";
 
+    // TODO: migrate remaining constants to be loaded from DeviceConfig
+    // TODO: migrate fg/bg values into single constants instance
+
     // Value element names within the Settings record
     static final String KEY_TIMEOUT = "bcast_timeout";
     static final String KEY_SLOW_TIME = "bcast_slow_time";
@@ -115,6 +121,35 @@
     // started its process can start a background activity.
     public long ALLOW_BG_ACTIVITY_START_TIMEOUT = DEFAULT_ALLOW_BG_ACTIVITY_START_TIMEOUT;
 
+    /**
+     * For {@link BroadcastQueueModernImpl}: Maximum number of process queues to
+     * dispatch broadcasts to simultaneously.
+     */
+    public int MAX_RUNNING_PROCESS_QUEUES = DEFAULT_MAX_RUNNING_PROCESS_QUEUES;
+    private static final int DEFAULT_MAX_RUNNING_PROCESS_QUEUES = 4;
+
+    /**
+     * For {@link BroadcastQueueModernImpl}: Maximum number of active broadcasts
+     * to dispatch to a "running" process queue before we retire them back to
+     * being "runnable" to give other processes a chance to run.
+     */
+    public int MAX_RUNNING_ACTIVE_BROADCASTS = DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS;
+    private static final int DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS = 16;
+
+    /**
+     * For {@link BroadcastQueueModernImpl}: Default delay to apply to normal
+     * broadcasts, giving a chance for debouncing of rapidly changing events.
+     */
+    public long DELAY_NORMAL_MILLIS = DEFAULT_DELAY_NORMAL_MILLIS;
+    private static final long DEFAULT_DELAY_NORMAL_MILLIS = 10_000 * Build.HW_TIMEOUT_MULTIPLIER;
+
+    /**
+     * For {@link BroadcastQueueModernImpl}: Default delay to apply to
+     * broadcasts targeting cached applications.
+     */
+    public long DELAY_CACHED_MILLIS = DEFAULT_DELAY_CACHED_MILLIS;
+    private static final long DEFAULT_DELAY_CACHED_MILLIS = 30_000 * Build.HW_TIMEOUT_MULTIPLIER;
+
     // Settings override tracking for this instance
     private String mSettingsKey;
     private SettingsObserver mSettingsObserver;
@@ -128,7 +163,7 @@
 
         @Override
         public void onChange(boolean selfChange) {
-            updateConstants();
+            updateSettingsConstants();
         }
     }
 
@@ -148,11 +183,15 @@
         mSettingsObserver = new SettingsObserver(handler);
         mResolver.registerContentObserver(Settings.Global.getUriFor(mSettingsKey),
                 false, mSettingsObserver);
+        updateSettingsConstants();
 
-        updateConstants();
+        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+                new HandlerExecutor(handler), this::updateDeviceConfigConstants);
+        updateDeviceConfigConstants(
+                DeviceConfig.getProperties(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER));
     }
 
-    private void updateConstants() {
+    private void updateSettingsConstants() {
         synchronized (mParser) {
             try {
                 mParser.setString(Settings.Global.getString(mResolver, mSettingsKey));
@@ -173,6 +212,17 @@
         }
     }
 
+    private void updateDeviceConfigConstants(@NonNull DeviceConfig.Properties properties) {
+        MAX_RUNNING_PROCESS_QUEUES = properties.getInt("bcast_max_running_process_queues",
+                DEFAULT_MAX_RUNNING_PROCESS_QUEUES);
+        MAX_RUNNING_ACTIVE_BROADCASTS = properties.getInt("bcast_max_running_active_broadcasts",
+                DEFAULT_MAX_RUNNING_ACTIVE_BROADCASTS);
+        DELAY_NORMAL_MILLIS = properties.getLong("bcast_delay_normal_millis",
+                DEFAULT_DELAY_NORMAL_MILLIS);
+        DELAY_CACHED_MILLIS = properties.getLong("bcast_delay_cached_millis",
+                DEFAULT_DELAY_CACHED_MILLIS);
+    }
+
     /**
      * Standard dumpsys support; invoked from BroadcastQueue dump
      */
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index f9fcc9e..77eefb4 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -21,6 +21,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UptimeMillisLong;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.util.IndentingPrintWriter;
 
@@ -28,6 +29,8 @@
 import com.android.internal.os.SomeArgs;
 
 import java.util.ArrayDeque;
+import java.util.Iterator;
+import java.util.Objects;
 
 /**
  * Queue of pending {@link BroadcastRecord} entries intended for delivery to a
@@ -40,21 +43,13 @@
  * Internally each queue consists of a pending broadcasts which are waiting to
  * be dispatched, and a single active broadcast which is currently being
  * dispatched.
+ * <p>
+ * This entire class is marked as {@code NotThreadSafe} since it's the
+ * responsibility of the caller to always interact with a relevant lock held.
  */
+// @NotThreadSafe
 class BroadcastProcessQueue {
-    /**
-     * Default delay to apply to background broadcasts, giving a chance for
-     * debouncing of rapidly changing events.
-     */
-    // TODO: shift hard-coded defaults to BroadcastConstants
-    private static final long DELAY_DEFAULT_MILLIS = 10_000;
-
-    /**
-     * Default delay to apply to broadcasts targeting cached applications.
-     */
-    // TODO: shift hard-coded defaults to BroadcastConstants
-    private static final long DELAY_CACHED_MILLIS = 30_000;
-
+    final @NonNull BroadcastConstants constants;
     final @NonNull String processName;
     final int uid;
 
@@ -78,6 +73,11 @@
     @Nullable ProcessRecord app;
 
     /**
+     * Track name to use for {@link Trace} events.
+     */
+    @Nullable String traceTrackName;
+
+    /**
      * Ordered collection of broadcasts that are waiting to be dispatched to
      * this process, as a pair of {@link BroadcastRecord} and the index into
      * {@link BroadcastRecord#receivers} that represents the receiver.
@@ -102,6 +102,12 @@
     private int mActiveCountSinceIdle;
 
     /**
+     * Flag indicating that the currently active broadcast is being dispatched
+     * was scheduled via a cold start.
+     */
+    private boolean mActiveViaColdStart;
+
+    /**
      * Count of {@link #mPending} broadcasts of these various flavors.
      */
     private int mCountForeground;
@@ -113,8 +119,10 @@
 
     private boolean mProcessCached;
 
-    public BroadcastProcessQueue(@NonNull String processName, int uid) {
-        this.processName = processName;
+    public BroadcastProcessQueue(@NonNull BroadcastConstants constants,
+            @NonNull String processName, int uid) {
+        this.constants = Objects.requireNonNull(constants);
+        this.processName = Objects.requireNonNull(processName);
         this.uid = uid;
     }
 
@@ -148,6 +156,51 @@
     }
 
     /**
+     * Functional interface that tests a {@link BroadcastRecord} that has been
+     * previously enqueued in {@link BroadcastProcessQueue}.
+     */
+    @FunctionalInterface
+    public interface BroadcastPredicate {
+        public boolean test(@NonNull BroadcastRecord r, int index);
+    }
+
+    /**
+     * Functional interface that consumes a {@link BroadcastRecord} that has
+     * been previously enqueued in {@link BroadcastProcessQueue}.
+     */
+    @FunctionalInterface
+    public interface BroadcastConsumer {
+        public void accept(@NonNull BroadcastRecord r, int index);
+    }
+
+    /**
+     * Remove any broadcasts matching the given predicate.
+     * <p>
+     * Predicates that choose to remove a broadcast <em>must</em> finish
+     * delivery of the matched broadcast, to ensure that situations like ordered
+     * broadcasts are handled consistently.
+     */
+    public boolean removeMatchingBroadcasts(@NonNull BroadcastPredicate predicate,
+            @NonNull BroadcastConsumer consumer) {
+        boolean didSomething = false;
+        final Iterator<SomeArgs> it = mPending.iterator();
+        while (it.hasNext()) {
+            final SomeArgs args = it.next();
+            final BroadcastRecord record = (BroadcastRecord) args.arg1;
+            final int index = args.argi1;
+            if (predicate.test(record, index)) {
+                consumer.accept(record, index);
+                args.recycle();
+                it.remove();
+                didSomething = true;
+            }
+        }
+        // TODO: also check any active broadcast once we have a better "nonce"
+        // representing each scheduled broadcast to avoid races
+        return didSomething;
+    }
+
+    /**
      * Update if this process is in the "cached" state, typically signaling that
      * broadcast dispatch should be paused or delayed.
      */
@@ -187,6 +240,14 @@
         return mActiveCountSinceIdle;
     }
 
+    public void setActiveViaColdStart(boolean activeViaColdStart) {
+        mActiveViaColdStart = activeViaColdStart;
+    }
+
+    public boolean getActiveViaColdStart() {
+        return mActiveViaColdStart;
+    }
+
     /**
      * Set the currently active broadcast to the next pending broadcast.
      */
@@ -197,6 +258,7 @@
         mActive = (BroadcastRecord) next.arg1;
         mActiveIndex = next.argi1;
         mActiveCountSinceIdle++;
+        mActiveViaColdStart = false;
         next.recycle();
         if (mActive.isForeground()) {
             mCountForeground--;
@@ -217,21 +279,55 @@
         mActive = null;
         mActiveIndex = 0;
         mActiveCountSinceIdle = 0;
+        mActiveViaColdStart = false;
     }
 
-    public void setActiveDeliveryState(int deliveryState) {
-        checkState(isActive(), "isActive");
-        mActive.setDeliveryState(mActiveIndex, deliveryState);
+    public void traceProcessStartingBegin() {
+        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                traceTrackName, toShortString() + " starting", hashCode());
     }
 
+    public void traceProcessRunningBegin() {
+        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                traceTrackName, toShortString() + " running", hashCode());
+    }
+
+    public void traceProcessEnd() {
+        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                traceTrackName, hashCode());
+    }
+
+    public void traceActiveBegin() {
+        final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
+        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                traceTrackName, mActive.toShortString() + " scheduled", cookie);
+    }
+
+    public void traceActiveEnd() {
+        final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
+        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                traceTrackName, cookie);
+    }
+
+    /**
+     * Return the broadcast being actively dispatched in this process.
+     */
     public @NonNull BroadcastRecord getActive() {
         checkState(isActive(), "isActive");
         return mActive;
     }
 
-    public @NonNull Object getActiveReceiver() {
+    /**
+     * Return the index into {@link BroadcastRecord#receivers} of the receiver
+     * being actively dispatched in this process.
+     */
+    public int getActiveIndex() {
         checkState(isActive(), "isActive");
-        return mActive.receivers.get(mActiveIndex);
+        return mActiveIndex;
+    }
+
+    public boolean isEmpty() {
+        return (mActive != null) && mPending.isEmpty();
     }
 
     public boolean isActive() {
@@ -257,7 +353,7 @@
         return mRunnableAt;
     }
 
-    private void invalidateRunnableAt() {
+    public void invalidateRunnableAt() {
         mRunnableAtInvalidated = true;
     }
 
@@ -267,7 +363,17 @@
     private void updateRunnableAt() {
         final SomeArgs next = mPending.peekFirst();
         if (next != null) {
-            final long runnableAt = ((BroadcastRecord) next.arg1).enqueueTime;
+            final BroadcastRecord r = (BroadcastRecord) next.arg1;
+            final int index = next.argi1;
+
+            // If our next broadcast is ordered, and we're not the next receiver
+            // in line, then we're not runnable at all
+            if (r.ordered && r.finishedCount != index) {
+                mRunnableAt = Long.MAX_VALUE;
+                return;
+            }
+
+            final long runnableAt = r.enqueueTime;
             if (mCountForeground > 0) {
                 mRunnableAt = runnableAt;
             } else if (mCountOrdered > 0) {
@@ -275,9 +381,9 @@
             } else if (mCountAlarm > 0) {
                 mRunnableAt = runnableAt;
             } else if (mProcessCached) {
-                mRunnableAt = runnableAt + DELAY_CACHED_MILLIS;
+                mRunnableAt = runnableAt + constants.DELAY_CACHED_MILLIS;
             } else {
-                mRunnableAt = runnableAt + DELAY_DEFAULT_MILLIS;
+                mRunnableAt = runnableAt + constants.DELAY_NORMAL_MILLIS;
             }
         } else {
             mRunnableAt = Long.MAX_VALUE;
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index b1be022..972a1ce 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -147,7 +147,7 @@
      */
     @GuardedBy("mService")
     public abstract boolean cleanupDisabledPackageReceiversLocked(@Nullable String packageName,
-            @Nullable Set<String> filterByClasses, int userId, boolean doit);
+            @Nullable Set<String> filterByClasses, int userId);
 
     /**
      * Quickly determine if this queue has broadcasts that are still waiting to
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index a980db1..28bd9c3 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -46,7 +46,6 @@
 import android.app.IApplicationThread;
 import android.app.RemoteServiceException.CannotDeliverBroadcastException;
 import android.app.usage.UsageEvents.Event;
-import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.IIntentReceiver;
@@ -1447,7 +1446,7 @@
         return null;
     }
 
-    private void logBootCompletedBroadcastCompletionLatencyIfPossible(BroadcastRecord r) {
+    static void logBootCompletedBroadcastCompletionLatencyIfPossible(BroadcastRecord r) {
         // Only log after last receiver.
         // In case of split BOOT_COMPLETED broadcast, make sure only call this method on the
         // last BroadcastRecord of the split broadcast which has non-null resultTo.
@@ -1509,19 +1508,12 @@
         if (targetPackage == null) {
             return;
         }
-        getUsageStatsManagerInternal().reportBroadcastDispatched(
+        mService.mUsageStatsService.reportBroadcastDispatched(
                 r.callingUid, targetPackage, UserHandle.of(r.userId),
                 r.options.getIdForResponseEvent(), SystemClock.elapsedRealtime(),
                 mService.getUidStateLocked(targetUid));
     }
 
-    @NonNull
-    private UsageStatsManagerInternal getUsageStatsManagerInternal() {
-        final UsageStatsManagerInternal usageStatsManagerInternal =
-                LocalServices.getService(UsageStatsManagerInternal.class);
-        return usageStatsManagerInternal;
-    }
-
     private void maybeAddAllowBackgroundActivityStartsToken(ProcessRecord proc, BroadcastRecord r) {
         if (r == null || proc == null || !r.allowBackgroundActivityStarts) {
             return;
@@ -1693,18 +1685,15 @@
     }
 
     public boolean cleanupDisabledPackageReceiversLocked(
-            String packageName, Set<String> filterByClasses, int userId, boolean doit) {
+            String packageName, Set<String> filterByClasses, int userId) {
         boolean didSomething = false;
         for (int i = mParallelBroadcasts.size() - 1; i >= 0; i--) {
             didSomething |= mParallelBroadcasts.get(i).cleanupDisabledPackageReceiversLocked(
-                    packageName, filterByClasses, userId, doit);
-            if (!doit && didSomething) {
-                return true;
-            }
+                    packageName, filterByClasses, userId, true);
         }
 
         didSomething |= mDispatcher.cleanupDisabledPackageReceiversLocked(packageName,
-                filterByClasses, userId, doit);
+                filterByClasses, userId, true);
 
         return didSomething;
     }
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 8dfb22e..a36a9f6 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -19,11 +19,21 @@
 import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY;
 import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE;
 
+import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED;
+import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD;
+import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM;
+import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__MANIFEST;
+import static com.android.internal.util.FrameworkStatsLog.BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__RUNTIME;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_BROADCAST;
 import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList;
 import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList;
+import static com.android.server.am.BroadcastRecord.deliveryStateToString;
+import static com.android.server.am.BroadcastRecord.getReceiverPackageName;
 import static com.android.server.am.BroadcastRecord.getReceiverProcessName;
 import static com.android.server.am.BroadcastRecord.getReceiverUid;
+import static com.android.server.am.BroadcastRecord.isDeliveryStateTerminal;
+import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER;
 import static com.android.server.am.OomAdjuster.OOM_ADJ_REASON_START_RECEIVER;
 
 import android.annotation.NonNull;
@@ -32,16 +42,23 @@
 import android.app.IApplicationThread;
 import android.app.RemoteServiceException.CannotDeliverBroadcastException;
 import android.app.UidObserver;
+import android.app.usage.UsageEvents.Event;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -49,7 +66,12 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
 import com.android.internal.os.TimeoutRecord;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.am.BroadcastProcessQueue.BroadcastConsumer;
+import com.android.server.am.BroadcastProcessQueue.BroadcastPredicate;
 import com.android.server.am.BroadcastRecord.DeliveryState;
 
 import java.io.FileDescriptor;
@@ -59,6 +81,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
+import java.util.function.Predicate;
 
 /**
  * Alternative {@link BroadcastQueue} implementation which pivots broadcasts to
@@ -68,6 +91,21 @@
  * {@link BroadcastProcessQueue} instance. Each queue has a concept of being
  * "runnable at" a particular time in the future, which supports arbitrarily
  * pausing or delaying delivery on a per-process basis.
+ * <p>
+ * To keep things easy to reason about, there is a <em>very strong</em>
+ * preference to have broadcast interactions flow through a consistent set of
+ * methods in this specific order:
+ * <ol>
+ * <li>{@link #updateRunnableList} promotes a per-process queue to be runnable
+ * when it has relevant pending broadcasts
+ * <li>{@link #updateRunningList} promotes a runnable queue to be running and
+ * schedules delivery of the first broadcast
+ * <li>{@link #scheduleReceiverColdLocked} requests any needed cold-starts, and
+ * results are reported back via {@link #onApplicationAttachedLocked}
+ * <li>{@link #scheduleReceiverWarmLocked} requests dispatch of the currently
+ * active broadcast to a running app, and results are reported back via
+ * {@link #finishReceiverLocked}
+ * </ol>
  */
 class BroadcastQueueModernImpl extends BroadcastQueue {
     BroadcastQueueModernImpl(ActivityManagerService service, Handler handler,
@@ -80,40 +118,27 @@
             BroadcastConstants fgConstants, BroadcastConstants bgConstants,
             BroadcastSkipPolicy skipPolicy, BroadcastHistory history) {
         super(service, handler, "modern", skipPolicy, history);
+
+        // For the moment, read agnostic constants from foreground
+        mConstants = Objects.requireNonNull(fgConstants);
         mFgConstants = Objects.requireNonNull(fgConstants);
         mBgConstants = Objects.requireNonNull(bgConstants);
+
         mLocalHandler = new Handler(handler.getLooper(), mLocalCallback);
+
+        // We configure runnable size only once at boot; it'd be too complex to
+        // try resizing dynamically at runtime
+        mRunning = new BroadcastProcessQueue[mConstants.MAX_RUNNING_PROCESS_QUEUES];
     }
 
-    // TODO: add support for ordered broadcasts
     // TODO: add support for replacing pending broadcasts
     // TODO: add support for merging pending broadcasts
 
-    // TODO: add trace points for debugging broadcast flows
-    // TODO: record broadcast state change timing statistics
-    // TODO: record historical broadcast statistics
+    // TODO: consider reordering foreground broadcasts within queue
 
-    // TODO: pause queues for apps involved in backup/restore
     // TODO: pause queues when background services are running
     // TODO: pause queues when processes are frozen
 
-    // TODO: clean up queues for removed apps
-
-    /**
-     * Maximum number of process queues to dispatch broadcasts to
-     * simultaneously.
-     */
-    // TODO: shift hard-coded defaults to BroadcastConstants
-    private static final int MAX_RUNNING_PROCESS_QUEUES = 4;
-
-    /**
-     * Maximum number of active broadcasts to dispatch to a "running" process
-     * queue before we retire them back to being "runnable" to give other
-     * processes a chance to run.
-     */
-    // TODO: shift hard-coded defaults to BroadcastConstants
-    private static final int MAX_RUNNING_ACTIVE_BROADCASTS = 16;
-
     /**
      * Map from UID to per-process broadcast queues. If a UID hosts more than
      * one process, each additional process is stored as a linked list using
@@ -136,11 +161,14 @@
     private BroadcastProcessQueue mRunnableHead = null;
 
     /**
-     * Collection of queues which are "running". This will never be larger than
-     * {@link #MAX_RUNNING_PROCESS_QUEUES}.
+     * Array of queues which are currently "running", which may have gaps that
+     * are {@code null}.
+     *
+     * @see #getRunningSize
+     * @see #getRunningIndexOf
      */
     @GuardedBy("mService")
-    private final ArrayList<BroadcastProcessQueue> mRunning = new ArrayList<>();
+    private final BroadcastProcessQueue[] mRunning;
 
     /**
      * Single queue which is "running" but is awaiting a cold start to be
@@ -156,11 +184,13 @@
     @GuardedBy("mService")
     private final ArrayList<CountDownLatch> mWaitingForIdle = new ArrayList<>();
 
+    private final BroadcastConstants mConstants;
     private final BroadcastConstants mFgConstants;
     private final BroadcastConstants mBgConstants;
 
     private static final int MSG_UPDATE_RUNNING_LIST = 1;
     private static final int MSG_DELIVERY_TIMEOUT = 2;
+    private static final int MSG_BG_ACTIVITY_START_TIMEOUT = 3;
 
     private void enqueueUpdateRunningList() {
         mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST);
@@ -184,11 +214,44 @@
                 }
                 return true;
             }
+            case MSG_BG_ACTIVITY_START_TIMEOUT: {
+                synchronized (mService) {
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    final ProcessRecord app = (ProcessRecord) args.arg1;
+                    final BroadcastRecord r = (BroadcastRecord) args.arg2;
+                    args.recycle();
+                    app.removeAllowBackgroundActivityStartsToken(r);
+                }
+                return true;
+            }
         }
         return false;
     };
 
     /**
+     * Return the total number of active queues contained inside
+     * {@link #mRunning}.
+     */
+    private int getRunningSize() {
+        int size = 0;
+        for (int i = 0; i < mRunning.length; i++) {
+            if (mRunning[i] != null) size++;
+        }
+        return size;
+    }
+
+    /**
+     * Return the first index of the given value contained inside
+     * {@link #mRunning}, otherwise {@code -1}.
+     */
+    private int getRunningIndexOf(@Nullable BroadcastProcessQueue test) {
+        for (int i = 0; i < mRunning.length; i++) {
+            if (mRunning[i] == test) return i;
+        }
+        return -1;
+    }
+
+    /**
      * Consider updating the list of "runnable" queues, specifically with
      * relation to the given queue.
      * <p>
@@ -198,7 +261,7 @@
      */
     @GuardedBy("mService")
     private void updateRunnableList(@NonNull BroadcastProcessQueue queue) {
-        if (mRunning.contains(queue)) {
+        if (getRunningIndexOf(queue) >= 0) {
             // Already running; they'll be reinserted into the runnable list
             // once they finish running, so no need to update them now
             return;
@@ -215,9 +278,7 @@
                         ? queue.runnableAtPrev.getRunnableAt() <= queue.getRunnableAt() : true;
                 final boolean nextHigher = (queue.runnableAtNext != null)
                         ? queue.runnableAtNext.getRunnableAt() >= queue.getRunnableAt() : true;
-                if (prevLower && nextHigher) {
-                    return;
-                } else {
+                if (!prevLower || !nextHigher) {
                     mRunnableHead = removeFromRunnableList(mRunnableHead, queue);
                     mRunnableHead = insertIntoRunnableList(mRunnableHead, queue);
                 }
@@ -227,20 +288,26 @@
         } else if (inQueue) {
             mRunnableHead = removeFromRunnableList(mRunnableHead, queue);
         }
+
+        // If app isn't running, and there's nothing in the queue, clean up
+        if (queue.isEmpty() && !queue.isProcessWarm()) {
+            removeProcessQueue(queue.processName, queue.uid);
+        }
     }
 
     /**
      * Consider updating the list of "running" queues.
      * <p>
      * This method can promote "runnable" queues to become "running", subject to
-     * a maximum of {@link #MAX_RUNNING_PROCESS_QUEUES} warm processes and only
-     * one pending cold-start.
+     * a maximum of {@link BroadcastConstants#MAX_RUNNING_PROCESS_QUEUES} warm
+     * processes and only one pending cold-start.
      */
     @GuardedBy("mService")
     private void updateRunningList() {
-        int avail = MAX_RUNNING_PROCESS_QUEUES - mRunning.size();
+        int avail = mRunning.length - getRunningSize();
         if (avail == 0) return;
 
+        final int cookie = traceBegin(TAG, "updateRunningList");
         final long now = SystemClock.uptimeMillis();
 
         // If someone is waiting to go idle, everything is runnable now
@@ -285,23 +352,35 @@
                     + " from runnable to running; process is " + queue.app);
 
             // Allocate this available permit and start running!
-            mRunning.add(queue);
+            final int queueIndex = getRunningIndexOf(null);
+            mRunning[queueIndex] = queue;
             avail--;
 
             // Remove ourselves from linked list of runnable things
             mRunnableHead = removeFromRunnableList(mRunnableHead, queue);
 
-            queue.makeActiveNextPending();
+            // Emit all trace events for this process into a consistent track
+            queue.traceTrackName = TAG + ".mRunning[" + queueIndex + "]";
 
-            // If we're already warm, schedule it; otherwise we'll wait for the
-            // cold start to circle back around
+            // If we're already warm, boost OOM adjust now; if cold we'll boost
+            // it after the app has been started
             if (processWarm) {
+                notifyStartedRunning(queue);
+            }
+
+            // If we're already warm, schedule next pending broadcast now;
+            // otherwise we'll wait for the cold start to circle back around
+            queue.makeActiveNextPending();
+            if (processWarm) {
+                queue.traceProcessRunningBegin();
                 scheduleReceiverWarmLocked(queue);
             } else {
+                queue.traceProcessStartingBegin();
                 scheduleReceiverColdLocked(queue);
             }
 
-            mService.enqueueOomAdjTargetLocked(queue.app);
+            // We've moved at least one process into running state above, so we
+            // need to kick off an OOM adjustment pass
             updateOomAdj = true;
 
             // Move to considering next runnable queue
@@ -316,6 +395,8 @@
             mWaitingForIdle.forEach((latch) -> latch.countDown());
             mWaitingForIdle.clear();
         }
+
+        traceEnd(TAG, cookie);
     }
 
     @Override
@@ -324,9 +405,13 @@
         if ((mRunningColdStart != null) && (mRunningColdStart.app == app)) {
             // We've been waiting for this app to cold start, and it's ready
             // now; dispatch its next broadcast and clear the slot
-            scheduleReceiverWarmLocked(mRunningColdStart);
+            final BroadcastProcessQueue queue = mRunningColdStart;
             mRunningColdStart = null;
 
+            queue.traceProcessEnd();
+            queue.traceProcessRunningBegin();
+            scheduleReceiverWarmLocked(queue);
+
             // We might be willing to kick off another cold start
             enqueueUpdateRunningList();
             didSomething = true;
@@ -366,6 +451,11 @@
                 finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
                 didSomething = true;
             }
+
+            // If queue has nothing else pending, consider cleaning it
+            if (queue.isEmpty()) {
+                updateRunnableList(queue);
+            }
         }
 
         return didSomething;
@@ -374,7 +464,7 @@
     @Override
     public int getPreferredSchedulingGroupLocked(@NonNull ProcessRecord app) {
         final BroadcastProcessQueue queue = getProcessQueue(app);
-        if ((queue != null) && mRunning.contains(queue)) {
+        if ((queue != null) && getRunningIndexOf(queue) >= 0) {
             return queue.getPreferredSchedulingGroupLocked();
         }
         return ProcessList.SCHED_GROUP_UNDEFINED;
@@ -385,6 +475,17 @@
         // TODO: handle empty receivers to deliver result immediately
         if (r.receivers == null) return;
 
+        final IntentFilter removeMatchingFilter = (r.options != null)
+                ? r.options.getRemoveMatchingFilter() : null;
+        if (removeMatchingFilter != null) {
+            final Predicate<Intent> removeMatching = removeMatchingFilter.asPredicate();
+            skipMatchingBroadcasts(QUEUE_PREDICATE_ANY, (testRecord, testReceiver) -> {
+                // We only allow caller to clear broadcasts they enqueued
+                return (testRecord.callingUid == r.callingUid)
+                        && removeMatching.test(testRecord.intent);
+            });
+        }
+
         r.enqueueTime = SystemClock.uptimeMillis();
         r.enqueueRealTime = SystemClock.elapsedRealtime();
         r.enqueueClockTime = System.currentTimeMillis();
@@ -399,11 +500,20 @@
         }
     }
 
+    /**
+     * Schedule the currently active broadcast on the given queue when we know
+     * the process is cold. This kicks off a cold start and will eventually call
+     * through to {@link #scheduleReceiverWarmLocked} once it's ready.
+     */
     private void scheduleReceiverColdLocked(@NonNull BroadcastProcessQueue queue) {
         checkState(queue.isActive(), "isActive");
 
+        // Remember that active broadcast was scheduled via a cold start
+        queue.setActiveViaColdStart(true);
+
         final BroadcastRecord r = queue.getActive();
-        final Object receiver = queue.getActiveReceiver();
+        final int index = queue.getActiveIndex();
+        final Object receiver = r.receivers.get(index);
 
         final ApplicationInfo info = ((ResolveInfo) receiver).activityInfo.applicationInfo;
         final ComponentName component = ((ResolveInfo) receiver).activityInfo.getComponentName();
@@ -421,18 +531,52 @@
         if (DEBUG_BROADCAST) logv("Scheduling " + r + " to cold " + queue);
         queue.app = mService.startProcessLocked(queue.processName, info, true, intentFlags,
                 hostingRecord, zygotePolicyFlags, allowWhileBooting, false);
-        if (queue.app == null) {
+        if (queue.app != null) {
+            notifyStartedRunning(queue);
+        } else {
             mRunningColdStart = null;
             finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
         }
     }
 
+    /**
+     * Schedule the currently active broadcast on the given queue when we know
+     * the process is warm.
+     * <p>
+     * There is a <em>very strong</em> preference to consistently handle all
+     * results by calling through to {@link #finishReceiverLocked}, both in the
+     * case where a broadcast is handled by a remote app, and the case where the
+     * broadcast was finished locally without the remote app being involved.
+     */
     private void scheduleReceiverWarmLocked(@NonNull BroadcastProcessQueue queue) {
         checkState(queue.isActive(), "isActive");
 
         final ProcessRecord app = queue.app;
         final BroadcastRecord r = queue.getActive();
-        final Object receiver = queue.getActiveReceiver();
+        final int index = queue.getActiveIndex();
+        final Object receiver = r.receivers.get(index);
+
+        // If someone already finished this broadcast, finish immediately
+        final int oldDeliveryState = getDeliveryState(r, index);
+        if (isDeliveryStateTerminal(oldDeliveryState)) {
+            finishReceiverLocked(queue, oldDeliveryState);
+            return;
+        }
+
+        // Consider additional cases where we'd want fo finish immediately
+        if (app.isInFullBackup()) {
+            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+            return;
+        }
+        if (mSkipPolicy.shouldSkip(r, receiver)) {
+            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+            return;
+        }
+        final Intent receiverIntent = r.getReceiverIntent(receiver);
+        if (receiverIntent == null) {
+            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
+            return;
+        }
 
         if (!r.timeoutExempt) {
             final long timeout = r.isForeground() ? mFgConstants.TIMEOUT : mBgConstants.TIMEOUT;
@@ -440,26 +584,33 @@
                     Message.obtain(mLocalHandler, MSG_DELIVERY_TIMEOUT, queue), timeout);
         }
 
-        // TODO: apply temp allowlist exemptions
-        // TODO: apply background activity launch exemptions
+        if (r.allowBackgroundActivityStarts) {
+            app.addOrUpdateAllowBackgroundActivityStartsToken(r, r.mBackgroundActivityStartsToken);
 
-        if (mSkipPolicy.shouldSkip(r, receiver)) {
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
-            return;
+            final long timeout = r.isForeground() ? mFgConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT
+                    : mBgConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT;
+            final SomeArgs args = SomeArgs.obtain();
+            args.arg1 = app;
+            args.arg2 = r;
+            mLocalHandler.sendMessageDelayed(
+                    Message.obtain(mLocalHandler, MSG_BG_ACTIVITY_START_TIMEOUT, args), timeout);
         }
 
-        final Intent receiverIntent = r.getReceiverIntent(receiver);
-        if (receiverIntent == null) {
-            finishReceiverLocked(queue, BroadcastRecord.DELIVERY_SKIPPED);
-            return;
+        if (r.options != null && r.options.getTemporaryAppAllowlistDuration() > 0) {
+            mService.tempAllowlistUidLocked(queue.uid,
+                    r.options.getTemporaryAppAllowlistDuration(),
+                    r.options.getTemporaryAppAllowlistReasonCode(), r.toShortString(),
+                    r.options.getTemporaryAppAllowlistType(), r.callingUid);
         }
 
         if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app);
+        setDeliveryState(queue, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED);
+
         final IApplicationThread thread = app.getThread();
         if (thread != null) {
             try {
-                queue.setActiveDeliveryState(BroadcastRecord.DELIVERY_SCHEDULED);
                 if (receiver instanceof BroadcastFilter) {
+                    notifyScheduleRegisteredReceiver(app, r, (BroadcastFilter) receiver);
                     thread.scheduleRegisteredReceiver(
                             ((BroadcastFilter) receiver).receiverList.receiver, receiverIntent,
                             r.resultCode, r.resultData, r.resultExtras, r.ordered, r.initialSticky,
@@ -471,26 +622,62 @@
                         finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED);
                     }
                 } else {
+                    notifyScheduleReceiver(app, r, (ResolveInfo) receiver);
                     thread.scheduleReceiver(receiverIntent, ((ResolveInfo) receiver).activityInfo,
                             null, r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId,
                             app.mState.getReportedProcState());
                 }
             } catch (RemoteException e) {
                 finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
-                synchronized (app.mService) {
-                    app.scheduleCrashLocked(TAG, CannotDeliverBroadcastException.TYPE_ID, null);
-                }
+                app.scheduleCrashLocked(TAG, CannotDeliverBroadcastException.TYPE_ID, null);
             }
         } else {
             finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE);
         }
     }
 
+    /**
+     * Schedule the final {@link BroadcastRecord#resultTo} delivery for an
+     * ordered broadcast; assumes the sender is still a warm process.
+     */
+    private void scheduleResultTo(@NonNull BroadcastRecord r) {
+        if ((r.callerApp == null) || (r.resultTo == null)) return;
+        final ProcessRecord app = r.callerApp;
+        final IApplicationThread thread = app.getThread();
+        if (thread != null) {
+            mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(
+                    app, OOM_ADJ_REASON_FINISH_RECEIVER);
+            try {
+                thread.scheduleRegisteredReceiver(r.resultTo, r.intent,
+                        r.resultCode, r.resultData, r.resultExtras, false, r.initialSticky,
+                        r.userId, app.mState.getReportedProcState());
+            } catch (RemoteException e) {
+                app.scheduleCrashLocked(TAG, CannotDeliverBroadcastException.TYPE_ID, null);
+            }
+        }
+    }
+
     @Override
     public boolean finishReceiverLocked(@NonNull ProcessRecord app, int resultCode,
             @Nullable String resultData, @Nullable Bundle resultExtras, boolean resultAbort,
             boolean waitForServices) {
         final BroadcastProcessQueue queue = getProcessQueue(app);
+        final BroadcastRecord r = queue.getActive();
+        r.resultCode = resultCode;
+        r.resultData = resultData;
+        r.resultExtras = resultExtras;
+        if (!r.isNoAbort()) {
+            r.resultAbort = resultAbort;
+        }
+
+        // When the caller aborted an ordered broadcast, we mark all remaining
+        // receivers as skipped
+        if (r.ordered && r.resultAbort) {
+            for (int i = r.finishedCount + 1; i < r.receivers.size(); i++) {
+                setDeliveryState(null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED);
+            }
+        }
+
         return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED);
     }
 
@@ -498,28 +685,26 @@
             @DeliveryState int deliveryState) {
         checkState(queue.isActive(), "isActive");
 
-        queue.setActiveDeliveryState(deliveryState);
+        final ProcessRecord app = queue.app;
+        final BroadcastRecord r = queue.getActive();
+        final int index = queue.getActiveIndex();
+        final Object receiver = r.receivers.get(index);
 
-        if (deliveryState != BroadcastRecord.DELIVERY_DELIVERED) {
-            Slog.w(TAG, "Delivery state of " + queue.getActive() + " to " + queue + " changed to "
-                    + BroadcastRecord.deliveryStateToString(deliveryState));
-        }
+        setDeliveryState(queue, r, index, receiver, deliveryState);
 
         if (deliveryState == BroadcastRecord.DELIVERY_TIMEOUT) {
-            if (queue.app != null && !queue.app.isDebugging()) {
+            if (app != null && !app.isDebugging()) {
                 mService.appNotResponding(queue.app, TimeoutRecord
-                        .forBroadcastReceiver("Broadcast of " + queue.getActive().toShortString()));
+                        .forBroadcastReceiver("Broadcast of " + r.toShortString()));
             }
         } else {
             mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT, queue);
         }
 
-        // TODO: if we're the last receiver of this broadcast, record to history
-
         // Even if we have more broadcasts, if we've made reasonable progress
         // and someone else is waiting, retire ourselves to avoid starvation
         final boolean shouldRetire = (mRunnableHead != null)
-                && (queue.getActiveCountSinceIdle() > MAX_RUNNING_ACTIVE_BROADCASTS);
+                && (queue.getActiveCountSinceIdle() > mConstants.MAX_RUNNING_ACTIVE_BROADCASTS);
 
         if (queue.isRunnable() && queue.isProcessWarm() && !shouldRetire) {
             // We're on a roll; move onto the next broadcast for this process
@@ -529,21 +714,150 @@
         } else {
             // We've drained running broadcasts; maybe move back to runnable
             queue.makeActiveIdle();
-            mRunning.remove(queue);
-            // App is no longer running a broadcast, so update its OOM
-            // adjust during our next pass; no need for an immediate update
-            mService.enqueueOomAdjTargetLocked(queue.app);
+            queue.traceProcessEnd();
+
+            final int queueIndex = getRunningIndexOf(queue);
+            mRunning[queueIndex] = null;
             updateRunnableList(queue);
             enqueueUpdateRunningList();
+
+            // Tell other OS components that app is not actively running, giving
+            // a chance to update OOM adjustment
+            notifyStoppedRunning(queue);
             return false;
         }
     }
 
+    /**
+     * Set the delivery state on the given broadcast, then apply any additional
+     * bookkeeping related to ordered broadcasts.
+     */
+    private void setDeliveryState(@Nullable BroadcastProcessQueue queue,
+            @NonNull BroadcastRecord r, int index, @NonNull Object receiver,
+            @DeliveryState int newDeliveryState) {
+        final int oldDeliveryState = getDeliveryState(r, index);
+
+        if (newDeliveryState != BroadcastRecord.DELIVERY_DELIVERED) {
+            Slog.w(TAG, "Delivery state of " + r + " to " + receiver + " changed from "
+                    + deliveryStateToString(oldDeliveryState) + " to "
+                    + deliveryStateToString(newDeliveryState));
+        }
+
+        r.setDeliveryState(index, newDeliveryState);
+
+        // Emit any relevant tracing results when we're changing the delivery
+        // state as part of running from a queue
+        if (queue != null) {
+            if (newDeliveryState == BroadcastRecord.DELIVERY_SCHEDULED) {
+                queue.traceActiveBegin();
+            } else if ((oldDeliveryState == BroadcastRecord.DELIVERY_SCHEDULED)
+                    && isDeliveryStateTerminal(newDeliveryState)) {
+                queue.traceActiveEnd();
+            }
+        }
+
+        // If we're moving into a terminal state, we might have internal
+        // bookkeeping to update for ordered broadcasts
+        if (!isDeliveryStateTerminal(oldDeliveryState)
+                && isDeliveryStateTerminal(newDeliveryState)) {
+            r.finishedCount++;
+            notifyFinishReceiver(queue, r, index, receiver);
+
+            if (r.ordered) {
+                if (r.finishedCount < r.receivers.size()) {
+                    // We just finished an ordered receiver, which means the
+                    // next receiver might now be runnable
+                    final Object nextReceiver = r.receivers.get(r.finishedCount);
+                    final BroadcastProcessQueue nextQueue = getProcessQueue(
+                            getReceiverProcessName(nextReceiver), getReceiverUid(nextReceiver));
+                    nextQueue.invalidateRunnableAt();
+                    updateRunnableList(nextQueue);
+                } else {
+                    // Everything finished, so deliver final result
+                    scheduleResultTo(r);
+                }
+            }
+        }
+    }
+
+    private @DeliveryState int getDeliveryState(@NonNull BroadcastRecord r, int index) {
+        return r.getDeliveryState(index);
+    }
+
     @Override
-    public boolean cleanupDisabledPackageReceiversLocked(String packageName,
-            Set<String> filterByClasses, int userId, boolean doit) {
-        // TODO: implement
-        return false;
+    public boolean cleanupDisabledPackageReceiversLocked(@Nullable String packageName,
+            @Nullable Set<String> filterByClasses, int userId) {
+        final Predicate<BroadcastProcessQueue> queuePredicate;
+        final BroadcastPredicate broadcastPredicate;
+        if (packageName != null) {
+            // Caller provided a package and user ID, so we're focused on queues
+            // belonging to a specific UID
+            final int uid = mService.mPackageManagerInt.getPackageUid(
+                    packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
+            queuePredicate = (q) -> {
+                return q.uid == uid;
+            };
+
+            // If caller provided a set of classes, filter to skip only those;
+            // otherwise we skip all broadcasts
+            if (filterByClasses != null) {
+                broadcastPredicate = (r, i) -> {
+                    final Object receiver = r.receivers.get(i);
+                    if (receiver instanceof ResolveInfo) {
+                        final ActivityInfo info = ((ResolveInfo) receiver).activityInfo;
+                        return packageName.equals(info.packageName)
+                                && filterByClasses.contains(info.name);
+                    } else {
+                        return false;
+                    }
+                };
+            } else {
+                broadcastPredicate = (r, i) -> {
+                    final Object receiver = r.receivers.get(i);
+                    return packageName.equals(getReceiverPackageName(receiver));
+                };
+            }
+        } else {
+            // Caller is cleaning up an entire user ID; skip all broadcasts
+            queuePredicate = (q) -> {
+                return UserHandle.getUserId(q.uid) == userId;
+            };
+            broadcastPredicate = BROADCAST_PREDICATE_ANY;
+        }
+        return skipMatchingBroadcasts(queuePredicate, broadcastPredicate);
+    }
+
+    private static final Predicate<BroadcastProcessQueue> QUEUE_PREDICATE_ANY =
+            (q) -> true;
+    private static final BroadcastPredicate BROADCAST_PREDICATE_ANY =
+            (r, i) -> true;
+
+    /**
+     * Typical consumer that will skip the given broadcast, usually as a result
+     * of it matching a predicate.
+     */
+    private final BroadcastConsumer mBroadcastConsumerSkip = (r, i) -> {
+        setDeliveryState(null, r, i, r.receivers.get(i), BroadcastRecord.DELIVERY_SKIPPED);
+    };
+
+    private boolean skipMatchingBroadcasts(
+            @NonNull Predicate<BroadcastProcessQueue> queuePredicate,
+            @NonNull BroadcastPredicate broadcastPredicate) {
+        // Note that we carefully preserve any "skipped" broadcasts in their
+        // queues so that we follow our normal flow for "finishing" a broadcast,
+        // which is where we handle things like ordered broadcasts.
+        boolean didSomething = false;
+        for (int i = 0; i < mProcessQueues.size(); i++) {
+            BroadcastProcessQueue leaf = mProcessQueues.valueAt(i);
+            while (leaf != null) {
+                if (queuePredicate.test(leaf)) {
+                    didSomething |= leaf.removeMatchingBroadcasts(broadcastPredicate,
+                            mBroadcastConsumerSkip);
+                }
+                leaf = leaf.processNameNext;
+            }
+        }
+        return didSomething;
     }
 
     @Override
@@ -569,7 +883,7 @@
 
     @Override
     public boolean isIdleLocked() {
-        return (mRunnableHead == null) && mRunning.isEmpty();
+        return (mRunnableHead == null) && (getRunningSize() == 0);
     }
 
     @Override
@@ -594,7 +908,7 @@
 
     @Override
     public String describeStateLocked() {
-        return mRunning.size() + " running";
+        return getRunningSize() + " running";
     }
 
     @Override
@@ -608,17 +922,179 @@
         // TODO: implement
     }
 
+    private int traceBegin(String trackName, String methodName) {
+        final int cookie = methodName.hashCode();
+        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                trackName, methodName, cookie);
+        return cookie;
+    }
+
+    private void traceEnd(String trackName, int cookie) {
+        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+                trackName, cookie);
+    }
+
     private void updateWarmProcess(@NonNull BroadcastProcessQueue queue) {
         if (!queue.isProcessWarm()) {
             queue.app = mService.getProcessRecordLocked(queue.processName, queue.uid);
         }
     }
 
-    private @NonNull BroadcastProcessQueue getOrCreateProcessQueue(@NonNull ProcessRecord app) {
+    /**
+     * Inform other parts of OS that the given broadcast queue has started
+     * running, typically for internal bookkeeping.
+     */
+    private void notifyStartedRunning(@NonNull BroadcastProcessQueue queue) {
+        if (queue.app != null) {
+            queue.app.mReceivers.incrementCurReceivers();
+
+            queue.app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
+
+            // Don't bump its LRU position if it's in the background restricted.
+            if (mService.mInternal.getRestrictionLevel(
+                    queue.uid) < ActivityManager.RESTRICTION_LEVEL_RESTRICTED_BUCKET) {
+                mService.updateLruProcessLocked(queue.app, false, null);
+            }
+
+            mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(queue.app,
+                    OOM_ADJ_REASON_START_RECEIVER);
+
+            mService.enqueueOomAdjTargetLocked(queue.app);
+        }
+    }
+
+    /**
+     * Inform other parts of OS that the given broadcast queue has stopped
+     * running, typically for internal bookkeeping.
+     */
+    private void notifyStoppedRunning(@NonNull BroadcastProcessQueue queue) {
+        if (queue.app != null) {
+            // Update during our next pass; no need for an immediate update
+            mService.enqueueOomAdjTargetLocked(queue.app);
+
+            queue.app.mReceivers.decrementCurReceivers();
+        }
+    }
+
+    /**
+     * Inform other parts of OS that the given broadcast was just scheduled for
+     * a registered receiver, typically for internal bookkeeping.
+     */
+    private void notifyScheduleRegisteredReceiver(@NonNull ProcessRecord app,
+            @NonNull BroadcastRecord r, @NonNull BroadcastFilter receiver) {
+        reportUsageStatsBroadcastDispatched(app, r);
+    }
+
+    /**
+     * Inform other parts of OS that the given broadcast was just scheduled for
+     * a manifest receiver, typically for internal bookkeeping.
+     */
+    private void notifyScheduleReceiver(@NonNull ProcessRecord app,
+            @NonNull BroadcastRecord r, @NonNull ResolveInfo receiver) {
+        reportUsageStatsBroadcastDispatched(app, r);
+
+        final String receiverPackageName = receiver.activityInfo.packageName;
+        app.addPackage(receiverPackageName,
+                receiver.activityInfo.applicationInfo.longVersionCode, mService.mProcessStats);
+
+        final boolean targetedBroadcast = r.intent.getComponent() != null;
+        final boolean targetedSelf = Objects.equals(r.callerPackage, receiverPackageName);
+        if (targetedBroadcast && !targetedSelf) {
+            mService.mUsageStatsService.reportEvent(receiverPackageName,
+                    r.userId, Event.APP_COMPONENT_USED);
+        }
+
+        mService.notifyPackageUse(receiverPackageName,
+                PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER);
+
+        mService.mPackageManagerInt.setPackageStoppedState(
+                receiverPackageName, false, r.userId);
+    }
+
+    private void reportUsageStatsBroadcastDispatched(@NonNull ProcessRecord app,
+            @NonNull BroadcastRecord r) {
+        final long idForResponseEvent = (r.options != null)
+                ? r.options.getIdForResponseEvent() : 0L;
+        if (idForResponseEvent <= 0) return;
+
+        final String targetPackage;
+        if (r.intent.getPackage() != null) {
+            targetPackage = r.intent.getPackage();
+        } else if (r.intent.getComponent() != null) {
+            targetPackage = r.intent.getComponent().getPackageName();
+        } else {
+            targetPackage = null;
+        }
+        if (targetPackage == null) return;
+
+        mService.mUsageStatsService.reportBroadcastDispatched(r.callingUid, targetPackage,
+                UserHandle.of(r.userId), idForResponseEvent, SystemClock.elapsedRealtime(),
+                mService.getUidStateLocked(app.uid));
+    }
+
+    /**
+     * Inform other parts of OS that the given broadcast was just finished,
+     * typically for internal bookkeeping.
+     */
+    private void notifyFinishReceiver(@Nullable BroadcastProcessQueue queue,
+            @NonNull BroadcastRecord r, int index, @NonNull Object receiver) {
+        // Report statistics for each individual receiver
+        final int uid = getReceiverUid(receiver);
+        final int senderUid = (r.callingUid == -1) ? Process.SYSTEM_UID : r.callingUid;
+        final String actionName = ActivityManagerService.getShortAction(r.intent.getAction());
+        final int receiverType = (receiver instanceof BroadcastFilter)
+                ? BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__RUNTIME
+                : BROADCAST_DELIVERY_EVENT_REPORTED__RECEIVER_TYPE__MANIFEST;
+        final int type;
+        if (queue == null) {
+            type = BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_UNKNOWN;
+        } else if (queue.getActiveViaColdStart()) {
+            type = BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_COLD;
+        } else {
+            type = BROADCAST_DELIVERY_EVENT_REPORTED__PROC_START_TYPE__PROCESS_START_TYPE_WARM;
+        }
+        // With the new per-process queues, there's no delay between being
+        // "dispatched" and "scheduled", so we report no "receive delay"
+        final long dispatchDelay = r.scheduledTime[index] - r.enqueueTime;
+        final long receiveDelay = 0;
+        final long finishDelay = r.duration[index];
+        FrameworkStatsLog.write(BROADCAST_DELIVERY_EVENT_REPORTED, uid, senderUid, actionName,
+                receiverType, type, dispatchDelay, receiveDelay, finishDelay);
+
+        final boolean recordFinished = (r.finishedCount == r.receivers.size());
+        if (recordFinished) {
+            mHistory.addBroadcastToHistoryLocked(r);
+
+            r.nextReceiver = r.receivers.size();
+            BroadcastQueueImpl.logBootCompletedBroadcastCompletionLatencyIfPossible(r);
+
+            if (r.intent.getComponent() == null && r.intent.getPackage() == null
+                    && (r.intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
+                int manifestCount = 0;
+                int manifestSkipCount = 0;
+                for (int i = 0; i < r.receivers.size(); i++) {
+                    if (r.receivers.get(i) instanceof ResolveInfo) {
+                        manifestCount++;
+                        if (r.delivery[i] == BroadcastRecord.DELIVERY_SKIPPED) {
+                            manifestSkipCount++;
+                        }
+                    }
+                }
+
+                final long dispatchTime = SystemClock.uptimeMillis() - r.enqueueTime;
+                mService.addBroadcastStatLocked(r.intent.getAction(), r.callerPackage,
+                        manifestCount, manifestSkipCount, dispatchTime);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    @NonNull BroadcastProcessQueue getOrCreateProcessQueue(@NonNull ProcessRecord app) {
         return getOrCreateProcessQueue(app.processName, app.info.uid);
     }
 
-    private @NonNull BroadcastProcessQueue getOrCreateProcessQueue(@NonNull String processName,
+    @VisibleForTesting
+    @NonNull BroadcastProcessQueue getOrCreateProcessQueue(@NonNull String processName,
             int uid) {
         BroadcastProcessQueue leaf = mProcessQueues.get(uid);
         while (leaf != null) {
@@ -630,7 +1106,7 @@
             leaf = leaf.processNameNext;
         }
 
-        BroadcastProcessQueue created = new BroadcastProcessQueue(processName, uid);
+        BroadcastProcessQueue created = new BroadcastProcessQueue(mConstants, processName, uid);
         created.app = mService.getProcessRecordLocked(processName, uid);
 
         if (leaf == null) {
@@ -641,11 +1117,13 @@
         return created;
     }
 
-    private @Nullable BroadcastProcessQueue getProcessQueue(@NonNull ProcessRecord app) {
+    @VisibleForTesting
+    @Nullable BroadcastProcessQueue getProcessQueue(@NonNull ProcessRecord app) {
         return getProcessQueue(app.processName, app.info.uid);
     }
 
-    private @Nullable BroadcastProcessQueue getProcessQueue(@NonNull String processName, int uid) {
+    @VisibleForTesting
+    @Nullable BroadcastProcessQueue getProcessQueue(@NonNull String processName, int uid) {
         BroadcastProcessQueue leaf = mProcessQueues.get(uid);
         while (leaf != null) {
             if (Objects.equals(leaf.processName, processName)) {
@@ -656,6 +1134,35 @@
         return null;
     }
 
+    @VisibleForTesting
+    @Nullable BroadcastProcessQueue removeProcessQueue(@NonNull ProcessRecord app) {
+        return removeProcessQueue(app.processName, app.info.uid);
+    }
+
+    @VisibleForTesting
+    @Nullable BroadcastProcessQueue removeProcessQueue(@NonNull String processName,
+            int uid) {
+        BroadcastProcessQueue prev = null;
+        BroadcastProcessQueue leaf = mProcessQueues.get(uid);
+        while (leaf != null) {
+            if (Objects.equals(leaf.processName, processName)) {
+                if (prev != null) {
+                    prev.processNameNext = leaf.processNameNext;
+                } else {
+                    if (leaf.processNameNext != null) {
+                        mProcessQueues.put(uid, leaf.processNameNext);
+                    } else {
+                        mProcessQueues.remove(uid);
+                    }
+                }
+                return leaf;
+            }
+            prev = leaf;
+            leaf = leaf.processNameNext;
+        }
+        return null;
+    }
+
     @Override
     public void dumpDebug(@NonNull ProtoOutputStream proto, long fieldId) {
         long token = proto.start(fieldId);
@@ -703,16 +1210,16 @@
         ipw.println();
         ipw.println("🏃 Running:");
         ipw.increaseIndent();
-        if (mRunning.isEmpty()) {
-            ipw.println("(none)");
-        } else {
-            for (BroadcastProcessQueue queue : mRunning) {
-                if (queue == mRunningColdStart) {
-                    ipw.print("🥶 ");
-                } else {
-                    ipw.print("\u3000 ");
-                }
+        for (BroadcastProcessQueue queue : mRunning) {
+            if ((queue != null) && (queue == mRunningColdStart)) {
+                ipw.print("🥶 ");
+            } else {
+                ipw.print("\u3000 ");
+            }
+            if (queue != null) {
                 ipw.println(queue.toShortString());
+            } else {
+                ipw.println("(none)");
             }
         }
         ipw.decreaseIndent();
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index 16eeb7bf..ae7f2a5 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -23,7 +23,9 @@
 import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_CHANGE_ID;
 import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_NONE;
 import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROADCAST_TARGET_T_ONLY;
+import static com.android.server.am.BroadcastQueue.checkState;
 
+import android.annotation.DurationMillisLong;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -111,6 +113,7 @@
     int anrCount;           // has this broadcast record hit any ANRs?
     int manifestCount;      // number of manifest receivers dispatched.
     int manifestSkipCount;  // number of manifest receivers skipped.
+    int finishedCount;      // number of receivers finished.
     BroadcastQueue queue;   // the outbound queue handling this broadcast
 
     // if set to true, app's process will be temporarily allowed to start activities from background
@@ -167,6 +170,22 @@
         }
     }
 
+    /**
+     * Return if the given delivery state is "terminal", where no additional
+     * delivery state changes will be made.
+     */
+    static boolean isDeliveryStateTerminal(@DeliveryState int deliveryState) {
+        switch (deliveryState) {
+            case DELIVERY_DELIVERED:
+            case DELIVERY_SKIPPED:
+            case DELIVERY_TIMEOUT:
+            case DELIVERY_FAILURE:
+                return true;
+            default:
+                return false;
+        }
+    }
+
     ProcessRecord curApp;       // hosting application of current receiver.
     ComponentName curComponent; // the receiver class that is currently running.
     ActivityInfo curReceiver;   // the manifest receiver that is currently running.
@@ -545,6 +564,10 @@
         }
     }
 
+    @DeliveryState int getDeliveryState(int index) {
+        return delivery[index];
+    }
+
     boolean isForeground() {
         return (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0;
     }
@@ -553,6 +576,10 @@
         return (intent.getFlags() & Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
     }
 
+    boolean isNoAbort() {
+        return (intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0;
+    }
+
     @NonNull String getHostingRecordTriggerType() {
         if (alarm) {
             return HostingRecord.TRIGGER_TYPE_ALARM;
@@ -606,7 +633,7 @@
         }
     }
 
-    static String getReceiverProcessName(@NonNull Object receiver) {
+    static @NonNull String getReceiverProcessName(@NonNull Object receiver) {
         if (receiver instanceof BroadcastFilter) {
             return ((BroadcastFilter) receiver).receiverList.app.processName;
         } else /* if (receiver instanceof ResolveInfo) */ {
@@ -614,6 +641,14 @@
         }
     }
 
+    static @NonNull String getReceiverPackageName(@NonNull Object receiver) {
+        if (receiver instanceof BroadcastFilter) {
+            return ((BroadcastFilter) receiver).receiverList.app.info.packageName;
+        } else /* if (receiver instanceof ResolveInfo) */ {
+            return ((ResolveInfo) receiver).activityInfo.packageName;
+        }
+    }
+
     public BroadcastRecord maybeStripForHistory() {
         if (!intent.canStripForHistory()) {
             return this;
@@ -665,13 +700,21 @@
 
     @Override
     public String toString() {
+        String label = intent.getAction();
+        if (label == null) {
+            label = intent.toString();
+        }
         return "BroadcastRecord{"
             + Integer.toHexString(System.identityHashCode(this))
-            + " u" + userId + " " + intent.getAction() + "}";
+            + " u" + userId + " " + label + "}";
     }
 
     public String toShortString() {
-        return intent.getAction() + "/u" + userId;
+        String label = intent.getAction();
+        if (label == null) {
+            label = intent.toString();
+        }
+        return label + "/u" + userId;
     }
 
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
diff --git a/services/core/java/com/android/server/am/ProcessReceiverRecord.java b/services/core/java/com/android/server/am/ProcessReceiverRecord.java
index 8d3e9669..34a2b03 100644
--- a/services/core/java/com/android/server/am/ProcessReceiverRecord.java
+++ b/services/core/java/com/android/server/am/ProcessReceiverRecord.java
@@ -34,29 +34,61 @@
      */
     private final ArraySet<BroadcastRecord> mCurReceivers = new ArraySet<BroadcastRecord>();
 
+    private int mCurReceiversSize;
+
     /**
      * All IIntentReceivers that are registered from this process.
      */
     private final ArraySet<ReceiverList> mReceivers = new ArraySet<>();
 
     int numberOfCurReceivers() {
-        return mCurReceivers.size();
+        return mCurReceiversSize;
     }
 
+    void incrementCurReceivers() {
+        mCurReceiversSize++;
+    }
+
+    void decrementCurReceivers() {
+        mCurReceiversSize--;
+    }
+
+    /**
+     * @deprecated we're moving towards tracking only a reference count to
+     *             improve performance.
+     */
+    @Deprecated
     BroadcastRecord getCurReceiverAt(int index) {
         return mCurReceivers.valueAt(index);
     }
 
+    /**
+     * @deprecated we're moving towards tracking only a reference count to
+     *             improve performance.
+     */
+    @Deprecated
     boolean hasCurReceiver(BroadcastRecord receiver) {
         return mCurReceivers.contains(receiver);
     }
 
+    /**
+     * @deprecated we're moving towards tracking only a reference count to
+     *             improve performance.
+     */
+    @Deprecated
     void addCurReceiver(BroadcastRecord receiver) {
         mCurReceivers.add(receiver);
+        mCurReceiversSize = mCurReceivers.size();
     }
 
+    /**
+     * @deprecated we're moving towards tracking only a reference count to
+     *             improve performance.
+     */
+    @Deprecated
     void removeCurReceiver(BroadcastRecord receiver) {
         mCurReceivers.remove(receiver);
+        mCurReceiversSize = mCurReceivers.size();
     }
 
     int numberOfReceivers() {
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 7c7b01c9..82fb1e8 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -1387,7 +1387,7 @@
         int i = 0;
         for (; i < profilesToStartSize && i < (getMaxRunningUsers() - 1); ++i) {
             // NOTE: this method is setting the profiles of the current user - which is always
-            // assigned to the default display - so there's no need to pass PARENT_DISPLAY
+            // assigned to the default display
             startUser(profilesToStart.get(i).id, /* foreground= */ false);
         }
         if (i < profilesToStartSize) {
@@ -1430,10 +1430,7 @@
             return false;
         }
 
-        int displayId = mInjector.isUsersOnSecondaryDisplaysEnabled()
-                ? UserManagerInternal.PARENT_DISPLAY
-                : Display.DEFAULT_DISPLAY;
-        return startUserNoChecks(userId, displayId, /* foreground= */ false,
+        return startUserNoChecks(userId, Display.DEFAULT_DISPLAY, /* foreground= */ false,
                 /* unlockListener= */ null);
     }
 
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/core/java/com/android/server/broadcastradio/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
index ab553a8..3ede0a2 100644
--- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/BroadcastRadioService.java
@@ -21,21 +21,23 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.hardware.radio.IRadioService;
-import android.util.Slog;
 
 import com.android.server.SystemService;
 
+import java.util.ArrayList;
+
 public class BroadcastRadioService extends SystemService {
-    private static final String TAG = "BcRadioSrv";
     private final IRadioService mServiceImpl;
+
     public BroadcastRadioService(Context context) {
         super(context);
-        mServiceImpl = new BroadcastRadioServiceHidl(this);
+        ArrayList<String> serviceNameList = IRadioServiceAidlImpl.getServicesNames();
+        mServiceImpl = serviceNameList.isEmpty() ? new IRadioServiceHidlImpl(this)
+                : new IRadioServiceAidlImpl(this, serviceNameList);
     }
 
     @Override
     public void onStart() {
-        Slog.v(TAG, "BroadcastRadioService onStart()");
         publishBinderService(Context.RADIO_SERVICE, mServiceImpl.asBinder());
     }
 
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
new file mode 100644
index 0000000..0770062
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -0,0 +1,124 @@
+/**
+ * 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.broadcastradio;
+
+import android.hardware.broadcastradio.IBroadcastRadio;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.IRadioService;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import com.android.server.utils.Slogf;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Wrapper for AIDL interface for BroadcastRadio HAL
+ */
+final class IRadioServiceAidlImpl extends IRadioService.Stub {
+    private static final String TAG = "BcRadioSrvAidl";
+
+    private static final List<String> SERVICE_NAMES = Arrays.asList(
+            IBroadcastRadio.DESCRIPTOR + "/amfm", IBroadcastRadio.DESCRIPTOR + "/dab");
+
+    private final com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl mHalAidl;
+    private final BroadcastRadioService mService;
+
+    /**
+     * Gets names of all AIDL BroadcastRadio HAL services available.
+     */
+    public static ArrayList<String> getServicesNames() {
+        ArrayList<String> serviceList = new ArrayList<>();
+        for (int i = 0; i < SERVICE_NAMES.size(); i++) {
+            IBinder serviceBinder = ServiceManager.waitForDeclaredService(SERVICE_NAMES.get(i));
+            if (serviceBinder != null) {
+                serviceList.add(SERVICE_NAMES.get(i));
+            }
+        }
+        return serviceList;
+    }
+
+    IRadioServiceAidlImpl(BroadcastRadioService service, ArrayList<String> serviceList) {
+        Slogf.i(TAG, "Initialize BroadcastRadioServiceAidl(%s)", service);
+        mService = Objects.requireNonNull(service);
+        mHalAidl =
+                new com.android.server.broadcastradio.aidl.BroadcastRadioServiceImpl(serviceList);
+    }
+
+    @Override
+    public List<RadioManager.ModuleProperties> listModules() {
+        mService.enforcePolicyAccess();
+        return mHalAidl.listModules();
+    }
+
+    @Override
+    public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
+            boolean withAudio, ITunerCallback callback) throws RemoteException {
+        if (isDebugEnabled()) {
+            Slogf.d(TAG, "Opening module %d", moduleId);
+        }
+        mService.enforcePolicyAccess();
+        if (callback == null) {
+            throw new IllegalArgumentException("Callback must not be null");
+        }
+        return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback);
+    }
+
+    @Override
+    public ICloseHandle addAnnouncementListener(int[] enabledTypes,
+            IAnnouncementListener listener) {
+        if (isDebugEnabled()) {
+            Slogf.d(TAG, "Adding announcement listener for %s", Arrays.toString(enabledTypes));
+        }
+        Objects.requireNonNull(enabledTypes);
+        Objects.requireNonNull(listener);
+        mService.enforcePolicyAccess();
+
+        return mHalAidl.addAnnouncementListener(enabledTypes, listener);
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+        IndentingPrintWriter radioPrintWriter = new IndentingPrintWriter(printWriter);
+        radioPrintWriter.printf("BroadcastRadioService\n");
+
+        radioPrintWriter.increaseIndent();
+        radioPrintWriter.printf("AIDL HAL:\n");
+
+        radioPrintWriter.increaseIndent();
+        mHalAidl.dumpInfo(radioPrintWriter);
+        radioPrintWriter.decreaseIndent();
+
+        radioPrintWriter.decreaseIndent();
+    }
+
+    private static boolean isDebugEnabled() {
+        return Log.isLoggable(TAG, Log.DEBUG);
+    }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/BroadcastRadioServiceHidl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
similarity index 96%
rename from services/core/java/com/android/server/broadcastradio/BroadcastRadioServiceHidl.java
rename to services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 5cb6770..28b6d02 100644
--- a/services/core/java/com/android/server/broadcastradio/BroadcastRadioServiceHidl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -41,7 +41,7 @@
 /**
  * Wrapper for HIDL interface for BroadcastRadio HAL
  */
-final class BroadcastRadioServiceHidl extends IRadioService.Stub {
+final class IRadioServiceHidlImpl extends IRadioService.Stub {
     private static final String TAG = "BcRadioSrvHidl";
 
     private final com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1;
@@ -52,7 +52,7 @@
     private final BroadcastRadioService mService;
     private final List<RadioManager.ModuleProperties> mV1Modules;
 
-    BroadcastRadioServiceHidl(BroadcastRadioService service) {
+    IRadioServiceHidlImpl(BroadcastRadioService service) {
         mService = Objects.requireNonNull(service);
         mHal1 = new com.android.server.broadcastradio.hal1.BroadcastRadioService(mLock);
         mV1Modules = mHal1.loadModules();
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/AnnouncementAggregator.java b/services/core/java/com/android/server/broadcastradio/aidl/AnnouncementAggregator.java
new file mode 100644
index 0000000..b618aa3
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/aidl/AnnouncementAggregator.java
@@ -0,0 +1,213 @@
+/*
+ * 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.broadcastradio.aidl;
+
+import android.annotation.Nullable;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.utils.Slogf;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Announcement aggregator extending {@link ICloseHandle} to support broadcast radio announcement
+ */
+public final class AnnouncementAggregator extends ICloseHandle.Stub {
+    private static final String TAG = "BcRadioAidlSrv.AnnAggr";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final Object mLock;
+    private final IAnnouncementListener mListener;
+    private final IBinder.DeathRecipient mDeathRecipient = new DeathRecipient();
+
+    @GuardedBy("mLock")
+    private final List<ModuleWatcher> mModuleWatchers = new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private boolean mIsClosed;
+
+    /**
+     * Constructs Announcement aggregator with AnnouncementListener of BroadcastRadio AIDL HAL.
+     */
+    public AnnouncementAggregator(IAnnouncementListener listener, Object lock) {
+        mListener = Objects.requireNonNull(listener, "listener cannot be null");
+        mLock = Objects.requireNonNull(lock, "lock cannot be null");
+        try {
+            listener.asBinder().linkToDeath(mDeathRecipient, /* flags= */ 0);
+        } catch (RemoteException ex) {
+            ex.rethrowFromSystemServer();
+        }
+    }
+
+    private final class ModuleWatcher extends IAnnouncementListener.Stub {
+
+        @Nullable
+        private ICloseHandle mCloseHandle;
+
+        public List<Announcement> mCurrentList = new ArrayList<>();
+
+        public void onListUpdated(List<Announcement> active) {
+            if (DEBUG) {
+                Slogf.d(TAG, "onListUpdate for %s", active);
+            }
+            mCurrentList = Objects.requireNonNull(active, "active cannot be null");
+            AnnouncementAggregator.this.onListUpdated();
+        }
+
+        public void setCloseHandle(ICloseHandle closeHandle) {
+            if (DEBUG) {
+                Slogf.d(TAG, "Set close handle %s", closeHandle);
+            }
+            mCloseHandle = Objects.requireNonNull(closeHandle, "closeHandle cannot be null");
+        }
+
+        public void close() throws RemoteException {
+            if (DEBUG) {
+                Slogf.d(TAG, "Close module watcher.");
+            }
+            if (mCloseHandle != null) mCloseHandle.close();
+        }
+
+        public void dumpInfo(IndentingPrintWriter pw) {
+            pw.printf("ModuleWatcher:\n");
+
+            pw.increaseIndent();
+            pw.printf("Close handle: %s\n", mCloseHandle);
+            pw.printf("Current announcement list: %s\n", mCurrentList);
+            pw.decreaseIndent();
+        }
+    }
+
+    private class DeathRecipient implements IBinder.DeathRecipient {
+        public void binderDied() {
+            try {
+                close();
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, ex, "Cannot close Announcement aggregator for DeathRecipient");
+            }
+        }
+    }
+
+    private void onListUpdated() {
+        if (DEBUG) {
+            Slogf.d(TAG, "onListUpdated()");
+        }
+        synchronized (mLock) {
+            if (mIsClosed) {
+                Slogf.e(TAG, "Announcement aggregator is closed, it shouldn't receive callbacks");
+                return;
+            }
+            List<Announcement> combined = new ArrayList<>(mModuleWatchers.size());
+            for (int i = 0; i < mModuleWatchers.size(); i++) {
+                combined.addAll(mModuleWatchers.get(i).mCurrentList);
+            }
+            try {
+                mListener.onListUpdated(combined);
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, ex, "mListener.onListUpdated() failed");
+            }
+        }
+    }
+
+    /**
+     * Watches the given RadioModule by adding Announcement Listener to it
+     */
+    public void watchModule(RadioModule radioModule, int[] enabledTypes) {
+        if (DEBUG) {
+            Slogf.d(TAG, "Watch module for %s with enabled types %s",
+                    radioModule, Arrays.toString(enabledTypes));
+        }
+        synchronized (mLock) {
+            if (mIsClosed) {
+                throw new IllegalStateException("Failed to watch module"
+                        + "since announcement aggregator has already been closed");
+            }
+
+            ModuleWatcher watcher = new ModuleWatcher();
+            ICloseHandle closeHandle;
+            try {
+                closeHandle = radioModule.addAnnouncementListener(watcher, enabledTypes);
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, ex, "Failed to add announcement listener");
+                return;
+            }
+            watcher.setCloseHandle(closeHandle);
+            mModuleWatchers.add(watcher);
+        }
+    }
+
+    @Override
+    public void close() throws RemoteException {
+        if (DEBUG) {
+            Slogf.d(TAG, "Close watchModule");
+        }
+        synchronized (mLock) {
+            if (mIsClosed) {
+                Slogf.w(TAG, "Announcement aggregator has already been closed.");
+                return;
+            }
+
+            mListener.asBinder().unlinkToDeath(mDeathRecipient, /* flags= */ 0);
+
+            for (int i = 0; i < mModuleWatchers.size(); i++) {
+                ModuleWatcher moduleWatcher = mModuleWatchers.get(i);
+                try {
+                    moduleWatcher.close();
+                } catch (Exception e) {
+                    Slogf.e(TAG, "Failed to close module watcher %s: %s",
+                            moduleWatcher, e);
+                }
+            }
+            mModuleWatchers.clear();
+
+            mIsClosed = true;
+        }
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
+        IndentingPrintWriter announcementPrintWriter = new IndentingPrintWriter(printWriter);
+        announcementPrintWriter.printf("AnnouncementAggregator\n");
+
+        announcementPrintWriter.increaseIndent();
+        synchronized (mLock) {
+            announcementPrintWriter.printf("Is session closed? %s\n", mIsClosed ? "Yes" : "No");
+            announcementPrintWriter.printf("Module Watchers [%d]:\n", mModuleWatchers.size());
+
+            announcementPrintWriter.increaseIndent();
+            for (int i = 0; i < mModuleWatchers.size(); i++) {
+                mModuleWatchers.get(i).dumpInfo(announcementPrintWriter);
+            }
+            announcementPrintWriter.decreaseIndent();
+
+        }
+        announcementPrintWriter.decreaseIndent();
+    }
+
+}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
new file mode 100644
index 0000000..71ba296
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
@@ -0,0 +1,285 @@
+/*
+ * 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.broadcastradio.aidl;
+
+import android.annotation.Nullable;
+import android.hardware.broadcastradio.IBroadcastRadio;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioTuner;
+import android.os.IBinder;
+import android.os.IServiceCallback;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.utils.Slogf;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Broadcast radio service using BroadcastRadio AIDL HAL
+ */
+public final class BroadcastRadioServiceImpl {
+    private static final String TAG = "BcRadioAidlSrv";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private int mNextModuleId;
+
+    @GuardedBy("mLock")
+    private final Map<String, Integer> mServiceNameToModuleIdMap = new ArrayMap<>();
+
+    // Map from module ID to RadioModule created by mServiceListener.onRegistration().
+    @GuardedBy("mLock")
+    private final SparseArray<RadioModule> mModules = new SparseArray<>();
+
+    private final IServiceCallback.Stub mServiceListener = new IServiceCallback.Stub() {
+        @Override
+        public void onRegistration(String name, final IBinder newBinder) {
+            Slogf.i(TAG, "onRegistration for %s", name);
+            Integer moduleId;
+            synchronized (mLock) {
+                // If the service has been registered before, reuse its previous module ID.
+                moduleId = mServiceNameToModuleIdMap.get(name);
+                boolean newService = false;
+                if (moduleId == null) {
+                    newService = true;
+                    moduleId = mNextModuleId;
+                }
+
+                RadioModule radioModule =
+                        RadioModule.tryLoadingModule(moduleId, name, newBinder, mLock);
+                if (radioModule == null) {
+                    Slogf.w(TAG, "No module %s with id %d (HAL AIDL)", name, moduleId);
+                    return;
+                }
+                try {
+                    radioModule.setInternalHalCallback();
+                } catch (RemoteException ex) {
+                    Slogf.wtf(TAG, ex, "Broadcast radio module %s with id %d (HAL AIDL) "
+                            + "cannot register HAL callback", name, moduleId);
+                    return;
+                }
+                if (DEBUG) {
+                    Slogf.d(TAG, "Loaded broadcast radio module %s with id %d (HAL AIDL)",
+                            name, moduleId);
+                }
+                RadioModule prevModule = mModules.get(moduleId);
+                mModules.put(moduleId, radioModule);
+                if (prevModule != null) {
+                    prevModule.closeSessions(RadioTuner.ERROR_HARDWARE_FAILURE);
+                }
+
+                if (newService) {
+                    mServiceNameToModuleIdMap.put(name, moduleId);
+                    mNextModuleId++;
+                }
+
+                try {
+                    BroadcastRadioDeathRecipient deathRecipient =
+                            new BroadcastRadioDeathRecipient(moduleId);
+                    radioModule.getService().asBinder().linkToDeath(deathRecipient, moduleId);
+                } catch (RemoteException ex) {
+                    Slogf.w(TAG, "Service has already died, so remove its entry from mModules.");
+                    mModules.remove(moduleId);
+                }
+            }
+        }
+    };
+
+    private final class BroadcastRadioDeathRecipient implements IBinder.DeathRecipient {
+        private final int mModuleId;
+
+        BroadcastRadioDeathRecipient(int moduleId) {
+            mModuleId = moduleId;
+        }
+
+        @Override
+        public void binderDied() {
+            Slogf.i(TAG, "ServiceDied for module id %d", mModuleId);
+            synchronized (mLock) {
+                RadioModule prevModule = mModules.removeReturnOld(mModuleId);
+                if (prevModule != null) {
+                    prevModule.closeSessions(RadioTuner.ERROR_HARDWARE_FAILURE);
+                }
+
+                for (Map.Entry<String, Integer> entry : mServiceNameToModuleIdMap.entrySet()) {
+                    if (entry.getValue() == mModuleId) {
+                        Slogf.w(TAG, "Service %s died, removed RadioModule with ID %d",
+                                entry.getKey(), mModuleId);
+                        return;
+                    }
+                }
+            }
+        }
+    };
+
+    /**
+     * Constructs BroadcastRadioServiceImpl using AIDL HAL using the list of names of AIDL
+     * BroadcastRadio HAL services {@code serviceNameList}
+     */
+    public BroadcastRadioServiceImpl(ArrayList<String> serviceNameList) {
+        mNextModuleId = 0;
+        if (DEBUG) {
+            Slogf.d(TAG, "Initializing BroadcastRadioServiceImpl %s",
+                    IBroadcastRadio.DESCRIPTOR);
+        }
+        for (int i = 0; i < serviceNameList.size(); i++) {
+            try {
+                ServiceManager.registerForNotifications(serviceNameList.get(i), mServiceListener);
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, ex, "failed to register for service notifications for service %s",
+                        serviceNameList.get(i));
+            }
+        }
+    }
+
+    /**
+     * Gets all AIDL {@link com.android.server.broadcastradio.aidl.RadioModule}.
+     */
+    public List<RadioManager.ModuleProperties> listModules() {
+        synchronized (mLock) {
+            List<RadioManager.ModuleProperties> moduleList = new ArrayList<>(mModules.size());
+            for (int i = 0; i < mModules.size(); i++) {
+                moduleList.add(mModules.valueAt(i).mProperties);
+            }
+            return moduleList;
+        }
+    }
+
+    /**
+     * Gets the AIDL RadioModule for the given {@code moduleId}. Null will be returned if not found.
+     */
+    public boolean hasModule(int id) {
+        synchronized (mLock) {
+            return mModules.contains(id);
+        }
+    }
+
+    /**
+     * Returns whether any AIDL {@link com.android.server.broadcastradio.aidl.RadioModule} exists.
+     */
+    public boolean hasAnyModules() {
+        synchronized (mLock) {
+            return mModules.size() != 0;
+        }
+    }
+
+    /**
+     * Opens {@link ITuner} session for the AIDL
+     * {@link com.android.server.broadcastradio.aidl.RadioModule} given {@code moduleId}.
+     */
+    @Nullable
+    public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
+            boolean withAudio, ITunerCallback callback) throws RemoteException {
+        if (DEBUG) {
+            Slogf.d(TAG, "Open AIDL radio session");
+        }
+        Objects.requireNonNull(callback);
+
+        if (!withAudio) {
+            throw new IllegalArgumentException("Non-audio sessions not supported with AIDL HAL");
+        }
+
+        RadioModule radioModule;
+        synchronized (mLock) {
+            radioModule = mModules.get(moduleId);
+            if (radioModule == null) {
+                Slogf.e(TAG, "Invalid module ID %d", moduleId);
+                return null;
+            }
+        }
+
+        TunerSession tunerSession = radioModule.openSession(callback);
+        if (legacyConfig != null) {
+            tunerSession.setConfiguration(legacyConfig);
+        }
+        return tunerSession;
+    }
+
+    /**
+     * Adds AnnouncementListener for every
+     * {@link com.android.server.broadcastradio.aidl.RadioModule}.
+     */
+    public ICloseHandle addAnnouncementListener(int[] enabledTypes,
+            IAnnouncementListener listener) {
+        if (DEBUG) {
+            Slogf.d(TAG, "Add AnnouncementListener with enable types %s",
+                    Arrays.toString(enabledTypes));
+        }
+        AnnouncementAggregator aggregator = new AnnouncementAggregator(listener, mLock);
+        boolean anySupported = false;
+        synchronized (mLock) {
+            for (int i = 0; i < mModules.size(); i++) {
+                try {
+                    aggregator.watchModule(mModules.valueAt(i), enabledTypes);
+                    anySupported = true;
+                } catch (UnsupportedOperationException ex) {
+                    Slogf.w(TAG, ex, "Announcements not supported for this module");
+                }
+            }
+        }
+        if (!anySupported) {
+            Slogf.w(TAG, "There are no HAL modules that support announcements");
+        }
+        return aggregator;
+    }
+
+    /**
+     * Dump state of broadcastradio service for AIDL HAL.
+     *
+     * @param pw The file to which {@link BroadcastRadioServiceImpl} state is dumped.
+     */
+    public void dumpInfo(IndentingPrintWriter pw) {
+        synchronized (mLock) {
+            pw.printf("Next module id available: %d\n", mNextModuleId);
+            pw.printf("ServiceName to module id map:\n");
+
+            pw.increaseIndent();
+            for (Map.Entry<String, Integer> entry : mServiceNameToModuleIdMap.entrySet()) {
+                pw.printf("Service name: %s, module id: %d\n", entry.getKey(), entry.getValue());
+            }
+            pw.decreaseIndent();
+
+            pw.printf("Radio modules [%d]:\n", mModules.size());
+
+            pw.increaseIndent();
+            for (int i = 0; i < mModules.size(); i++) {
+                pw.printf("Module id=%d:\n", mModules.keyAt(i));
+
+                pw.increaseIndent();
+                mModules.valueAt(i).dumpInfo(pw);
+                pw.decreaseIndent();
+            }
+            pw.decreaseIndent();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
new file mode 100644
index 0000000..d90f9c4
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
@@ -0,0 +1,483 @@
+/*
+ * 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.broadcastradio.aidl;
+
+import android.annotation.Nullable;
+import android.hardware.broadcastradio.AmFmRegionConfig;
+import android.hardware.broadcastradio.Announcement;
+import android.hardware.broadcastradio.DabTableEntry;
+import android.hardware.broadcastradio.IdentifierType;
+import android.hardware.broadcastradio.Metadata;
+import android.hardware.broadcastradio.ProgramFilter;
+import android.hardware.broadcastradio.ProgramIdentifier;
+import android.hardware.broadcastradio.ProgramInfo;
+import android.hardware.broadcastradio.ProgramListChunk;
+import android.hardware.broadcastradio.Properties;
+import android.hardware.broadcastradio.Result;
+import android.hardware.broadcastradio.VendorKeyValue;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.hardware.radio.RadioMetadata;
+import android.os.ParcelableException;
+import android.os.ServiceSpecificException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.IntArray;
+
+import com.android.server.utils.Slogf;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A utils class converting data types between AIDL broadcast radio HAL and
+ * {@link android.hardware.radio}
+ */
+final class ConversionUtils {
+    // TODO(b/241118988): Add unit test for ConversionUtils class
+    private static final String TAG = "BcRadioAidlSrv.convert";
+
+    private ConversionUtils() {
+        throw new UnsupportedOperationException("ConversionUtils class is noninstantiable");
+    }
+
+    static RuntimeException throwOnError(RuntimeException halException, String action) {
+        if (!(halException instanceof ServiceSpecificException)) {
+            return new ParcelableException(new RuntimeException(
+                    action + ": unknown error"));
+        }
+        int result = ((ServiceSpecificException) halException).errorCode;
+        switch (result) {
+            case Result.UNKNOWN_ERROR:
+                return new ParcelableException(new RuntimeException(action
+                        + ": UNKNOWN_ERROR"));
+            case Result.INTERNAL_ERROR:
+                return new ParcelableException(new RuntimeException(action
+                        + ": INTERNAL_ERROR"));
+            case Result.INVALID_ARGUMENTS:
+                return new IllegalArgumentException(action + ": INVALID_ARGUMENTS");
+            case Result.INVALID_STATE:
+                return new IllegalStateException(action + ": INVALID_STATE");
+            case Result.NOT_SUPPORTED:
+                return new UnsupportedOperationException(action + ": NOT_SUPPORTED");
+            case Result.TIMEOUT:
+                return new ParcelableException(new RuntimeException(action + ": TIMEOUT"));
+            default:
+                return new ParcelableException(new RuntimeException(
+                        action + ": unknown error (" + result + ")"));
+        }
+    }
+
+    static VendorKeyValue[] vendorInfoToHalVendorKeyValues(@Nullable Map<String, String> info) {
+        if (info == null) {
+            return new VendorKeyValue[]{};
+        }
+
+        ArrayList<VendorKeyValue> list = new ArrayList<>();
+        for (Map.Entry<String, String> entry : info.entrySet()) {
+            VendorKeyValue elem = new VendorKeyValue();
+            elem.key = entry.getKey();
+            elem.value = entry.getValue();
+            if (elem.key == null || elem.value == null) {
+                Slogf.w(TAG, "VendorKeyValue contains invalid entry: key = %s, value = %s",
+                        elem.key, elem.value);
+                continue;
+            }
+            list.add(elem);
+        }
+
+        return list.toArray(VendorKeyValue[]::new);
+    }
+
+    static Map<String, String> vendorInfoFromHalVendorKeyValues(@Nullable VendorKeyValue[] info) {
+        if (info == null) {
+            return Collections.emptyMap();
+        }
+
+        Map<String, String> map = new ArrayMap<>();
+        for (VendorKeyValue kvp : info) {
+            if (kvp.key == null || kvp.value == null) {
+                Slogf.w(TAG, "VendorKeyValue contains invalid entry: key = %s, value = %s",
+                        kvp.key, kvp.value);
+                continue;
+            }
+            map.put(kvp.key, kvp.value);
+        }
+
+        return map;
+    }
+
+    @ProgramSelector.ProgramType
+    private static int identifierTypeToProgramType(
+            @ProgramSelector.IdentifierType int idType) {
+        switch (idType) {
+            case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY:
+            case ProgramSelector.IDENTIFIER_TYPE_RDS_PI:
+                // TODO(b/69958423): verify AM/FM with frequency range
+                return ProgramSelector.PROGRAM_TYPE_FM;
+            case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT:
+                // TODO(b/69958423): verify AM/FM with frequency range
+                return ProgramSelector.PROGRAM_TYPE_FM_HD;
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
+            case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+                return ProgramSelector.PROGRAM_TYPE_DAB;
+            case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
+            case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
+                return ProgramSelector.PROGRAM_TYPE_DRMO;
+            case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
+            case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
+                return ProgramSelector.PROGRAM_TYPE_SXM;
+        }
+        if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
+                && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
+            return idType;
+        }
+        return ProgramSelector.PROGRAM_TYPE_INVALID;
+    }
+
+    private static int[] identifierTypesToProgramTypes(int[] idTypes) {
+        Set<Integer> programTypes = new ArraySet<>();
+
+        for (int i = 0; i < idTypes.length; i++) {
+            int pType = identifierTypeToProgramType(idTypes[i]);
+
+            if (pType == ProgramSelector.PROGRAM_TYPE_INVALID) continue;
+
+            programTypes.add(pType);
+            if (pType == ProgramSelector.PROGRAM_TYPE_FM) {
+                // TODO(b/69958423): verify AM/FM with region info
+                programTypes.add(ProgramSelector.PROGRAM_TYPE_AM);
+            }
+            if (pType == ProgramSelector.PROGRAM_TYPE_FM_HD) {
+                // TODO(b/69958423): verify AM/FM with region info
+                programTypes.add(ProgramSelector.PROGRAM_TYPE_AM_HD);
+            }
+        }
+
+        int[] programTypesArray = new int[programTypes.size()];
+        int i = 0;
+        for (int programType : programTypes) {
+            programTypesArray[i++] = programType;
+        }
+        return programTypesArray;
+    }
+
+    private static RadioManager.BandDescriptor[] amfmConfigToBands(
+            @Nullable AmFmRegionConfig config) {
+        if (config == null) {
+            return new RadioManager.BandDescriptor[0];
+        }
+
+        int len = config.ranges.length;
+        List<RadioManager.BandDescriptor> bands = new ArrayList<>();
+
+        // Just a placeholder value.
+        int region = RadioManager.REGION_ITU_1;
+
+        for (int i = 0; i < len; i++) {
+            Utils.FrequencyBand bandType = Utils.getBand(config.ranges[i].lowerBound);
+            if (bandType == Utils.FrequencyBand.UNKNOWN) {
+                Slogf.e(TAG, "Unknown frequency band at %d kHz", config.ranges[i].lowerBound);
+                continue;
+            }
+            if (bandType == Utils.FrequencyBand.FM) {
+                bands.add(new RadioManager.FmBandDescriptor(region, RadioManager.BAND_FM,
+                        config.ranges[i].lowerBound, config.ranges[i].upperBound,
+                        config.ranges[i].spacing,
+
+                        // TODO(b/69958777): stereo, rds, ta, af, ea
+                        /* stereo= */ true, /* rds= */ true, /* ta= */ true, /* af= */ true,
+                        /* ea= */ true
+                ));
+            } else {  // AM
+                bands.add(new RadioManager.AmBandDescriptor(region, RadioManager.BAND_AM,
+                        config.ranges[i].lowerBound, config.ranges[i].upperBound,
+                        config.ranges[i].spacing,
+
+                        // TODO(b/69958777): stereo
+                        /* stereo= */ true
+                ));
+            }
+        }
+
+        return bands.toArray(RadioManager.BandDescriptor[]::new);
+    }
+
+    @Nullable
+    private static Map<String, Integer> dabConfigFromHalDabTableEntries(
+            @Nullable DabTableEntry[] config) {
+        if (config == null) {
+            return null;
+        }
+        Map<String, Integer> dabConfig = new ArrayMap<>();
+        for (int i = 0; i < config.length; i++) {
+            dabConfig.put(config[i].label, config[i].frequencyKhz);
+        }
+        return dabConfig;
+    }
+
+    static RadioManager.ModuleProperties propertiesFromHalProperties(int id,
+            String serviceName, Properties prop,
+            @Nullable AmFmRegionConfig amfmConfig, @Nullable DabTableEntry[] dabConfig) {
+        Objects.requireNonNull(serviceName);
+        Objects.requireNonNull(prop);
+
+        int[] supportedProgramTypes = identifierTypesToProgramTypes(prop.supportedIdentifierTypes);
+
+        return new RadioManager.ModuleProperties(
+                id,
+                serviceName,
+
+                // There is no Class concept in HAL AIDL.
+                RadioManager.CLASS_AM_FM,
+
+                prop.maker,
+                prop.product,
+                prop.version,
+                prop.serial,
+
+                // HAL AIDL only supports single tuner and audio source per
+                // HAL implementation instance.
+                /* numTuners= */ 1,
+                /* numAudioSources= */ 1,
+                /* isInitializationRequired= */ false,
+                /* isCaptureSupported= */ false,
+
+                amfmConfigToBands(amfmConfig),
+                /* isBgScanSupported= */ true,
+                supportedProgramTypes,
+                prop.supportedIdentifierTypes,
+                dabConfigFromHalDabTableEntries(dabConfig),
+                vendorInfoFromHalVendorKeyValues(prop.vendorInfo)
+        );
+    }
+
+    static ProgramIdentifier identifierToHalProgramIdentifier(ProgramSelector.Identifier id) {
+        ProgramIdentifier hwId = new ProgramIdentifier();
+        hwId.type = id.getType();
+        hwId.value = id.getValue();
+        return hwId;
+    }
+
+    @Nullable
+    static ProgramSelector.Identifier identifierFromHalProgramIdentifier(
+            ProgramIdentifier id) {
+        if (id.type == IdentifierType.INVALID) {
+            return null;
+        }
+        return new ProgramSelector.Identifier(id.type, id.value);
+    }
+
+    static android.hardware.broadcastradio.ProgramSelector programSelectorToHalProgramSelector(
+            ProgramSelector sel) {
+        android.hardware.broadcastradio.ProgramSelector hwSel =
+                new android.hardware.broadcastradio.ProgramSelector();
+
+        hwSel.primaryId = identifierToHalProgramIdentifier(sel.getPrimaryId());
+        ProgramSelector.Identifier[] secondaryIds = sel.getSecondaryIds();
+        ArrayList<ProgramIdentifier> secondaryIdList = new ArrayList<>(secondaryIds.length);
+        for (int i = 0; i < secondaryIds.length; i++) {
+            secondaryIdList.add(identifierToHalProgramIdentifier(secondaryIds[i]));
+        }
+        hwSel.secondaryIds = secondaryIdList.toArray(ProgramIdentifier[]::new);
+        return hwSel;
+    }
+
+    private static boolean isEmpty(
+            android.hardware.broadcastradio.ProgramSelector sel) {
+        return sel.primaryId.type == IdentifierType.INVALID && sel.primaryId.value == 0
+                && sel.secondaryIds.length == 0;
+    }
+
+    @Nullable
+    static ProgramSelector programSelectorFromHalProgramSelector(
+            android.hardware.broadcastradio.ProgramSelector sel) {
+        if (isEmpty(sel)) {
+            return null;
+        }
+
+        List<ProgramSelector.Identifier> secondaryIdList = new ArrayList<>();
+        for (int i = 0; i < sel.secondaryIds.length; i++) {
+            if (sel.secondaryIds[i] != null) {
+                secondaryIdList.add(identifierFromHalProgramIdentifier(sel.secondaryIds[i]));
+            }
+        }
+
+        return new ProgramSelector(
+                identifierTypeToProgramType(sel.primaryId.type),
+                Objects.requireNonNull(identifierFromHalProgramIdentifier(sel.primaryId)),
+                secondaryIdList.toArray(new ProgramSelector.Identifier[0]),
+                /* vendorIds= */ null);
+    }
+
+    private static RadioMetadata radioMetadataFromHalMetadata(Metadata[] meta) {
+        RadioMetadata.Builder builder = new RadioMetadata.Builder();
+
+        for (int i = 0; i < meta.length; i++) {
+            switch (meta[i].getTag()) {
+                case Metadata.rdsPs:
+                    builder.putString(RadioMetadata.METADATA_KEY_RDS_PS, meta[i].getRdsPs());
+                    break;
+                case Metadata.rdsPty:
+                    builder.putInt(RadioMetadata.METADATA_KEY_RDS_PTY, meta[i].getRdsPty());
+                    break;
+                case Metadata.rbdsPty:
+                    builder.putInt(RadioMetadata.METADATA_KEY_RBDS_PTY, meta[i].getRbdsPty());
+                    break;
+                case Metadata.rdsRt:
+                    builder.putString(RadioMetadata.METADATA_KEY_RDS_RT, meta[i].getRdsRt());
+                    break;
+                case Metadata.songTitle:
+                    builder.putString(RadioMetadata.METADATA_KEY_TITLE, meta[i].getSongTitle());
+                    break;
+                case Metadata.songArtist:
+                    builder.putString(RadioMetadata.METADATA_KEY_ARTIST, meta[i].getSongArtist());
+                    break;
+                case Metadata.songAlbum:
+                    builder.putString(RadioMetadata.METADATA_KEY_ALBUM, meta[i].getSongAlbum());
+                    break;
+                case Metadata.stationIcon:
+                    builder.putInt(RadioMetadata.METADATA_KEY_ICON, meta[i].getStationIcon());
+                    break;
+                case Metadata.albumArt:
+                    builder.putInt(RadioMetadata.METADATA_KEY_ART, meta[i].getAlbumArt());
+                    break;
+                case Metadata.programName:
+                    builder.putString(RadioMetadata.METADATA_KEY_PROGRAM_NAME,
+                            meta[i].getProgramName());
+                    break;
+                case Metadata.dabEnsembleName:
+                    builder.putString(RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME,
+                            meta[i].getDabEnsembleName());
+                    break;
+                case Metadata.dabEnsembleNameShort:
+                    builder.putString(RadioMetadata.METADATA_KEY_DAB_ENSEMBLE_NAME_SHORT,
+                            meta[i].getDabEnsembleNameShort());
+                    break;
+                case Metadata.dabServiceName:
+                    builder.putString(RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME,
+                            meta[i].getDabServiceName());
+                    break;
+                case Metadata.dabServiceNameShort:
+                    builder.putString(RadioMetadata.METADATA_KEY_DAB_SERVICE_NAME_SHORT,
+                            meta[i].getDabServiceNameShort());
+                    break;
+                case Metadata.dabComponentName:
+                    builder.putString(RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME,
+                            meta[i].getDabComponentName());
+                    break;
+                case Metadata.dabComponentNameShort:
+                    builder.putString(RadioMetadata.METADATA_KEY_DAB_COMPONENT_NAME_SHORT,
+                            meta[i].getDabComponentNameShort());
+                    break;
+                default:
+                    Slogf.w(TAG, "Ignored unknown metadata entry: %s", meta[i]);
+                    break;
+            }
+
+        }
+
+        return builder.build();
+    }
+
+    static RadioManager.ProgramInfo programInfoFromHalProgramInfo(ProgramInfo info) {
+        Collection<ProgramSelector.Identifier> relatedContent = new ArrayList<>();
+        if (info.relatedContent != null) {
+            for (int i = 0; i < info.relatedContent.length; i++) {
+                ProgramSelector.Identifier relatedContentId =
+                        identifierFromHalProgramIdentifier(info.relatedContent[i]);
+                if (relatedContentId != null) {
+                    relatedContent.add(relatedContentId);
+                }
+            }
+        }
+
+        return new RadioManager.ProgramInfo(
+                Objects.requireNonNull(programSelectorFromHalProgramSelector(info.selector)),
+                identifierFromHalProgramIdentifier(info.logicallyTunedTo),
+                identifierFromHalProgramIdentifier(info.physicallyTunedTo),
+                relatedContent,
+                info.infoFlags,
+                info.signalQuality,
+                radioMetadataFromHalMetadata(info.metadata),
+                vendorInfoFromHalVendorKeyValues(info.vendorInfo)
+        );
+    }
+
+    static ProgramFilter filterToHalProgramFilter(@Nullable ProgramList.Filter filter) {
+        if (filter == null) {
+            filter = new ProgramList.Filter();
+        }
+
+        ProgramFilter hwFilter = new ProgramFilter();
+
+        IntArray identifierTypeList = new IntArray(filter.getIdentifierTypes().size());
+        ArrayList<ProgramIdentifier> identifiersList = new ArrayList<>();
+        Iterator<Integer> typeIterator = filter.getIdentifierTypes().iterator();
+        while (typeIterator.hasNext()) {
+            identifierTypeList.add(typeIterator.next());
+        }
+        Iterator<ProgramSelector.Identifier> idIterator = filter.getIdentifiers().iterator();
+        while (idIterator.hasNext()) {
+            identifiersList.add(identifierToHalProgramIdentifier(idIterator.next()));
+        }
+
+        hwFilter.identifierTypes = identifierTypeList.toArray();
+        hwFilter.identifiers = identifiersList.toArray(ProgramIdentifier[]::new);
+        hwFilter.includeCategories = filter.areCategoriesIncluded();
+        hwFilter.excludeModifications = filter.areModificationsExcluded();
+
+        return hwFilter;
+    }
+
+    static ProgramList.Chunk chunkFromHalProgramListChunk(ProgramListChunk chunk) {
+        Set<RadioManager.ProgramInfo> modified = new ArraySet<>(chunk.modified.length);
+        for (int i = 0; i < chunk.modified.length; i++) {
+            modified.add(programInfoFromHalProgramInfo(chunk.modified[i]));
+        }
+        Set<ProgramSelector.Identifier> removed = new ArraySet<>();
+        if (chunk.removed != null) {
+            for (int i = 0; i < chunk.removed.length; i++) {
+                ProgramSelector.Identifier removedId =
+                        identifierFromHalProgramIdentifier(chunk.removed[i]);
+                if (removedId != null) {
+                    removed.add(removedId);
+                }
+            }
+        }
+        return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed);
+    }
+
+    public static android.hardware.radio.Announcement announcementFromHalAnnouncement(
+            Announcement hwAnnouncement) {
+        return new android.hardware.radio.Announcement(
+                Objects.requireNonNull(programSelectorFromHalProgramSelector(
+                        hwAnnouncement.selector)),
+                hwAnnouncement.type,
+                vendorInfoFromHalVendorKeyValues(hwAnnouncement.vendorInfo)
+        );
+    }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java
new file mode 100644
index 0000000..095a5fa
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java
@@ -0,0 +1,304 @@
+/*
+ * 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.broadcastradio.aidl;
+
+import android.annotation.Nullable;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class to filter and update program info for HAL clients from broadcast radio AIDL HAL
+ */
+final class ProgramInfoCache {
+
+    /**
+     * Maximum number of {@link RadioManager#ProgramInfo} elements that will be put into a
+     * ProgramList.Chunk.mModified array. Used to try to ensure a single ProgramList.Chunk
+     * stays within the AIDL data size limit.
+     */
+    private static final int MAX_NUM_MODIFIED_PER_CHUNK = 100;
+
+    /**
+     * Maximum number of {@link ProgramSelector#Identifier} elements that will be put
+     * into the removed array of {@link ProgramList#Chunk}. Used to try to ensure a single
+     * {@link ProgramList#Chunk} stays within the AIDL data size limit.
+     */
+    private static final int MAX_NUM_REMOVED_PER_CHUNK = 500;
+
+    /**
+     * Map from primary identifier to corresponding {@link RadioManager#ProgramInfo}.
+     */
+    private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mProgramInfoMap =
+            new ArrayMap<>();
+
+    /**
+     * Flag indicating whether mProgramInfoMap is considered complete based upon the received
+     * updates.
+     */
+    private boolean mComplete = true;
+
+    /**
+     * Optional filter used in {@link ProgramInfoCache#filterAndUpdateFromInternal}. Usually this
+     * field is null for a HAL-side cache and non-null for an AIDL-side cache.
+     */
+    @Nullable private final ProgramList.Filter mFilter;
+
+    ProgramInfoCache(@Nullable ProgramList.Filter filter) {
+        mFilter = filter;
+    }
+
+    @VisibleForTesting
+    ProgramInfoCache(@Nullable ProgramList.Filter filter, boolean complete,
+            RadioManager.ProgramInfo... programInfos) {
+        mFilter = filter;
+        mComplete = complete;
+        for (int i = 0; i < programInfos.length; i++) {
+            mProgramInfoMap.put(programInfos[i].getSelector().getPrimaryId(), programInfos[i]);
+        }
+    }
+
+    @VisibleForTesting
+    boolean programInfosAreExactly(RadioManager.ProgramInfo... programInfos) {
+        Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> expectedMap = new ArrayMap<>();
+        for (int i = 0; i < programInfos.length; i++) {
+            expectedMap.put(programInfos[i].getSelector().getPrimaryId(), programInfos[i]);
+        }
+        return expectedMap.equals(mProgramInfoMap);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("ProgramInfoCache(mComplete = ");
+        sb.append(mComplete);
+        sb.append(", mFilter = ");
+        sb.append(mFilter);
+        sb.append(", mProgramInfoMap = [");
+        mProgramInfoMap.forEach((id, programInfo) -> {
+            sb.append(", ");
+            sb.append(programInfo);
+        });
+        return sb.append("])").toString();
+    }
+
+    public boolean isComplete() {
+        return mComplete;
+    }
+
+    @Nullable
+    public ProgramList.Filter getFilter() {
+        return mFilter;
+    }
+
+    @VisibleForTesting
+    void updateFromHalProgramListChunk(
+            android.hardware.broadcastradio.ProgramListChunk chunk) {
+        if (chunk.purge) {
+            mProgramInfoMap.clear();
+        }
+        for (int i = 0; i < chunk.modified.length; i++) {
+            RadioManager.ProgramInfo programInfo =
+                    ConversionUtils.programInfoFromHalProgramInfo(chunk.modified[i]);
+            mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo);
+        }
+        if (chunk.removed != null) {
+            for (int i = 0; i < chunk.removed.length; i++) {
+                mProgramInfoMap.remove(
+                        ConversionUtils.identifierFromHalProgramIdentifier(chunk.removed[i]));
+            }
+        }
+        mComplete = chunk.complete;
+    }
+
+    List<ProgramList.Chunk> filterAndUpdateFromInternal(ProgramInfoCache other,
+            boolean purge) {
+        return filterAndUpdateFromInternal(other, purge, MAX_NUM_MODIFIED_PER_CHUNK,
+                MAX_NUM_REMOVED_PER_CHUNK);
+    }
+
+    @VisibleForTesting
+    List<ProgramList.Chunk> filterAndUpdateFromInternal(ProgramInfoCache other,
+            boolean purge, int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
+        if (purge) {
+            mProgramInfoMap.clear();
+        }
+        // If mProgramInfoMap is empty, we treat this update as a purge because this might be the
+        // first update to an AIDL client that changed its filter.
+        if (mProgramInfoMap.isEmpty()) {
+            purge = true;
+        }
+
+        Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
+        Set<ProgramSelector.Identifier> removed = new ArraySet<>(mProgramInfoMap.keySet());
+        for (Map.Entry<ProgramSelector.Identifier, RadioManager.ProgramInfo> entry
+                : other.mProgramInfoMap.entrySet()) {
+            ProgramSelector.Identifier id = entry.getKey();
+            if (!passesFilter(id)) {
+                continue;
+            }
+            removed.remove(id);
+
+            RadioManager.ProgramInfo newInfo = entry.getValue();
+            if (!shouldIncludeInModified(newInfo)) {
+                continue;
+            }
+            mProgramInfoMap.put(id, newInfo);
+            modified.add(newInfo);
+        }
+        for (ProgramSelector.Identifier rem : removed) {
+            mProgramInfoMap.remove(rem);
+        }
+        mComplete = other.mComplete;
+        return buildChunks(purge, mComplete, modified, maxNumModifiedPerChunk, removed,
+                maxNumRemovedPerChunk);
+    }
+
+    @Nullable
+    List<ProgramList.Chunk> filterAndApplyChunk(ProgramList.Chunk chunk) {
+        return filterAndApplyChunkInternal(chunk, MAX_NUM_MODIFIED_PER_CHUNK,
+                MAX_NUM_REMOVED_PER_CHUNK);
+    }
+
+    @VisibleForTesting
+    @Nullable
+    List<ProgramList.Chunk> filterAndApplyChunkInternal(ProgramList.Chunk chunk,
+            int maxNumModifiedPerChunk, int maxNumRemovedPerChunk) {
+        if (chunk.isPurge()) {
+            mProgramInfoMap.clear();
+        }
+
+        Set<RadioManager.ProgramInfo> modified = new ArraySet<>();
+        Set<ProgramSelector.Identifier> removed = new ArraySet<>();
+        for (RadioManager.ProgramInfo info : chunk.getModified()) {
+            ProgramSelector.Identifier id = info.getSelector().getPrimaryId();
+            if (!passesFilter(id) || !shouldIncludeInModified(info)) {
+                continue;
+            }
+            mProgramInfoMap.put(id, info);
+            modified.add(info);
+        }
+        for (ProgramSelector.Identifier id : chunk.getRemoved()) {
+            if (mProgramInfoMap.containsKey(id)) {
+                mProgramInfoMap.remove(id);
+                removed.add(id);
+            }
+        }
+        if (modified.isEmpty() && removed.isEmpty() && mComplete == chunk.isComplete()
+                && !chunk.isPurge()) {
+            return null;
+        }
+        mComplete = chunk.isComplete();
+        return buildChunks(chunk.isPurge(), mComplete, modified, maxNumModifiedPerChunk, removed,
+                maxNumRemovedPerChunk);
+    }
+
+    private boolean passesFilter(ProgramSelector.Identifier id) {
+        if (mFilter == null) {
+            return true;
+        }
+        if (!mFilter.getIdentifierTypes().isEmpty()
+                && !mFilter.getIdentifierTypes().contains(id.getType())) {
+            return false;
+        }
+        if (!mFilter.getIdentifiers().isEmpty() && !mFilter.getIdentifiers().contains(id)) {
+            return false;
+        }
+        return mFilter.areCategoriesIncluded() || !id.isCategoryType();
+    }
+
+    private boolean shouldIncludeInModified(RadioManager.ProgramInfo newInfo) {
+        RadioManager.ProgramInfo oldInfo = mProgramInfoMap.get(
+                newInfo.getSelector().getPrimaryId());
+        if (oldInfo == null) {
+            return true;
+        }
+        if (mFilter != null && mFilter.areModificationsExcluded()) {
+            return false;
+        }
+        return !oldInfo.equals(newInfo);
+    }
+
+    private static int roundUpFraction(int numerator, int denominator) {
+        return (numerator / denominator) + (numerator % denominator > 0 ? 1 : 0);
+    }
+
+    private static List<ProgramList.Chunk> buildChunks(boolean purge, boolean complete,
+            @Nullable Collection<RadioManager.ProgramInfo> modified, int maxNumModifiedPerChunk,
+            @Nullable Collection<ProgramSelector.Identifier> removed, int maxNumRemovedPerChunk) {
+        // Communication protocol requires that if purge is set, removed is empty.
+        if (purge) {
+            removed = null;
+        }
+
+        // Determine number of chunks we need to send.
+        int numChunks = purge ? 1 : 0;
+        if (modified != null) {
+            numChunks = Math.max(numChunks,
+                    roundUpFraction(modified.size(), maxNumModifiedPerChunk));
+        }
+        if (removed != null) {
+            numChunks = Math.max(numChunks, roundUpFraction(removed.size(), maxNumRemovedPerChunk));
+        }
+        if (numChunks == 0) {
+            return new ArrayList<>();
+        }
+
+        // Try to make similarly-sized chunks by evenly distributing elements from modified and
+        // removed among them.
+        int modifiedPerChunk = 0;
+        int removedPerChunk = 0;
+        Iterator<RadioManager.ProgramInfo> modifiedIter = null;
+        Iterator<ProgramSelector.Identifier> removedIter = null;
+        if (modified != null) {
+            modifiedPerChunk = roundUpFraction(modified.size(), numChunks);
+            modifiedIter = modified.iterator();
+        }
+        if (removed != null) {
+            removedPerChunk = roundUpFraction(removed.size(), numChunks);
+            removedIter = removed.iterator();
+        }
+        List<ProgramList.Chunk> chunks = new ArrayList<>(numChunks);
+        for (int i = 0; i < numChunks; i++) {
+            ArraySet<RadioManager.ProgramInfo> modifiedChunk = new ArraySet<>();
+            ArraySet<ProgramSelector.Identifier> removedChunk = new ArraySet<>();
+            if (modifiedIter != null) {
+                for (int j = 0; j < modifiedPerChunk && modifiedIter.hasNext(); j++) {
+                    modifiedChunk.add(modifiedIter.next());
+                }
+            }
+            if (removedIter != null) {
+                for (int j = 0; j < removedPerChunk && removedIter.hasNext(); j++) {
+                    removedChunk.add(removedIter.next());
+                }
+            }
+            chunks.add(new ProgramList.Chunk(purge && i == 0, complete && (i == numChunks - 1),
+                      modifiedChunk, removedChunk));
+        }
+        return chunks;
+    }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioLogger.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioLogger.java
new file mode 100644
index 0000000..cca351b
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioLogger.java
@@ -0,0 +1,52 @@
+/**
+ * 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.broadcastradio.aidl;
+
+import android.text.TextUtils;
+import android.util.IndentingPrintWriter;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.server.utils.Slogf;
+
+/**
+ * Event logger to log and dump events of radio module and tuner session
+ * for AIDL broadcast radio HAL
+ */
+final class RadioLogger {
+    private final String mTag;
+    private final boolean mDebug;
+    private final LocalLog mEventLogger;
+
+    RadioLogger(String tag, int loggerQueueSize) {
+        mTag = tag;
+        mDebug = Log.isLoggable(mTag, Log.DEBUG);
+        mEventLogger = new LocalLog(loggerQueueSize);
+    }
+
+    void logRadioEvent(String logFormat, Object... args) {
+        String log = TextUtils.formatSimple(logFormat, args);
+        mEventLogger.log(log);
+        if (mDebug) {
+            Slogf.d(mTag, logFormat, args);
+        }
+    }
+
+    void dump(IndentingPrintWriter pw) {
+        mEventLogger.dump(pw);
+    }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
new file mode 100644
index 0000000..c6dc431
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -0,0 +1,523 @@
+/*
+ * 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.broadcastradio.aidl;
+
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.hardware.broadcastradio.AmFmRegionConfig;
+import android.hardware.broadcastradio.Announcement;
+import android.hardware.broadcastradio.DabTableEntry;
+import android.hardware.broadcastradio.IAnnouncementListener;
+import android.hardware.broadcastradio.IBroadcastRadio;
+import android.hardware.broadcastradio.ICloseHandle;
+import android.hardware.broadcastradio.ITunerCallback;
+import android.hardware.broadcastradio.ProgramInfo;
+import android.hardware.broadcastradio.ProgramListChunk;
+import android.hardware.broadcastradio.ProgramSelector;
+import android.hardware.broadcastradio.VendorKeyValue;
+import android.hardware.radio.RadioManager;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.utils.Slogf;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+final class RadioModule {
+    private static final String TAG = "BcRadioAidlSrv.module";
+    private static final int RADIO_EVENT_LOGGER_QUEUE_SIZE = 25;
+
+    private final IBroadcastRadio mService;
+    public final RadioManager.ModuleProperties mProperties;
+
+    private final Object mLock;
+    private final Handler mHandler;
+    private final RadioLogger mLogger;
+
+    /**
+     * Tracks antenna state reported by HAL (if any).
+     */
+    @GuardedBy("mLock")
+    private Boolean mAntennaConnected;
+
+    @GuardedBy("mLock")
+    private RadioManager.ProgramInfo mCurrentProgramInfo;
+
+    @GuardedBy("mLock")
+    private final ProgramInfoCache mProgramInfoCache = new ProgramInfoCache(null);
+
+    @GuardedBy("mLock")
+    private android.hardware.radio.ProgramList.Filter mUnionOfAidlProgramFilters;
+
+    /**
+     * Set of active AIDL tuner sessions created through openSession().
+     */
+    @GuardedBy("mLock")
+    private final ArraySet<TunerSession> mAidlTunerSessions = new ArraySet<>();
+
+    /**
+     * Callback registered with the HAL to relay callbacks to AIDL clients.
+     */
+    private final ITunerCallback mHalTunerCallback = new ITunerCallback.Stub() {
+        @Override
+        public int getInterfaceVersion() {
+            return this.VERSION;
+        }
+
+        @Override
+        public String getInterfaceHash() {
+            return this.HASH;
+        }
+
+        public void onTuneFailed(int result, ProgramSelector programSelector) {
+            fireLater(() -> {
+                synchronized (mLock) {
+                    android.hardware.radio.ProgramSelector csel =
+                            ConversionUtils.programSelectorFromHalProgramSelector(programSelector);
+                    fanoutAidlCallbackLocked(cb -> cb.onTuneFailed(result, csel));
+                }
+            });
+        }
+
+        @Override
+        public void onCurrentProgramInfoChanged(ProgramInfo halProgramInfo) {
+            fireLater(() -> {
+                synchronized (mLock) {
+                    mCurrentProgramInfo =
+                            ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo);
+                    RadioManager.ProgramInfo currentProgramInfo = mCurrentProgramInfo;
+                    fanoutAidlCallbackLocked(cb -> {
+                        cb.onCurrentProgramInfoChanged(currentProgramInfo);
+                    });
+                }
+            });
+        }
+
+        @Override
+        public void onProgramListUpdated(ProgramListChunk programListChunk) {
+            fireLater(() -> {
+                synchronized (mLock) {
+                    android.hardware.radio.ProgramList.Chunk chunk =
+                            ConversionUtils.chunkFromHalProgramListChunk(programListChunk);
+                    mProgramInfoCache.filterAndApplyChunk(chunk);
+
+                    for (int i = 0; i < mAidlTunerSessions.size(); i++) {
+                        mAidlTunerSessions.valueAt(i).onMergedProgramListUpdateFromHal(chunk);
+                    }
+                }
+            });
+        }
+
+        @Override
+        public void onAntennaStateChange(boolean connected) {
+            fireLater(() -> {
+                synchronized (mLock) {
+                    mAntennaConnected = connected;
+                    fanoutAidlCallbackLocked(cb -> cb.onAntennaState(connected));
+                }
+            });
+        }
+
+        @Override
+        public void onConfigFlagUpdated(int flag, boolean value) {
+            fireLater(() -> {
+                // TODO(b/243853343): implement config flag update method in
+                //  android.hardware.radio.ITunerCallback
+            });
+        }
+
+        @Override
+        public void onParametersUpdated(VendorKeyValue[] parameters) {
+            fireLater(() -> {
+                synchronized (mLock) {
+                    Map<String, String> cparam =
+                            ConversionUtils.vendorInfoFromHalVendorKeyValues(parameters);
+                    fanoutAidlCallbackLocked(cb -> cb.onParametersUpdated(cparam));
+                }
+            });
+        }
+    };
+
+    @VisibleForTesting
+    RadioModule(IBroadcastRadio service,
+            RadioManager.ModuleProperties properties, Object lock) {
+        mProperties = Objects.requireNonNull(properties, "properties cannot be null");
+        mService = Objects.requireNonNull(service, "service cannot be null");
+        mLock = Objects.requireNonNull(lock, "lock cannot be null");
+        mHandler = new Handler(Looper.getMainLooper());
+        mLogger = new RadioLogger(TAG, RADIO_EVENT_LOGGER_QUEUE_SIZE);
+    }
+
+    @Nullable
+    public static RadioModule tryLoadingModule(int moduleId, String moduleName,
+            IBinder serviceBinder, Object lock) {
+        try {
+            Slogf.i(TAG, "Try loading module for module id = %d, module name = %s",
+                    moduleId, moduleName);
+            IBroadcastRadio service = IBroadcastRadio.Stub
+                    .asInterface(serviceBinder);
+            if (service == null) {
+                Slogf.w(TAG, "Module %s is null", moduleName);
+                return null;
+            }
+
+            AmFmRegionConfig amfmConfig;
+            try {
+                amfmConfig = service.getAmFmRegionConfig(/* full= */ false);
+            } catch (RuntimeException ex) {
+                Slogf.i(TAG, "Module %s does not has AMFM config", moduleName);
+                amfmConfig = null;
+            }
+
+            DabTableEntry[] dabConfig;
+            try {
+                dabConfig = service.getDabRegionConfig();
+            } catch (RuntimeException ex) {
+                Slogf.i(TAG, "Module %s does not has DAB config", moduleName);
+                dabConfig = null;
+            }
+
+            RadioManager.ModuleProperties prop = ConversionUtils.propertiesFromHalProperties(
+                    moduleId, moduleName, service.getProperties(), amfmConfig, dabConfig);
+
+            return new RadioModule(service, prop, lock);
+        } catch (RemoteException ex) {
+            Slogf.e(TAG, ex, "Failed to load module %s", moduleName);
+            return null;
+        }
+    }
+
+    public IBroadcastRadio getService() {
+        return mService;
+    }
+
+    void setInternalHalCallback() throws RemoteException {
+        synchronized (mLock) {
+            mService.setTunerCallback(mHalTunerCallback);
+        }
+    }
+
+    public TunerSession openSession(android.hardware.radio.ITunerCallback userCb)
+            throws RemoteException {
+        mLogger.logRadioEvent("Open TunerSession");
+        TunerSession tunerSession;
+        Boolean antennaConnected;
+        RadioManager.ProgramInfo currentProgramInfo;
+        synchronized (mLock) {
+            tunerSession = new TunerSession(this, mService, userCb, mLock);
+            mAidlTunerSessions.add(tunerSession);
+            antennaConnected = mAntennaConnected;
+            currentProgramInfo = mCurrentProgramInfo;
+        }
+        // Propagate state to new client.
+        // Note: These callbacks are invoked while holding mLock to prevent race conditions
+        // with new callbacks from the HAL.
+        if (antennaConnected != null) {
+            userCb.onAntennaState(antennaConnected);
+        }
+        if (currentProgramInfo != null) {
+            userCb.onCurrentProgramInfoChanged(currentProgramInfo);
+        }
+
+        return tunerSession;
+    }
+
+    public void closeSessions(int error) {
+        mLogger.logRadioEvent("Close TunerSessions %d", error);
+        // TunerSession.close() must be called without mAidlTunerSessions locked because
+        // it can call onTunerSessionClosed(). Therefore, the contents of mAidlTunerSessions
+        // are copied into a local array here.
+        TunerSession[] tunerSessions;
+        synchronized (mLock) {
+            tunerSessions = new TunerSession[mAidlTunerSessions.size()];
+            mAidlTunerSessions.toArray(tunerSessions);
+            mAidlTunerSessions.clear();
+        }
+
+        for (TunerSession tunerSession : tunerSessions) {
+            try {
+                tunerSession.close(error);
+            } catch (Exception e) {
+                Slogf.e(TAG, "Failed to close TunerSession %s: %s", tunerSession, e);
+            }
+        }
+    }
+
+    @GuardedBy("mLock")
+    @Nullable
+    private android.hardware.radio.ProgramList.Filter
+            buildUnionOfTunerSessionFiltersLocked() {
+        Set<Integer> idTypes = null;
+        Set<android.hardware.radio.ProgramSelector.Identifier> ids = null;
+        boolean includeCategories = false;
+        boolean excludeModifications = true;
+
+        for (int i = 0; i < mAidlTunerSessions.size(); i++) {
+            android.hardware.radio.ProgramList.Filter filter =
+                    mAidlTunerSessions.valueAt(i).getProgramListFilter();
+            if (filter == null) {
+                continue;
+            }
+
+            if (idTypes == null) {
+                idTypes = new ArraySet<>(filter.getIdentifierTypes());
+                ids = new ArraySet<>(filter.getIdentifiers());
+                includeCategories = filter.areCategoriesIncluded();
+                excludeModifications = filter.areModificationsExcluded();
+                continue;
+            }
+            if (!idTypes.isEmpty()) {
+                if (filter.getIdentifierTypes().isEmpty()) {
+                    idTypes.clear();
+                } else {
+                    idTypes.addAll(filter.getIdentifierTypes());
+                }
+            }
+
+            if (!ids.isEmpty()) {
+                if (filter.getIdentifiers().isEmpty()) {
+                    ids.clear();
+                } else {
+                    ids.addAll(filter.getIdentifiers());
+                }
+            }
+
+            includeCategories |= filter.areCategoriesIncluded();
+            excludeModifications &= filter.areModificationsExcluded();
+        }
+
+        return idTypes == null ? null : new android.hardware.radio.ProgramList.Filter(idTypes, ids,
+                includeCategories, excludeModifications);
+    }
+
+    void onTunerSessionProgramListFilterChanged(@Nullable TunerSession session) {
+        synchronized (mLock) {
+            onTunerSessionProgramListFilterChangedLocked(session);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void onTunerSessionProgramListFilterChangedLocked(@Nullable TunerSession session) {
+        android.hardware.radio.ProgramList.Filter newFilter =
+                buildUnionOfTunerSessionFiltersLocked();
+        if (newFilter == null) {
+            // If there are no AIDL clients remaining, we can stop updates from the HAL as well.
+            if (mUnionOfAidlProgramFilters == null) {
+                return;
+            }
+            mUnionOfAidlProgramFilters = null;
+            try {
+                mService.stopProgramListUpdates();
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, ex, "mHalTunerSession.stopProgramListUpdates() failed");
+            }
+            return;
+        }
+
+        synchronized (mLock) {
+            // If the HAL filter doesn't change, we can immediately send an update to the AIDL
+            // client.
+            if (newFilter.equals(mUnionOfAidlProgramFilters)) {
+                if (session != null) {
+                    session.updateProgramInfoFromHalCache(mProgramInfoCache);
+                }
+                return;
+            }
+
+            // Otherwise, update the HAL's filter, and AIDL clients will be updated when
+            // mHalTunerCallback.onProgramListUpdated() is called.
+            mUnionOfAidlProgramFilters = newFilter;
+            try {
+                mService.startProgramListUpdates(
+                        ConversionUtils.filterToHalProgramFilter(newFilter));
+            } catch (RuntimeException ex) {
+                throw ConversionUtils.throwOnError(ex, /* action= */ "Start Program ListUpdates");
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, ex, "mHalTunerSession.startProgramListUpdates() failed");
+            }
+        }
+    }
+
+    void onTunerSessionClosed(TunerSession tunerSession) {
+        synchronized (mLock) {
+            onTunerSessionsClosedLocked(tunerSession);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void onTunerSessionsClosedLocked(TunerSession... tunerSessions) {
+        for (TunerSession tunerSession : tunerSessions) {
+            mAidlTunerSessions.remove(tunerSession);
+        }
+        onTunerSessionProgramListFilterChanged(null);
+    }
+
+    // add to mHandler queue
+    private void fireLater(Runnable r) {
+        mHandler.post(() -> r.run());
+    }
+
+    interface AidlCallbackRunnable {
+        void run(android.hardware.radio.ITunerCallback callback) throws RemoteException;
+    }
+
+    // Invokes runnable with each TunerSession currently open.
+    void fanoutAidlCallback(AidlCallbackRunnable runnable) {
+        fireLater(() -> {
+            synchronized (mLock) {
+                fanoutAidlCallbackLocked(runnable);
+            }
+        });
+    }
+
+    @GuardedBy("mLock")
+    private void fanoutAidlCallbackLocked(AidlCallbackRunnable runnable) {
+        List<TunerSession> deadSessions = null;
+        for (int i = 0; i < mAidlTunerSessions.size(); i++) {
+            try {
+                runnable.run(mAidlTunerSessions.valueAt(i).mCallback);
+            } catch (DeadObjectException ex) {
+                // The other side died without calling close(), so just purge it from our records.
+                Slogf.e(TAG, "Removing dead TunerSession");
+                if (deadSessions == null) {
+                    deadSessions = new ArrayList<>();
+                }
+                deadSessions.add(mAidlTunerSessions.valueAt(i));
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, ex, "Failed to invoke ITunerCallback");
+            }
+        }
+        if (deadSessions != null) {
+            onTunerSessionsClosedLocked(deadSessions.toArray(
+                    new TunerSession[deadSessions.size()]));
+        }
+    }
+
+    public android.hardware.radio.ICloseHandle addAnnouncementListener(
+            android.hardware.radio.IAnnouncementListener listener,
+            int[] enabledTypes) throws RemoteException {
+        mLogger.logRadioEvent("Add AnnouncementListener");
+        byte[] enabledList = new byte[enabledTypes.length];
+        for (int index = 0; index < enabledList.length; index++) {
+            enabledList[index] = (byte) enabledTypes[index];
+        }
+
+        final ICloseHandle[] hwCloseHandle = {null};
+        IAnnouncementListener hwListener = new IAnnouncementListener.Stub() {
+            public int getInterfaceVersion() {
+                return this.VERSION;
+            }
+
+            public String getInterfaceHash() {
+                return this.HASH;
+            }
+
+            public void onListUpdated(Announcement[] hwAnnouncements)
+                    throws RemoteException {
+                List<android.hardware.radio.Announcement> announcements =
+                        new ArrayList<>(hwAnnouncements.length);
+                for (int i = 0; i < hwAnnouncements.length; i++) {
+                    announcements.add(
+                            ConversionUtils.announcementFromHalAnnouncement(hwAnnouncements[i]));
+                }
+                listener.onListUpdated(announcements);
+            }
+        };
+
+        synchronized (mLock) {
+            try {
+                hwCloseHandle[0] = mService.registerAnnouncementListener(hwListener, enabledList);
+            } catch (RuntimeException ex) {
+                throw ConversionUtils.throwOnError(ex, /* action= */ "AnnouncementListener");
+            }
+        }
+
+        return new android.hardware.radio.ICloseHandle.Stub() {
+            public void close() {
+                try {
+                    hwCloseHandle[0].close();
+                } catch (RemoteException ex) {
+                    Slogf.e(TAG, ex, "Failed closing announcement listener");
+                }
+                hwCloseHandle[0] = null;
+            }
+        };
+    }
+
+    Bitmap getImage(int id) {
+        mLogger.logRadioEvent("Get image for id = %d", id);
+        if (id == 0) throw new IllegalArgumentException("Image ID is missing");
+
+        byte[] rawImage;
+        synchronized (mLock) {
+            try {
+                rawImage = mService.getImage(id);
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
+            }
+        }
+
+        if (rawImage == null || rawImage.length == 0) return null;
+
+        return BitmapFactory.decodeByteArray(rawImage, 0, rawImage.length);
+    }
+
+    void dumpInfo(IndentingPrintWriter pw) {
+        pw.printf("RadioModule\n");
+
+        pw.increaseIndent();
+        synchronized (mLock) {
+            pw.printf("BroadcastRadioServiceImpl: %s\n", mService);
+            pw.printf("Properties: %s\n", mProperties);
+            pw.printf("Antenna state: ");
+            if (mAntennaConnected == null) {
+                pw.printf("undetermined\n");
+            } else {
+                pw.printf("%s\n", mAntennaConnected ? "connected" : "not connected");
+            }
+            pw.printf("current ProgramInfo: %s\n", mCurrentProgramInfo);
+            pw.printf("ProgramInfoCache: %s\n", mProgramInfoCache);
+            pw.printf("Union of AIDL ProgramFilters: %s\n", mUnionOfAidlProgramFilters);
+            pw.printf("AIDL TunerSessions [%d]:\n", mAidlTunerSessions.size());
+
+            pw.increaseIndent();
+            for (int i = 0; i < mAidlTunerSessions.size(); i++) {
+                mAidlTunerSessions.valueAt(i).dumpInfo(pw);
+            }
+            pw.decreaseIndent();
+        }
+        pw.printf("Radio module events:\n");
+
+        pw.increaseIndent();
+        mLogger.dump(pw);
+        pw.decreaseIndent();
+
+        pw.decreaseIndent();
+    }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
new file mode 100644
index 0000000..7c26a87
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
@@ -0,0 +1,390 @@
+/*
+ * 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.broadcastradio.aidl;
+
+import android.annotation.Nullable;
+import android.graphics.Bitmap;
+import android.hardware.broadcastradio.ConfigFlag;
+import android.hardware.broadcastradio.IBroadcastRadio;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.os.RemoteException;
+import android.util.ArraySet;
+import android.util.IndentingPrintWriter;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.utils.Slogf;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+final class TunerSession extends ITuner.Stub {
+    private static final String TAG = "BcRadioAidlSrv.session";
+    private static final int TUNER_EVENT_LOGGER_QUEUE_SIZE = 25;
+
+    private final Object mLock;
+
+    private final RadioLogger mLogger;
+    private final RadioModule mModule;
+    final android.hardware.radio.ITunerCallback mCallback;
+    private final IBroadcastRadio mService;
+
+    @GuardedBy("mLock")
+    private boolean mIsClosed;
+    @GuardedBy("mLock")
+    private boolean mIsMuted;
+    @GuardedBy("mLock")
+    private ProgramInfoCache mProgramInfoCache;
+
+    // necessary only for older APIs compatibility
+    @GuardedBy("mLock")
+    private RadioManager.BandConfig mPlaceHolderConfig;
+
+    TunerSession(RadioModule radioModule, IBroadcastRadio service,
+            android.hardware.radio.ITunerCallback callback,
+            Object lock) {
+        mModule = Objects.requireNonNull(radioModule, "radioModule cannot be null");
+        mService = Objects.requireNonNull(service, "service cannot be null");
+        mCallback = Objects.requireNonNull(callback, "callback cannot be null");
+        mLock = Objects.requireNonNull(lock, "lock cannot be null");
+        mLogger = new RadioLogger(TAG, TUNER_EVENT_LOGGER_QUEUE_SIZE);
+    }
+
+    @Override
+    public void close() {
+        mLogger.logRadioEvent("Close tuner session");
+        close(null);
+    }
+
+    /**
+     * Closes the TunerSession. If error is non-null, the client's onError() callback is invoked
+     * first with the specified error, see {@link
+     * android.hardware.radio.RadioTuner.Callback#onError}.
+     *
+     * @param error Error to send to client before session is closed. If null, there is no error
+     *              when closing the session.
+     */
+    public void close(@Nullable Integer error) {
+        if (error == null) {
+            mLogger.logRadioEvent("Close tuner session on error null");
+        } else {
+            mLogger.logRadioEvent("Close tuner session on error %d", error);
+        }
+        synchronized (mLock) {
+            if (mIsClosed) return;
+            if (error != null) {
+                try {
+                    mCallback.onError(error);
+                } catch (RemoteException ex) {
+                    Slogf.w(TAG, ex, "mCallback.onError(%s) failed", error);
+                }
+            }
+            mIsClosed = true;
+            mModule.onTunerSessionClosed(this);
+        }
+    }
+
+    @Override
+    public boolean isClosed() {
+        synchronized (mLock) {
+            return mIsClosed;
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void checkNotClosedLocked() {
+        if (mIsClosed) {
+            throw new IllegalStateException("Tuner is closed, no further operations are allowed");
+        }
+    }
+
+    @Override
+    public void setConfiguration(RadioManager.BandConfig config) {
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            mPlaceHolderConfig = Objects.requireNonNull(config, "config cannot be null");
+        }
+        Slogf.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL AIDL");
+        mModule.fanoutAidlCallback(cb -> cb.onConfigurationChanged(config));
+    }
+
+    @Override
+    public RadioManager.BandConfig getConfiguration() {
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            return mPlaceHolderConfig;
+        }
+    }
+
+    @Override
+    public void setMuted(boolean mute) {
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            if (mIsMuted == mute) return;
+            mIsMuted = mute;
+        }
+        Slogf.w(TAG, "Mute %b via RadioService is not implemented - please handle it via app",
+                mute);
+    }
+
+    @Override
+    public boolean isMuted() {
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            return mIsMuted;
+        }
+    }
+
+    @Override
+    public void step(boolean directionDown, boolean skipSubChannel) throws RemoteException {
+        mLogger.logRadioEvent("Step with direction %s, skipSubChannel?  %s",
+                directionDown ? "down" : "up", skipSubChannel ? "yes" : "no");
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            try {
+                mService.step(!directionDown);
+            } catch (RuntimeException ex) {
+                throw ConversionUtils.throwOnError(ex, /* action= */ "step");
+            }
+        }
+    }
+
+    @Override
+    public void scan(boolean directionDown, boolean skipSubChannel) throws RemoteException {
+        mLogger.logRadioEvent("Scan with direction %s, skipSubChannel? %s",
+                directionDown ? "down" : "up", skipSubChannel ? "yes" : "no");
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            try {
+                mService.seek(!directionDown, skipSubChannel);
+            } catch (RuntimeException ex) {
+                throw ConversionUtils.throwOnError(ex, /* action= */ "seek");
+            }
+        }
+    }
+
+    @Override
+    public void tune(ProgramSelector selector) throws RemoteException {
+        mLogger.logRadioEvent("Tune with selector %s", selector);
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            try {
+                mService.tune(ConversionUtils.programSelectorToHalProgramSelector(selector));
+            } catch (RuntimeException ex) {
+                throw ConversionUtils.throwOnError(ex, /* action= */ "tune");
+            }
+        }
+    }
+
+    @Override
+    public void cancel() {
+        Slogf.i(TAG, "Cancel");
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            try {
+                mService.cancel();
+            } catch (RemoteException ex) {
+                Slogf.e(TAG, "Failed to cancel tuner session");
+                throw ex.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    @Override
+    public void cancelAnnouncement() {
+        // TODO(b/244485175): deperacte cancelAnnouncement
+        Slogf.i(TAG, "Announcements control doesn't involve cancelling at the HAL level in AIDL");
+    }
+
+    @Override
+    public Bitmap getImage(int id) {
+        mLogger.logRadioEvent("Get image for %d", id);
+        return mModule.getImage(id);
+    }
+
+    @Override
+    public boolean startBackgroundScan() {
+        Slogf.i(TAG, "Explicit background scan trigger is not supported with HAL AIDL");
+        mModule.fanoutAidlCallback(ITunerCallback::onBackgroundScanComplete);
+        return true;
+    }
+
+    @Override
+    public void startProgramListUpdates(ProgramList.Filter filter) throws RemoteException {
+        mLogger.logRadioEvent("Start programList updates %s", filter);
+        // If the AIDL client provides a null filter, it wants all updates, so use the most broad
+        // filter.
+        if (filter == null) {
+            filter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(),
+                    /* includeCategories= */ true,
+                    /* excludeModifications= */ false);
+        }
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            mProgramInfoCache = new ProgramInfoCache(filter);
+        }
+        // Note: RadioModule.onTunerSessionProgramListFilterChanged() must be called without mLock
+        // held since it can call getProgramListFilter() and onHalProgramInfoUpdated().
+        mModule.onTunerSessionProgramListFilterChanged(this);
+    }
+
+    ProgramList.Filter getProgramListFilter() {
+        synchronized (mLock) {
+            return mProgramInfoCache == null ? null : mProgramInfoCache.getFilter();
+        }
+    }
+
+    void onMergedProgramListUpdateFromHal(ProgramList.Chunk mergedChunk) {
+        List<ProgramList.Chunk> clientUpdateChunks;
+        synchronized (mLock) {
+            if (mProgramInfoCache == null) {
+                return;
+            }
+            clientUpdateChunks = mProgramInfoCache.filterAndApplyChunk(mergedChunk);
+        }
+        dispatchClientUpdateChunks(clientUpdateChunks);
+    }
+
+    void updateProgramInfoFromHalCache(ProgramInfoCache halCache) {
+        List<ProgramList.Chunk> clientUpdateChunks;
+        synchronized (mLock) {
+            if (mProgramInfoCache == null) {
+                return;
+            }
+            clientUpdateChunks = mProgramInfoCache.filterAndUpdateFromInternal(
+                    halCache, /* purge = */ true);
+        }
+        dispatchClientUpdateChunks(clientUpdateChunks);
+    }
+
+    private void dispatchClientUpdateChunks(@Nullable List<ProgramList.Chunk> chunks) {
+        if (chunks == null) {
+            return;
+        }
+        for (int i = 0; i < chunks.size(); i++) {
+            try {
+                mCallback.onProgramListUpdated(chunks.get(i));
+            } catch (RemoteException ex) {
+                Slogf.w(TAG, ex, "mCallback.onProgramListUpdated() failed");
+            }
+        }
+    }
+
+    @Override
+    public void stopProgramListUpdates() throws RemoteException {
+        mLogger.logRadioEvent("Stop programList updates");
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            mProgramInfoCache = null;
+        }
+        // Note: RadioModule.onTunerSessionProgramListFilterChanged() must be called without mLock
+        // held since it can call getProgramListFilter() and onHalProgramInfoUpdated().
+        mModule.onTunerSessionProgramListFilterChanged(this);
+    }
+
+    @Override
+    public boolean isConfigFlagSupported(int flag) {
+        try {
+            isConfigFlagSet(flag);
+            return true;
+        } catch (IllegalStateException | UnsupportedOperationException ex) {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean isConfigFlagSet(int flag) {
+        mLogger.logRadioEvent("is ConfigFlag %s set? ", ConfigFlag.$.toString(flag));
+        synchronized (mLock) {
+            checkNotClosedLocked();
+
+            try {
+                return mService.isConfigFlagSet(flag);
+            } catch (RuntimeException ex) {
+                throw ConversionUtils.throwOnError(ex, /* action= */ "isConfigFlagSet");
+            } catch (RemoteException ex) {
+                throw new RuntimeException("Failed to check flag " + ConfigFlag.$.toString(flag),
+                        ex);
+            }
+        }
+    }
+
+    @Override
+    public void setConfigFlag(int flag, boolean value) throws RemoteException {
+        mLogger.logRadioEvent("set ConfigFlag %s to %b ",
+                ConfigFlag.$.toString(flag), value);
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            try {
+                mService.setConfigFlag(flag, value);
+            } catch (RuntimeException ex) {
+                throw ConversionUtils.throwOnError(ex, /* action= */ "setConfigFlag");
+            }
+        }
+    }
+
+    @Override
+    public Map<String, String> setParameters(Map<String, String> parameters) {
+        mLogger.logRadioEvent("Set parameters ");
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            try {
+                return ConversionUtils.vendorInfoFromHalVendorKeyValues(mService.setParameters(
+                        ConversionUtils.vendorInfoToHalVendorKeyValues(parameters)));
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    @Override
+    public Map<String, String> getParameters(List<String> keys) {
+        mLogger.logRadioEvent("Get parameters ");
+        synchronized (mLock) {
+            checkNotClosedLocked();
+            try {
+                return ConversionUtils.vendorInfoFromHalVendorKeyValues(
+                        mService.getParameters(keys.toArray(new String[0])));
+            } catch (RemoteException ex) {
+                throw ex.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    void dumpInfo(IndentingPrintWriter pw) {
+        pw.printf("TunerSession\n");
+
+        pw.increaseIndent();
+        synchronized (mLock) {
+            pw.printf("Is session closed? %s\n", mIsClosed ? "Yes" : "No");
+            pw.printf("Is muted? %s\n", mIsMuted ? "Yes" : "No");
+            pw.printf("ProgramInfoCache: %s\n", mProgramInfoCache);
+            pw.printf("Config: %s\n", mPlaceHolderConfig);
+        }
+        pw.printf("Tuner session events:\n");
+
+        pw.increaseIndent();
+        mLogger.dump(pw);
+        pw.decreaseIndent();
+
+        pw.decreaseIndent();
+    }
+}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/Utils.java b/services/core/java/com/android/server/broadcastradio/aidl/Utils.java
new file mode 100644
index 0000000..b6bea5b
--- /dev/null
+++ b/services/core/java/com/android/server/broadcastradio/aidl/Utils.java
@@ -0,0 +1,44 @@
+/*
+ * 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.broadcastradio.aidl;
+
+final class Utils {
+
+    private Utils() {
+        throw new UnsupportedOperationException("Utils class is noninstantiable");
+    }
+
+    public enum FrequencyBand {
+        UNKNOWN,
+        FM,
+        AM_LW,
+        AM_MW,
+        AM_SW,
+    }
+
+    static FrequencyBand getBand(int freq) {
+        // keep in sync with hardware/interfaces/broadcastradio/common/utilsaidl/Utils.cpp
+        if (freq < 30) return FrequencyBand.UNKNOWN;
+        if (freq < 500) return FrequencyBand.AM_LW;
+        if (freq < 1705) return FrequencyBand.AM_MW;
+        if (freq < 30000) return FrequencyBand.AM_SW;
+        if (freq < 60000) return FrequencyBand.UNKNOWN;
+        if (freq < 110000) return FrequencyBand.FM;
+        return FrequencyBand.UNKNOWN;
+    }
+
+}
diff --git a/services/core/java/com/android/server/content/SyncStorageEngine.java b/services/core/java/com/android/server/content/SyncStorageEngine.java
index 2c6257f..5c679b8 100644
--- a/services/core/java/com/android/server/content/SyncStorageEngine.java
+++ b/services/core/java/com/android/server/content/SyncStorageEngine.java
@@ -2083,7 +2083,7 @@
             try (FileInputStream in = mStatusFile.openRead()) {
                 readStatusInfoLocked(in);
             }
-        } catch (IOException e) {
+        } catch (Exception e) {
             Slog.e(TAG, "Unable to read status info file.", e);
         }
     }
@@ -2483,7 +2483,7 @@
             try (FileInputStream in = mStatisticsFile.openRead()) {
                 readDayStatsLocked(in);
             }
-        } catch (IOException e) {
+        } catch (Exception e) {
             Slog.e(TAG, "Unable to read day stats file.", e);
         }
     }
diff --git a/services/core/java/com/android/server/display/WifiDisplayController.java b/services/core/java/com/android/server/display/WifiDisplayController.java
index 5b204ad..f6d06aa 100644
--- a/services/core/java/com/android/server/display/WifiDisplayController.java
+++ b/services/core/java/com/android/server/display/WifiDisplayController.java
@@ -885,7 +885,7 @@
                     }
                 });
             }
-        } else {
+        } else if (!networkInfo.isConnectedOrConnecting()) {
             mConnectedDeviceGroupInfo = null;
 
             // Disconnect if we lost the network while connecting or connected to a display.
diff --git a/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java b/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
index e7f6e67..349b94b 100644
--- a/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
+++ b/services/core/java/com/android/server/location/gnss/GnssListenerMultiplexer.java
@@ -425,18 +425,15 @@
     }
 
     private void onPackageReset(String packageName) {
-        // invoked when a package is "force quit" - move off the main thread
-        FgThread.getExecutor().execute(
-                () ->
-                        updateRegistrations(
-                                registration -> {
-                                    if (registration.getIdentity().getPackageName().equals(
-                                            packageName)) {
-                                        registration.remove();
-                                    }
+        updateRegistrations(
+                registration -> {
+                    if (registration.getIdentity().getPackageName().equals(
+                            packageName)) {
+                        registration.remove();
+                    }
 
-                                    return false;
-                                }));
+                    return false;
+                });
     }
 
     private boolean isResetableForPackage(String packageName) {
diff --git a/services/core/java/com/android/server/location/injector/SystemPackageResetHelper.java b/services/core/java/com/android/server/location/injector/SystemPackageResetHelper.java
index 91b0212..b0fe55d 100644
--- a/services/core/java/com/android/server/location/injector/SystemPackageResetHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemPackageResetHelper.java
@@ -27,6 +27,7 @@
 import android.net.Uri;
 
 import com.android.internal.util.Preconditions;
+import com.android.server.FgThread;
 
 /** Listens to appropriate broadcasts for queries and resets. */
 public class SystemPackageResetHelper extends PackageResetHelper {
@@ -120,7 +121,9 @@
                                     context.getPackageManager().getApplicationInfo(packageName,
                                             PackageManager.ApplicationInfoFlags.of(0));
                             if (!appInfo.enabled) {
-                                notifyPackageReset(packageName);
+                                // move off main thread
+                                FgThread.getExecutor().execute(
+                                        () -> notifyPackageReset(packageName));
                             }
                         } catch (PackageManager.NameNotFoundException e) {
                             return;
@@ -130,7 +133,8 @@
                 case Intent.ACTION_PACKAGE_REMOVED:
                     // fall through
                 case Intent.ACTION_PACKAGE_RESTARTED:
-                    notifyPackageReset(packageName);
+                    // move off main thread
+                    FgThread.getExecutor().execute(() -> notifyPackageReset(packageName));
                     break;
                 default:
                     break;
diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
index bd75251..338a995 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -2409,18 +2409,15 @@
     }
 
     private void onPackageReset(String packageName) {
-        // invoked when a package is "force quit" - move off the main thread
-        FgThread.getExecutor().execute(
-                () ->
-                        updateRegistrations(
-                                registration -> {
-                                    if (registration.getIdentity().getPackageName().equals(
-                                            packageName)) {
-                                        registration.remove();
-                                    }
+        updateRegistrations(
+                registration -> {
+                    if (registration.getIdentity().getPackageName().equals(
+                            packageName)) {
+                        registration.remove();
+                    }
 
-                                    return false;
-                                }));
+                    return false;
+                });
     }
 
     private boolean isResetableForPackage(String packageName) {
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 3211ca1..50bb051 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2161,7 +2161,7 @@
     }
 
     public final String resolveExternalPackageName(AndroidPackage pkg) {
-        if (pkg.getStaticSharedLibName() != null) {
+        if (pkg.getStaticSharedLibraryName() != null) {
             return pkg.getManifestPackageName();
         }
         return pkg.getPackageName();
@@ -2378,7 +2378,7 @@
         }
 
         final SharedLibraryInfo libraryInfo = getSharedLibraryInfo(
-                ps.getPkg().getStaticSharedLibName(), ps.getPkg().getStaticSharedLibVersion());
+                ps.getPkg().getStaticSharedLibraryName(), ps.getPkg().getStaticSharedLibVersion());
         if (libraryInfo == null) {
             return false;
         }
@@ -2434,7 +2434,7 @@
         }
 
         final SharedLibraryInfo libraryInfo = getSharedLibraryInfo(
-                ps.getPkg().getSdkLibName(), ps.getPkg().getSdkLibVersionMajor());
+                ps.getPkg().getSdkLibraryName(), ps.getPkg().getSdkLibVersionMajor());
         if (libraryInfo == null) {
             return false;
         }
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index 7242a56..7ff91f82 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -184,11 +184,11 @@
 
             if (pkg != null) {
                 SharedLibraryInfo libraryInfo = null;
-                if (pkg.getStaticSharedLibName() != null) {
-                    libraryInfo = computer.getSharedLibraryInfo(pkg.getStaticSharedLibName(),
+                if (pkg.getStaticSharedLibraryName() != null) {
+                    libraryInfo = computer.getSharedLibraryInfo(pkg.getStaticSharedLibraryName(),
                             pkg.getStaticSharedLibVersion());
-                } else if (pkg.getSdkLibName() != null) {
-                    libraryInfo = computer.getSharedLibraryInfo(pkg.getSdkLibName(),
+                } else if (pkg.getSdkLibraryName() != null) {
+                    libraryInfo = computer.getSharedLibraryInfo(pkg.getSdkLibraryName(),
                             pkg.getSdkLibVersionMajor());
                 }
 
@@ -544,7 +544,7 @@
             }
             outInfo.mRemovedPackage = ps.getPackageName();
             outInfo.mInstallerPackageName = ps.getInstallSource().installerPackageName;
-            outInfo.mIsStaticSharedLib = pkg != null && pkg.getStaticSharedLibName() != null;
+            outInfo.mIsStaticSharedLib = pkg != null && pkg.getStaticSharedLibraryName() != null;
             outInfo.mRemovedAppId = ps.getAppId();
             outInfo.mRemovedUsers = userIds;
             outInfo.mBroadcastUsers = userIds;
@@ -895,8 +895,8 @@
             final String packageName = ps.getPkg().getPackageName();
             // Skip over if system app, static shared library or and SDK library.
             if ((ps.getFlags() & ApplicationInfo.FLAG_SYSTEM) != 0
-                    || !TextUtils.isEmpty(ps.getPkg().getStaticSharedLibName())
-                    || !TextUtils.isEmpty(ps.getPkg().getSdkLibName())) {
+                    || !TextUtils.isEmpty(ps.getPkg().getStaticSharedLibraryName())
+                    || !TextUtils.isEmpty(ps.getPkg().getSdkLibraryName())) {
                 continue;
             }
             if (DEBUG_CLEAN_APKS) {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 1746d93..81d47a0 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -168,6 +168,7 @@
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.SharedLibraryWrapper;
 import com.android.server.pm.pkg.component.ComponentMutateUtils;
 import com.android.server.pm.pkg.component.ParsedInstrumentation;
 import com.android.server.pm.pkg.component.ParsedPermission;
@@ -351,7 +352,7 @@
         }
 
         if (reconciledPkg.mCollectedSharedLibraryInfos != null
-                || (oldPkgSetting != null && oldPkgSetting.getUsesLibraryInfos() != null)) {
+                || (oldPkgSetting != null && oldPkgSetting.getUsesLibraries() != null)) {
             // Reconcile if the new package or the old package uses shared libraries.
             // It is possible that the old package uses shared libraries but the new one doesn't.
             mSharedLibraries.executeSharedLibrariesUpdate(pkg, pkgSetting, null, null,
@@ -442,7 +443,7 @@
         // Also need to kill any apps that are dependent on the library, except the case of
         // installation of new version static shared library.
         if (clientLibPkgs != null) {
-            if (pkg.getStaticSharedLibName() == null || isReplace) {
+            if (pkg.getStaticSharedLibraryName() == null || isReplace) {
                 for (int i = 0; i < clientLibPkgs.size(); i++) {
                     AndroidPackage clientPkg = clientLibPkgs.get(i);
                     mPm.killApplication(clientPkg.getPackageName(),
@@ -1141,7 +1142,7 @@
             if (signatureCheckPs == null && parsedPackage.isSdkLibrary()) {
                 WatchedLongSparseArray<SharedLibraryInfo> libraryInfos =
                         mSharedLibraries.getSharedLibraryInfos(
-                                parsedPackage.getSdkLibName());
+                                parsedPackage.getSdkLibraryName());
                 if (libraryInfos != null && libraryInfos.size() > 0) {
                     // Any existing version would do.
                     SharedLibraryInfo libraryInfo = libraryInfos.valueAt(0);
@@ -1578,7 +1579,7 @@
                 removedInfo.mInstallerPackageName =
                         ps.getInstallSource().installerPackageName;
                 removedInfo.mIsStaticSharedLib =
-                        parsedPackage.getStaticSharedLibName() != null;
+                        parsedPackage.getStaticSharedLibraryName() != null;
                 removedInfo.mIsUpdate = true;
                 removedInfo.mOrigUsers = installedUsers;
                 removedInfo.mInstallReasons = new SparseIntArray(installedUsers.length);
@@ -2059,9 +2060,9 @@
 
                 // Retrieve the overlays for shared libraries of the package.
                 if (!ps.getPkgState().getUsesLibraryInfos().isEmpty()) {
-                    for (SharedLibraryInfo sharedLib : ps.getPkgState().getUsesLibraryInfos()) {
+                    for (SharedLibraryWrapper sharedLib : ps.getPkgState().getUsesLibraryInfos()) {
                         for (int currentUserId : UserManagerService.getInstance().getUserIds()) {
-                            if (!sharedLib.isDynamic()) {
+                            if (sharedLib.getType() != SharedLibraryInfo.TYPE_DYNAMIC) {
                                 // TODO(146804378): Support overlaying static shared libraries
                                 continue;
                             }
@@ -2684,7 +2685,7 @@
             }
 
             // Send installed broadcasts if the package is not a static shared lib.
-            if (request.getPkg().getStaticSharedLibName() == null) {
+            if (request.getPkg().getStaticSharedLibraryName() == null) {
                 mPm.mProcessLoggingHandler.invalidateBaseApkHash(request.getPkg().getBaseApkPath());
 
                 // Send added for users that see the package for the first time
@@ -2813,7 +2814,7 @@
                 // No need to kill consumers if it's installation of new version static shared lib.
                 final Computer snapshot = mPm.snapshotComputer();
                 final boolean dontKillApp = !update
-                        && request.getPkg().getStaticSharedLibName() != null;
+                        && request.getPkg().getStaticSharedLibraryName() != null;
                 for (int i = 0; i < request.getLibraryConsumers().size(); i++) {
                     AndroidPackage pkg = request.getLibraryConsumers().get(i);
                     // send broadcast that all consumers of the static shared library have changed
@@ -4297,7 +4298,7 @@
         long maxVersionCode = Long.MAX_VALUE;
 
         WatchedLongSparseArray<SharedLibraryInfo> versionedLib =
-                mSharedLibraries.getSharedLibraryInfos(pkg.getStaticSharedLibName());
+                mSharedLibraries.getSharedLibraryInfos(pkg.getStaticSharedLibraryName());
         if (versionedLib != null) {
             final int versionCount = versionedLib.size();
             for (int i = 0; i < versionCount; i++) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 9481f8a..2c460f8 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -210,7 +210,6 @@
 import com.android.server.pm.permission.PermissionManagerService;
 import com.android.server.pm.permission.PermissionManagerServiceInternal;
 import com.android.server.pm.pkg.AndroidPackage;
-import com.android.server.pm.pkg.PackageState;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.pkg.PackageUserState;
 import com.android.server.pm.pkg.PackageUserStateInternal;
@@ -223,7 +222,6 @@
 import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
 import com.android.server.pm.resolution.ComponentResolver;
 import com.android.server.pm.resolution.ComponentResolverApi;
-import com.android.server.pm.snapshot.PackageDataSnapshot;
 import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
 import com.android.server.pm.verify.domain.DomainVerificationService;
 import com.android.server.pm.verify.domain.proxy.DomainVerificationProxy;
@@ -5491,19 +5489,19 @@
                 AndroidPackage pkg = packageState.getPkg();
                 if (pkg != null) {
                     // Cannot hide SDK libs as they are controlled by SDK manager.
-                    if (pkg.getSdkLibName() != null) {
+                    if (pkg.getSdkLibraryName() != null) {
                         Slog.w(TAG, "Cannot hide package: " + packageName
                                 + " providing SDK library: "
-                                + pkg.getSdkLibName());
+                                + pkg.getSdkLibraryName());
                         return false;
                     }
                     // Cannot hide static shared libs as they are considered
                     // a part of the using app (emulating static linking). Also
                     // static libs are installed always on internal storage.
-                    if (pkg.getStaticSharedLibName() != null) {
+                    if (pkg.getStaticSharedLibraryName() != null) {
                         Slog.w(TAG, "Cannot hide package: " + packageName
                                 + " providing static shared library: "
-                                + pkg.getStaticSharedLibName());
+                                + pkg.getStaticSharedLibraryName());
                         return false;
                     }
                 }
@@ -5546,17 +5544,17 @@
             if (packageState != null && packageState.getPkg() != null) {
                 AndroidPackage pkg = packageState.getPkg();
                 // Cannot block uninstall SDK libs as they are controlled by SDK manager.
-                if (pkg.getSdkLibName() != null) {
+                if (pkg.getSdkLibraryName() != null) {
                     Slog.w(PackageManagerService.TAG, "Cannot block uninstall of package: " + packageName
-                            + " providing SDK library: " + pkg.getSdkLibName());
+                            + " providing SDK library: " + pkg.getSdkLibraryName());
                     return false;
                 }
                 // Cannot block uninstall of static shared libs as they are
                 // considered a part of the using app (emulating static linking).
                 // Also static libs are installed always on internal storage.
-                if (pkg.getStaticSharedLibName() != null) {
+                if (pkg.getStaticSharedLibraryName() != null) {
                     Slog.w(PackageManagerService.TAG, "Cannot block uninstall of package: " + packageName
-                            + " providing static shared library: " + pkg.getStaticSharedLibName());
+                            + " providing static shared library: " + pkg.getStaticSharedLibraryName());
                     return false;
                 }
             }
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 03f17bd..9050722 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -42,15 +42,17 @@
 import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.DataClass;
 import com.android.server.pm.parsing.pkg.AndroidPackageInternal;
-import com.android.server.pm.parsing.pkg.ParsedPackage;
 import com.android.server.pm.permission.LegacyPermissionDataProvider;
 import com.android.server.pm.permission.LegacyPermissionState;
 import com.android.server.pm.pkg.AndroidPackage;
 import com.android.server.pm.pkg.PackageState;
 import com.android.server.pm.pkg.PackageStateInternal;
 import com.android.server.pm.pkg.PackageStateUnserialized;
+import com.android.server.pm.pkg.PackageUserState;
 import com.android.server.pm.pkg.PackageUserStateImpl;
 import com.android.server.pm.pkg.PackageUserStateInternal;
+import com.android.server.pm.pkg.SharedLibrary;
+import com.android.server.pm.pkg.SharedLibraryWrapper;
 import com.android.server.pm.pkg.SuspendParams;
 import com.android.server.utils.SnapshotCache;
 import com.android.server.utils.WatchedArraySet;
@@ -1203,13 +1205,13 @@
 
     @NonNull
     @Override
-    public List<SharedLibraryInfo> getUsesLibraryInfos() {
-        return pkgState.getUsesLibraryInfos();
+    public List<SharedLibrary> getUsesLibraries() {
+        return (List<SharedLibrary>) (List<?>) pkgState.getUsesLibraryInfos();
     }
 
     @NonNull
     public PackageSetting addUsesLibraryInfo(@NonNull SharedLibraryInfo value) {
-        pkgState.addUsesLibraryInfo(value);
+        pkgState.addUsesLibraryInfo(new SharedLibraryWrapper(value));
         return this;
     }
 
@@ -1326,6 +1328,13 @@
         return this;
     }
 
+    @NonNull
+    @Override
+    public PackageUserState getStateForUser(@NonNull UserHandle user) {
+        PackageUserState userState = getUserStates().get(user.getIdentifier());
+        return userState == null ? PackageUserState.DEFAULT : userState;
+    }
+
 
 
     // Code below generated by codegen v1.0.23.
@@ -1487,10 +1496,10 @@
     }
 
     @DataClass.Generated(
-            time = 1659546705292L,
+            time = 1662666062860L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java",
-            inputSignatures = "private  int mSharedUserAppId\nprivate @android.annotation.Nullable java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mimeGroups\nprivate @java.lang.Deprecated @android.annotation.Nullable java.util.Set<java.lang.String> mOldCodePaths\nprivate @android.annotation.Nullable java.lang.String[] usesSdkLibraries\nprivate @android.annotation.Nullable long[] usesSdkLibrariesVersionsMajor\nprivate @android.annotation.Nullable java.lang.String[] usesStaticLibraries\nprivate @android.annotation.Nullable long[] usesStaticLibrariesVersions\nprivate @android.annotation.Nullable @java.lang.Deprecated java.lang.String legacyNativeLibraryPath\nprivate @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.Nullable java.lang.String mRealName\nprivate  int mAppId\nprivate @android.annotation.Nullable com.android.server.pm.parsing.pkg.AndroidPackageInternal pkg\nprivate @android.annotation.NonNull java.io.File mPath\nprivate @android.annotation.NonNull java.lang.String mPathString\nprivate  float mLoadingProgress\nprivate @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate  long mLastModifiedTime\nprivate  long lastUpdateTime\nprivate  long versionCode\nprivate @android.annotation.NonNull com.android.server.pm.PackageSignatures signatures\nprivate  boolean installPermissionsFixed\nprivate @android.annotation.NonNull com.android.server.pm.PackageKeySetData keySetData\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserStateImpl> mUserStates\nprivate @android.annotation.NonNull com.android.server.pm.InstallSource installSource\nprivate @android.annotation.Nullable java.lang.String volumeUuid\nprivate  int categoryOverride\nprivate  boolean updateAvailable\nprivate  boolean forceQueryableOverride\nprivate final @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized pkgState\nprivate @android.annotation.NonNull java.util.UUID mDomainSetId\nprivate final @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> mSnapshot\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> makeCache()\npublic  com.android.server.pm.PackageSetting snapshot()\npublic  void dumpDebug(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\npublic  com.android.server.pm.PackageSetting setAppId(int)\npublic  com.android.server.pm.PackageSetting setCpuAbiOverride(java.lang.String)\npublic  com.android.server.pm.PackageSetting setFirstInstallTimeFromReplaced(com.android.server.pm.pkg.PackageStateInternal,int[])\npublic  com.android.server.pm.PackageSetting setFirstInstallTime(long,int)\npublic  com.android.server.pm.PackageSetting setForceQueryableOverride(boolean)\npublic  com.android.server.pm.PackageSetting setInstallerPackageName(java.lang.String)\npublic  com.android.server.pm.PackageSetting setInstallSource(com.android.server.pm.InstallSource)\n  com.android.server.pm.PackageSetting removeInstallerPackage(java.lang.String)\npublic  com.android.server.pm.PackageSetting setIsOrphaned(boolean)\npublic  com.android.server.pm.PackageSetting setKeySetData(com.android.server.pm.PackageKeySetData)\npublic  com.android.server.pm.PackageSetting setLastModifiedTime(long)\npublic  com.android.server.pm.PackageSetting setLastUpdateTime(long)\npublic  com.android.server.pm.PackageSetting setLongVersionCode(long)\npublic  boolean setMimeGroup(java.lang.String,android.util.ArraySet<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setPkg(com.android.server.pm.parsing.pkg.AndroidPackageInternal)\npublic  com.android.server.pm.PackageSetting setPkgStateLibraryFiles(java.util.Collection<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setPrimaryCpuAbi(java.lang.String)\npublic  com.android.server.pm.PackageSetting setSecondaryCpuAbi(java.lang.String)\npublic  com.android.server.pm.PackageSetting setSignatures(com.android.server.pm.PackageSignatures)\npublic  com.android.server.pm.PackageSetting setVolumeUuid(java.lang.String)\npublic @java.lang.Override boolean isExternalStorage()\npublic  com.android.server.pm.PackageSetting setUpdateAvailable(boolean)\npublic  void setSharedUserAppId(int)\npublic @java.lang.Override int getSharedUserAppId()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override java.lang.String toString()\nprotected  void copyMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic  void updateFrom(com.android.server.pm.PackageSetting)\n  com.android.server.pm.PackageSetting updateMimeGroups(java.util.Set<java.lang.String>)\npublic @java.lang.Deprecated @java.lang.Override com.android.server.pm.permission.LegacyPermissionState getLegacyPermissionState()\npublic  com.android.server.pm.PackageSetting setInstallPermissionsFixed(boolean)\npublic  boolean isPrivileged()\npublic  boolean isOem()\npublic  boolean isVendor()\npublic  boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic  boolean isSystemExt()\npublic  boolean isOdm()\npublic  boolean isSystem()\npublic  android.content.pm.SigningDetails getSigningDetails()\npublic  com.android.server.pm.PackageSetting setSigningDetails(android.content.pm.SigningDetails)\npublic  void copyPackageSetting(com.android.server.pm.PackageSetting,boolean)\n @com.android.internal.annotations.VisibleForTesting com.android.server.pm.pkg.PackageUserStateImpl modifyUserState(int)\npublic  com.android.server.pm.pkg.PackageUserStateImpl getOrCreateUserState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateInternal readUserState(int)\n  void setEnabled(int,int,java.lang.String)\n  int getEnabled(int)\n  void setInstalled(boolean,int)\n  boolean getInstalled(int)\n  int getInstallReason(int)\n  void setInstallReason(int,int)\n  int getUninstallReason(int)\n  void setUninstallReason(int,int)\n @android.annotation.NonNull android.content.pm.overlay.OverlayPaths getOverlayPaths(int)\n  boolean setOverlayPathsForLibrary(java.lang.String,android.content.pm.overlay.OverlayPaths,int)\n  boolean isAnyInstalled(int[])\n  int[] queryInstalledUsers(int[],boolean)\n  long getCeDataInode(int)\n  void setCeDataInode(long,int)\n  boolean getStopped(int)\n  void setStopped(boolean,int)\n  boolean getNotLaunched(int)\n  void setNotLaunched(boolean,int)\n  boolean getHidden(int)\n  void setHidden(boolean,int)\n  int getDistractionFlags(int)\n  void setDistractionFlags(int,int)\npublic  boolean getInstantApp(int)\n  void setInstantApp(boolean,int)\n  boolean getVirtualPreload(int)\n  void setVirtualPreload(boolean,int)\n  void setUserState(int,long,int,boolean,boolean,boolean,boolean,int,android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>,boolean,boolean,java.lang.String,android.util.ArraySet<java.lang.String>,android.util.ArraySet<java.lang.String>,int,int,java.lang.String,java.lang.String,long)\n  void setUserState(int,com.android.server.pm.pkg.PackageUserStateInternal)\n  com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponents(int)\n  com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponents(int)\n  void setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setEnabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setDisabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  com.android.server.pm.pkg.PackageUserStateImpl modifyUserStateComponents(int,boolean,boolean)\n  void addDisabledComponent(java.lang.String,int)\n  void addEnabledComponent(java.lang.String,int)\n  boolean enableComponentLPw(java.lang.String,int)\n  boolean disableComponentLPw(java.lang.String,int)\n  boolean restoreComponentLPw(java.lang.String,int)\n  int getCurrentEnabledStateLPr(java.lang.String,int)\n  void removeUser(int)\npublic  int[] getNotInstalledUserIds()\n  void writePackageUserPermissionsProto(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\nprotected  void writeUsersInfoToProto(android.util.proto.ProtoOutputStream,long)\n  com.android.server.pm.PackageSetting setPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideNonLocalizedLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer,int)\npublic  void resetOverrideComponentLabelIcon(int)\npublic @android.annotation.Nullable java.lang.String getSplashScreenTheme(int)\npublic  boolean isLoading()\npublic  com.android.server.pm.PackageSetting setLoadingProgress(float)\npublic @android.annotation.NonNull @java.lang.Override long getVersionCode()\npublic @android.annotation.Nullable @java.lang.Override java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getMimeGroups()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String getPackageName()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.pm.pkg.AndroidPackage getAndroidPackage()\npublic @android.annotation.NonNull android.content.pm.SigningInfo getSigningInfo()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesSdkLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesSdkLibrariesVersionsMajor()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesStaticLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesStaticLibrariesVersions()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<android.content.pm.SharedLibraryInfo> getUsesLibraryInfos()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<java.lang.String> getUsesLibraryFiles()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @android.annotation.NonNull @java.lang.Override long[] getLastPackageUsageTime()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic  com.android.server.pm.PackageSetting setDomainSetId(java.util.UUID)\npublic  com.android.server.pm.PackageSetting setCategoryOverride(int)\npublic  com.android.server.pm.PackageSetting setLegacyNativeLibraryPath(java.lang.String)\npublic  com.android.server.pm.PackageSetting setMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic  com.android.server.pm.PackageSetting setOldCodePaths(java.util.Set<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setUsesSdkLibraries(java.lang.String[])\npublic  com.android.server.pm.PackageSetting setUsesSdkLibrariesVersionsMajor(long[])\npublic  com.android.server.pm.PackageSetting setUsesStaticLibraries(java.lang.String[])\npublic  com.android.server.pm.PackageSetting setUsesStaticLibrariesVersions(long[])\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageStateUnserialized getTransientState()\npublic @android.annotation.NonNull android.util.SparseArray<? extends PackageUserStateInternal> getUserStates()\npublic  com.android.server.pm.PackageSetting addMimeTypes(java.lang.String,java.util.Set<java.lang.String>)\nclass PackageSetting extends com.android.server.pm.SettingBase implements [com.android.server.pm.pkg.PackageStateInternal]\n@com.android.internal.util.DataClass(genGetters=true, genConstructor=false, genSetters=false, genBuilder=false)")
+            inputSignatures = "private  int mSharedUserAppId\nprivate @android.annotation.Nullable java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mimeGroups\nprivate @java.lang.Deprecated @android.annotation.Nullable java.util.Set<java.lang.String> mOldCodePaths\nprivate @android.annotation.Nullable java.lang.String[] usesSdkLibraries\nprivate @android.annotation.Nullable long[] usesSdkLibrariesVersionsMajor\nprivate @android.annotation.Nullable java.lang.String[] usesStaticLibraries\nprivate @android.annotation.Nullable long[] usesStaticLibrariesVersions\nprivate @android.annotation.Nullable @java.lang.Deprecated java.lang.String legacyNativeLibraryPath\nprivate @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.Nullable java.lang.String mRealName\nprivate  int mAppId\nprivate @android.annotation.Nullable com.android.server.pm.parsing.pkg.AndroidPackageInternal pkg\nprivate @android.annotation.NonNull java.io.File mPath\nprivate @android.annotation.NonNull java.lang.String mPathString\nprivate  float mLoadingProgress\nprivate @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate  long mLastModifiedTime\nprivate  long lastUpdateTime\nprivate  long versionCode\nprivate @android.annotation.NonNull com.android.server.pm.PackageSignatures signatures\nprivate  boolean installPermissionsFixed\nprivate @android.annotation.NonNull com.android.server.pm.PackageKeySetData keySetData\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserStateImpl> mUserStates\nprivate @android.annotation.NonNull com.android.server.pm.InstallSource installSource\nprivate @android.annotation.Nullable java.lang.String volumeUuid\nprivate  int categoryOverride\nprivate  boolean updateAvailable\nprivate  boolean forceQueryableOverride\nprivate final @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized pkgState\nprivate @android.annotation.NonNull java.util.UUID mDomainSetId\nprivate final @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> mSnapshot\nprivate  com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> makeCache()\npublic  com.android.server.pm.PackageSetting snapshot()\npublic  void dumpDebug(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\npublic  com.android.server.pm.PackageSetting setAppId(int)\npublic  com.android.server.pm.PackageSetting setCpuAbiOverride(java.lang.String)\npublic  com.android.server.pm.PackageSetting setFirstInstallTimeFromReplaced(com.android.server.pm.pkg.PackageStateInternal,int[])\npublic  com.android.server.pm.PackageSetting setFirstInstallTime(long,int)\npublic  com.android.server.pm.PackageSetting setForceQueryableOverride(boolean)\npublic  com.android.server.pm.PackageSetting setInstallerPackageName(java.lang.String)\npublic  com.android.server.pm.PackageSetting setInstallSource(com.android.server.pm.InstallSource)\n  com.android.server.pm.PackageSetting removeInstallerPackage(java.lang.String)\npublic  com.android.server.pm.PackageSetting setIsOrphaned(boolean)\npublic  com.android.server.pm.PackageSetting setKeySetData(com.android.server.pm.PackageKeySetData)\npublic  com.android.server.pm.PackageSetting setLastModifiedTime(long)\npublic  com.android.server.pm.PackageSetting setLastUpdateTime(long)\npublic  com.android.server.pm.PackageSetting setLongVersionCode(long)\npublic  boolean setMimeGroup(java.lang.String,android.util.ArraySet<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setPkg(com.android.server.pm.pkg.AndroidPackage)\npublic  com.android.server.pm.PackageSetting setPkgStateLibraryFiles(java.util.Collection<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setPrimaryCpuAbi(java.lang.String)\npublic  com.android.server.pm.PackageSetting setSecondaryCpuAbi(java.lang.String)\npublic  com.android.server.pm.PackageSetting setSignatures(com.android.server.pm.PackageSignatures)\npublic  com.android.server.pm.PackageSetting setVolumeUuid(java.lang.String)\npublic @java.lang.Override boolean isExternalStorage()\npublic  com.android.server.pm.PackageSetting setUpdateAvailable(boolean)\npublic  void setSharedUserAppId(int)\npublic @java.lang.Override int getSharedUserAppId()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override java.lang.String toString()\nprotected  void copyMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic  void updateFrom(com.android.server.pm.PackageSetting)\n  com.android.server.pm.PackageSetting updateMimeGroups(java.util.Set<java.lang.String>)\npublic @java.lang.Deprecated @java.lang.Override com.android.server.pm.permission.LegacyPermissionState getLegacyPermissionState()\npublic  com.android.server.pm.PackageSetting setInstallPermissionsFixed(boolean)\npublic  boolean isPrivileged()\npublic  boolean isOem()\npublic  boolean isVendor()\npublic  boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic  boolean isSystemExt()\npublic  boolean isOdm()\npublic  boolean isSystem()\npublic  android.content.pm.SigningDetails getSigningDetails()\npublic  com.android.server.pm.PackageSetting setSigningDetails(android.content.pm.SigningDetails)\npublic  void copyPackageSetting(com.android.server.pm.PackageSetting,boolean)\n @com.android.internal.annotations.VisibleForTesting com.android.server.pm.pkg.PackageUserStateImpl modifyUserState(int)\npublic  com.android.server.pm.pkg.PackageUserStateImpl getOrCreateUserState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateInternal readUserState(int)\n  void setEnabled(int,int,java.lang.String)\n  int getEnabled(int)\n  void setInstalled(boolean,int)\n  boolean getInstalled(int)\n  int getInstallReason(int)\n  void setInstallReason(int,int)\n  int getUninstallReason(int)\n  void setUninstallReason(int,int)\n @android.annotation.NonNull android.content.pm.overlay.OverlayPaths getOverlayPaths(int)\n  boolean setOverlayPathsForLibrary(java.lang.String,android.content.pm.overlay.OverlayPaths,int)\n  boolean isAnyInstalled(int[])\n  int[] queryInstalledUsers(int[],boolean)\n  long getCeDataInode(int)\n  void setCeDataInode(long,int)\n  boolean getStopped(int)\n  void setStopped(boolean,int)\n  boolean getNotLaunched(int)\n  void setNotLaunched(boolean,int)\n  boolean getHidden(int)\n  void setHidden(boolean,int)\n  int getDistractionFlags(int)\n  void setDistractionFlags(int,int)\npublic  boolean getInstantApp(int)\n  void setInstantApp(boolean,int)\n  boolean getVirtualPreload(int)\n  void setVirtualPreload(boolean,int)\n  void setUserState(int,long,int,boolean,boolean,boolean,boolean,int,android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>,boolean,boolean,java.lang.String,android.util.ArraySet<java.lang.String>,android.util.ArraySet<java.lang.String>,int,int,java.lang.String,java.lang.String,long)\n  void setUserState(int,com.android.server.pm.pkg.PackageUserStateInternal)\n  com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponents(int)\n  com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponents(int)\n  void setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setEnabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  void setDisabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n  com.android.server.pm.pkg.PackageUserStateImpl modifyUserStateComponents(int,boolean,boolean)\n  void addDisabledComponent(java.lang.String,int)\n  void addEnabledComponent(java.lang.String,int)\n  boolean enableComponentLPw(java.lang.String,int)\n  boolean disableComponentLPw(java.lang.String,int)\n  boolean restoreComponentLPw(java.lang.String,int)\n  int getCurrentEnabledStateLPr(java.lang.String,int)\n  void removeUser(int)\npublic  int[] getNotInstalledUserIds()\n  void writePackageUserPermissionsProto(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\nprotected  void writeUsersInfoToProto(android.util.proto.ProtoOutputStream,long)\n  com.android.server.pm.PackageSetting setPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideNonLocalizedLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer,int)\npublic  void resetOverrideComponentLabelIcon(int)\npublic @android.annotation.Nullable java.lang.String getSplashScreenTheme(int)\npublic  boolean isLoading()\npublic  com.android.server.pm.PackageSetting setLoadingProgress(float)\npublic @android.annotation.NonNull @java.lang.Override long getVersionCode()\npublic @android.annotation.Nullable @java.lang.Override java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getMimeGroups()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String getPackageName()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.pm.pkg.AndroidPackage getAndroidPackage()\npublic @android.annotation.NonNull android.content.pm.SigningInfo getSigningInfo()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesSdkLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesSdkLibrariesVersionsMajor()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesStaticLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesStaticLibrariesVersions()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<com.android.server.pm.pkg.SharedLibrary> getUsesLibraries()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryInfo(android.content.pm.SharedLibraryInfo)\npublic @android.annotation.NonNull @java.lang.Override java.util.List<java.lang.String> getUsesLibraryFiles()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryFile(java.lang.String)\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @android.annotation.NonNull @java.lang.Override long[] getLastPackageUsageTime()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic  com.android.server.pm.PackageSetting setDomainSetId(java.util.UUID)\npublic  com.android.server.pm.PackageSetting setCategoryOverride(int)\npublic  com.android.server.pm.PackageSetting setLegacyNativeLibraryPath(java.lang.String)\npublic  com.android.server.pm.PackageSetting setMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic  com.android.server.pm.PackageSetting setOldCodePaths(java.util.Set<java.lang.String>)\npublic  com.android.server.pm.PackageSetting setUsesSdkLibraries(java.lang.String[])\npublic  com.android.server.pm.PackageSetting setUsesSdkLibrariesVersionsMajor(long[])\npublic  com.android.server.pm.PackageSetting setUsesStaticLibraries(java.lang.String[])\npublic  com.android.server.pm.PackageSetting setUsesStaticLibrariesVersions(long[])\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageStateUnserialized getTransientState()\npublic @android.annotation.NonNull android.util.SparseArray<? extends PackageUserStateInternal> getUserStates()\npublic  com.android.server.pm.PackageSetting addMimeTypes(java.lang.String,java.util.Set<java.lang.String>)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\nclass PackageSetting extends com.android.server.pm.SettingBase implements [com.android.server.pm.pkg.PackageStateInternal]\n@com.android.internal.util.DataClass(genGetters=true, genConstructor=false, genSetters=false, genBuilder=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 55d4b36..bbc4fde 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -215,21 +215,21 @@
         r = null;
 
         // Any package can hold SDK or static shared libraries.
-        if (pkg.getSdkLibName() != null) {
+        if (pkg.getSdkLibraryName() != null) {
             if (mSharedLibraries.removeSharedLibrary(
-                    pkg.getSdkLibName(), pkg.getSdkLibVersionMajor())) {
+                    pkg.getSdkLibraryName(), pkg.getSdkLibVersionMajor())) {
                 if (DEBUG_REMOVE && chatty) {
                     if (r == null) {
                         r = new StringBuilder(256);
                     } else {
                         r.append(' ');
                     }
-                    r.append(pkg.getSdkLibName());
+                    r.append(pkg.getSdkLibraryName());
                 }
             }
         }
-        if (pkg.getStaticSharedLibName() != null) {
-            if (mSharedLibraries.removeSharedLibrary(pkg.getStaticSharedLibName(),
+        if (pkg.getStaticSharedLibraryName() != null) {
+            if (mSharedLibraries.removeSharedLibrary(pkg.getStaticSharedLibraryName(),
                     pkg.getStaticSharedLibVersion())) {
                 if (DEBUG_REMOVE && chatty) {
                     if (r == null) {
@@ -237,7 +237,7 @@
                     } else {
                         r.append(' ');
                     }
-                    r.append(pkg.getStaticSharedLibName());
+                    r.append(pkg.getStaticSharedLibraryName());
                 }
             }
         }
@@ -271,7 +271,7 @@
             outInfo.mRemovedPackage = packageName;
             outInfo.mInstallerPackageName = deletedPs.getInstallSource().installerPackageName;
             outInfo.mIsStaticSharedLib = deletedPkg != null
-                    && deletedPkg.getStaticSharedLibName() != null;
+                    && deletedPkg.getStaticSharedLibraryName() != null;
             outInfo.populateUsers(deletedPs.queryInstalledUsers(
                     mUserManagerInternal.getUserIds(), true), deletedPs);
             outInfo.mIsExternal = deletedPs.isExternalStorage();
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index 9bd8e12..bce6834 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -445,11 +445,11 @@
         }
 
         SharedLibraryInfo sdkLibraryInfo = null;
-        if (!TextUtils.isEmpty(parsedPackage.getSdkLibName())) {
+        if (!TextUtils.isEmpty(parsedPackage.getSdkLibraryName())) {
             sdkLibraryInfo = AndroidPackageUtils.createSharedLibraryForSdk(parsedPackage);
         }
         SharedLibraryInfo staticSharedLibraryInfo = null;
-        if (!TextUtils.isEmpty(parsedPackage.getStaticSharedLibName())) {
+        if (!TextUtils.isEmpty(parsedPackage.getStaticSharedLibraryName())) {
             staticSharedLibraryInfo =
                     AndroidPackageUtils.createSharedLibraryForStatic(parsedPackage);
         }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 80e9646..0558fbd 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -4667,17 +4667,17 @@
                             pw.println(libraryNames.get(i));
                 }
             }
-            if (pkg.getStaticSharedLibName() != null) {
+            if (pkg.getStaticSharedLibraryName() != null) {
                 pw.print(prefix); pw.println("  static library:");
                 pw.print(prefix); pw.print("    ");
-                pw.print("name:"); pw.print(pkg.getStaticSharedLibName());
+                pw.print("name:"); pw.print(pkg.getStaticSharedLibraryName());
                 pw.print(" version:"); pw.println(pkg.getStaticSharedLibVersion());
             }
 
-            if (pkg.getSdkLibName() != null) {
+            if (pkg.getSdkLibraryName() != null) {
                 pw.print(prefix); pw.println("  SDK library:");
                 pw.print(prefix); pw.print("    ");
-                pw.print("name:"); pw.print(pkg.getSdkLibName());
+                pw.print("name:"); pw.print(pkg.getSdkLibraryName());
                 pw.print(" versionMajor:"); pw.println(pkg.getSdkLibVersionMajor());
             }
 
diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
index 5905741..094e748 100644
--- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
+++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java
@@ -400,7 +400,7 @@
     @Nullable
     private SharedLibraryInfo getLatestStaticSharedLibraVersionLPr(@NonNull AndroidPackage pkg) {
         WatchedLongSparseArray<SharedLibraryInfo> versionedLib = mSharedLibraries.get(
-                pkg.getStaticSharedLibName());
+                pkg.getStaticSharedLibraryName());
         if (versionedLib == null) {
             return null;
         }
@@ -457,15 +457,15 @@
         // - Package manager is in a state where package isn't scanned yet. This will
         //   get called again after scanning to fix the dependencies.
         if (AndroidPackageUtils.isLibrary(pkg)) {
-            if (pkg.getSdkLibName() != null) {
+            if (pkg.getSdkLibraryName() != null) {
                 SharedLibraryInfo definedLibrary = getSharedLibraryInfo(
-                        pkg.getSdkLibName(), pkg.getSdkLibVersionMajor());
+                        pkg.getSdkLibraryName(), pkg.getSdkLibVersionMajor());
                 if (definedLibrary != null) {
                     action.accept(definedLibrary, libInfo);
                 }
-            } else if (pkg.getStaticSharedLibName() != null) {
+            } else if (pkg.getStaticSharedLibraryName() != null) {
                 SharedLibraryInfo definedLibrary = getSharedLibraryInfo(
-                        pkg.getStaticSharedLibName(), pkg.getStaticSharedLibVersion());
+                        pkg.getStaticSharedLibraryName(), pkg.getStaticSharedLibVersion());
                 if (definedLibrary != null) {
                     action.accept(definedLibrary, libInfo);
                 }
@@ -691,9 +691,9 @@
                         && !hasString(pkg.getUsesLibraries(), changingPkg.getLibraryNames())
                         && !hasString(pkg.getUsesOptionalLibraries(), changingPkg.getLibraryNames())
                         && !ArrayUtils.contains(pkg.getUsesStaticLibraries(),
-                        changingPkg.getStaticSharedLibName())
+                        changingPkg.getStaticSharedLibraryName())
                         && !ArrayUtils.contains(pkg.getUsesSdkLibraries(),
-                        changingPkg.getSdkLibName())) {
+                        changingPkg.getSdkLibraryName())) {
                     continue;
                 }
                 if (resultList == null) {
diff --git a/services/core/java/com/android/server/pm/SharedLibraryUtils.java b/services/core/java/com/android/server/pm/SharedLibraryUtils.java
index 274870d..2c28791 100644
--- a/services/core/java/com/android/server/pm/SharedLibraryUtils.java
+++ b/services/core/java/com/android/server/pm/SharedLibraryUtils.java
@@ -20,6 +20,7 @@
 import android.content.pm.SharedLibraryInfo;
 
 import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.SharedLibraryWrapper;
 import com.android.server.utils.WatchedLongSparseArray;
 
 import java.util.ArrayList;
@@ -79,8 +80,8 @@
         if (!pkgSetting.getTransientState().getUsesLibraryInfos().isEmpty()) {
             ArrayList<SharedLibraryInfo> retValue = new ArrayList<>();
             Set<String> collectedNames = new HashSet<>();
-            for (SharedLibraryInfo info : pkgSetting.getTransientState().getUsesLibraryInfos()) {
-                findSharedLibrariesRecursive(info, retValue, collectedNames);
+            for (SharedLibraryWrapper info : pkgSetting.getTransientState().getUsesLibraryInfos()) {
+                findSharedLibrariesRecursive(info.getInfo(), retValue, collectedNames);
             }
             return retValue;
         } else {
diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
index c3eb2fd..51bb412 100644
--- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java
+++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
@@ -548,7 +548,7 @@
                     if (pkg.isSdkLibrary()) {
                         Slog.w(TAG, "Cannot suspend package: " + packageName
                                 + " providing SDK library: "
-                                + pkg.getSdkLibName());
+                                + pkg.getSdkLibraryName());
                         continue;
                     }
                     // Cannot suspend static shared libs as they are considered
@@ -557,7 +557,7 @@
                     if (pkg.isStaticSharedLibrary()) {
                         Slog.w(TAG, "Cannot suspend package: " + packageName
                                 + " providing static shared library: "
-                                + pkg.getStaticSharedLibName());
+                                + pkg.getStaticSharedLibraryName());
                         continue;
                     }
                 }
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index b620249..b977025 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -46,15 +46,6 @@
     public @interface OwnerType {
     }
 
-    // TODO(b/245963156): move to Display.java (and @hide) if we decide to support profiles on MUMD
-    /**
-     * Used only when starting a profile (on systems that
-     * {@link android.os.UserManager#isUsersOnSecondaryDisplaysSupported() support users running on
-     * secondary displays}), to indicate the profile should be started in the same display as its
-     * parent user.
-     */
-    public static final int PARENT_DISPLAY = -2;
-
     public interface UserRestrictionsListener {
         /**
          * Called when a user restriction changes.
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index c77459d..ff87be99 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -6822,13 +6822,25 @@
                 Slogf.d(LOG_TAG, "assignUserToDisplay(%d, %d)", userId, displayId);
             }
 
+            // NOTE: Using Boolean instead of boolean as it will be re-used below
+            Boolean isProfile = null;
             if (displayId == Display.DEFAULT_DISPLAY) {
-                // Don't need to do anything because methods (such as isUserVisible()) already know
-                // that the current user (and their profiles) is assigned to the default display.
-                if (DBG_MUMD) {
-                    Slogf.d(LOG_TAG, "ignoring on default display");
+                if (mUsersOnSecondaryDisplaysEnabled) {
+                    // Profiles are only supported in the default display, but it cannot return yet
+                    // as it needs to check if the parent is also assigned to the DEFAULT_DISPLAY
+                    // (this is done indirectly below when it checks that the profile parent is the
+                    // current user, as the current user is always assigned to the DEFAULT_DISPLAY).
+                    isProfile = isProfileUnchecked(userId);
                 }
-                return;
+                if (isProfile == null || !isProfile) {
+                    // Don't need to do anything because methods (such as isUserVisible()) already
+                    // know that the current user (and their profiles) is assigned to the default
+                    // display.
+                    if (DBG_MUMD) {
+                        Slogf.d(LOG_TAG, "ignoring on default display");
+                    }
+                    return;
+                }
             }
 
             if (!mUsersOnSecondaryDisplaysEnabled) {
@@ -6846,18 +6858,21 @@
             Preconditions.checkArgument(userId != currentUserId,
                     "Cannot assign current user (%d) to other displays", currentUserId);
 
+            if (isProfile == null) {
+                isProfile = isProfileUnchecked(userId);
+            }
             synchronized (mUsersOnSecondaryDisplays) {
-                if (isProfileUnchecked(userId)) {
-                    // Profile can only start in the same display as parent
-                    Preconditions.checkArgument(displayId == UserManagerInternal.PARENT_DISPLAY,
-                            "Profile user can only be started in the same display as parent");
+                if (isProfile) {
+                    // Profile can only start in the same display as parent. And for simplicity,
+                    // that display must be the DEFAULT_DISPLAY.
+                    Preconditions.checkArgument(displayId == Display.DEFAULT_DISPLAY,
+                            "Profile user can only be started in the default display");
                     int parentUserId = getProfileParentId(userId);
-                    int parentDisplayId = mUsersOnSecondaryDisplays.get(parentUserId);
+                    Preconditions.checkArgument(parentUserId == currentUserId,
+                            "Only profile of current user can be assigned to a display");
                     if (DBG_MUMD) {
-                        Slogf.d(LOG_TAG, "Adding profile user %d -> display %d", userId,
-                                parentDisplayId);
+                        Slogf.d(LOG_TAG, "Ignoring profile user %d on default display", userId);
                     }
-                    mUsersOnSecondaryDisplays.put(userId, parentDisplayId);
                     return;
                 }
 
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index 1084145..bc3d7a6 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -81,6 +81,7 @@
 import libcore.util.EmptyArray;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -472,7 +473,11 @@
             PackageStateUnserialized pkgState = pkgSetting.getTransientState();
             info.hiddenUntilInstalled = pkgState.isHiddenUntilInstalled();
             List<String> usesLibraryFiles = pkgState.getUsesLibraryFiles();
-            List<SharedLibraryInfo> usesLibraryInfos = pkgState.getUsesLibraryInfos();
+            var usesLibraries = pkgState.getUsesLibraryInfos();
+            var usesLibraryInfos = new ArrayList<SharedLibraryInfo>();
+            for (int index = 0; index < usesLibraries.size(); index++) {
+                usesLibraryInfos.add(usesLibraries.get(index).getInfo());
+            }
             info.sharedLibraryFiles = usesLibraryFiles.isEmpty()
                     ? null : usesLibraryFiles.toArray(new String[0]);
             info.sharedLibraryInfos = usesLibraryInfos.isEmpty() ? null : usesLibraryInfos;
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
index f6585f6..ca8ba6c 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
@@ -91,7 +91,7 @@
     public static SharedLibraryInfo createSharedLibraryForSdk(AndroidPackage pkg) {
         return new SharedLibraryInfo(null, pkg.getPackageName(),
                 AndroidPackageUtils.getAllCodePaths(pkg),
-                pkg.getSdkLibName(),
+                pkg.getSdkLibraryName(),
                 pkg.getSdkLibVersionMajor(),
                 SharedLibraryInfo.TYPE_SDK_PACKAGE,
                 new VersionedPackage(pkg.getManifestPackageName(),
@@ -102,7 +102,7 @@
     public static SharedLibraryInfo createSharedLibraryForStatic(AndroidPackage pkg) {
         return new SharedLibraryInfo(null, pkg.getPackageName(),
                 AndroidPackageUtils.getAllCodePaths(pkg),
-                pkg.getStaticSharedLibName(),
+                pkg.getStaticSharedLibraryName(),
                 pkg.getStaticSharedLibVersion(),
                 SharedLibraryInfo.TYPE_STATIC,
                 new VersionedPackage(pkg.getManifestPackageName(),
@@ -230,7 +230,7 @@
 
     public static boolean isLibrary(AndroidPackage pkg) {
         // TODO(b/135203078): Can parsing just enforce these always match?
-        return pkg.getSdkLibName() != null || pkg.getStaticSharedLibName() != null
+        return pkg.getSdkLibraryName() != null || pkg.getStaticSharedLibraryName() != null
                 || !pkg.getLibraryNames().isEmpty();
     }
 
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
index fe63dec..a43b979 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java
@@ -57,6 +57,8 @@
 import com.android.internal.util.Parcelling.BuiltIn.ForInternedString;
 import com.android.server.pm.parsing.PackageInfoUtils;
 import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.AndroidPackageSplit;
+import com.android.server.pm.pkg.AndroidPackageSplitImpl;
 import com.android.server.pm.pkg.SELinuxUtil;
 import com.android.server.pm.pkg.component.ComponentMutateUtils;
 import com.android.server.pm.pkg.component.ParsedActivity;
@@ -90,6 +92,7 @@
 
 import java.io.File;
 import java.security.PublicKey;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -235,11 +238,11 @@
     private Map<String, String> overlayables = emptyMap();
     @Nullable
     @DataClass.ParcelWith(ForInternedString.class)
-    private String sdkLibName;
+    private String sdkLibraryName;
     private int sdkLibVersionMajor;
     @Nullable
     @DataClass.ParcelWith(ForInternedString.class)
-    private String staticSharedLibName;
+    private String staticSharedLibraryName;
     private long staticSharedLibVersion;
     @NonNull
     @DataClass.ParcelWith(Parcelling.BuiltIn.ForInternedStringList.class)
@@ -399,6 +402,8 @@
     private long mLongVersionCode;
     private int mLocaleConfigRes;
 
+    private List<AndroidPackageSplit> mSplits;
+
     @NonNull
     public static PackageImpl forParsing(@NonNull String packageName, @NonNull String baseCodePath,
             @NonNull String codePath, @NonNull TypedArray manifestArray, boolean isCoreApp) {
@@ -775,6 +780,51 @@
     }
 
     @Override
+    public List<AndroidPackageSplit> getSplits() {
+        if (mSplits == null) {
+            var splits = new ArrayList<AndroidPackageSplit>();
+            splits.add(new AndroidPackageSplitImpl(
+                    null,
+                    getBaseApkPath(),
+                    getBaseRevisionCode(),
+                    isHasCode() ? ApplicationInfo.FLAG_HAS_CODE : 0,
+                    getClassLoaderName()
+            ));
+
+            if (splitNames != null) {
+                for (int index = 0; index < splitNames.length; index++) {
+                    splits.add(new AndroidPackageSplitImpl(
+                            splitNames[index],
+                            splitCodePaths[index],
+                            splitRevisionCodes[index],
+                            splitFlags[index],
+                            splitClassLoaderNames[index]
+                    ));
+                }
+            }
+
+            if (splitDependencies != null) {
+                for (int index = 0; index < splitDependencies.size(); index++) {
+                    var splitIndex = splitDependencies.keyAt(index);
+                    var dependenciesByIndex = splitDependencies.valueAt(index);
+                    var dependencies = new ArrayList<AndroidPackageSplit>();
+                    for (int dependencyIndex : dependenciesByIndex) {
+                        // Legacy holdover, base dependencies are an array of -1 rather than empty
+                        if (dependencyIndex >= 0) {
+                            dependencies.add(splits.get(dependencyIndex));
+                        }
+                    }
+                    ((AndroidPackageSplitImpl) splits.get(splitIndex))
+                            .fillDependencies(Collections.unmodifiableList(dependencies));
+                }
+            }
+
+            mSplits = Collections.unmodifiableList(splits);
+        }
+        return mSplits;
+    }
+
+    @Override
     public String toString() {
         return "Package{"
                 + Integer.toHexString(System.identityHashCode(this))
@@ -1209,8 +1259,8 @@
 
     @Nullable
     @Override
-    public String getSdkLibName() {
-        return sdkLibName;
+    public String getSdkLibraryName() {
+        return sdkLibraryName;
     }
 
     @Override
@@ -1279,8 +1329,8 @@
 
     @Nullable
     @Override
-    public String getStaticSharedLibName() {
-        return staticSharedLibName;
+    public String getStaticSharedLibraryName() {
+        return staticSharedLibraryName;
     }
 
     @Override
@@ -2218,8 +2268,8 @@
     }
 
     @Override
-    public PackageImpl setSdkLibName(String sdkLibName) {
-        this.sdkLibName = TextUtils.safeIntern(sdkLibName);
+    public PackageImpl setSdkLibraryName(String sdkLibraryName) {
+        this.sdkLibraryName = TextUtils.safeIntern(sdkLibraryName);
         return this;
     }
 
@@ -2261,8 +2311,8 @@
     }
 
     @Override
-    public PackageImpl setStaticSharedLibName(String staticSharedLibName) {
-        this.staticSharedLibName = TextUtils.safeIntern(staticSharedLibName);
+    public PackageImpl setStaticSharedLibraryName(String staticSharedLibraryName) {
+        this.staticSharedLibraryName = TextUtils.safeIntern(staticSharedLibraryName);
         return this;
     }
 
@@ -2977,9 +3027,9 @@
         dest.writeString(this.overlayCategory);
         dest.writeInt(this.overlayPriority);
         sForInternedStringValueMap.parcel(this.overlayables, dest, flags);
-        sForInternedString.parcel(this.sdkLibName, dest, flags);
+        sForInternedString.parcel(this.sdkLibraryName, dest, flags);
         dest.writeInt(this.sdkLibVersionMajor);
-        sForInternedString.parcel(this.staticSharedLibName, dest, flags);
+        sForInternedString.parcel(this.staticSharedLibraryName, dest, flags);
         dest.writeLong(this.staticSharedLibVersion);
         sForInternedStringList.parcel(this.libraryNames, dest, flags);
         sForInternedStringList.parcel(this.usesLibraries, dest, flags);
@@ -3127,9 +3177,9 @@
         this.overlayCategory = in.readString();
         this.overlayPriority = in.readInt();
         this.overlayables = sForInternedStringValueMap.unparcel(in);
-        this.sdkLibName = sForInternedString.unparcel(in);
+        this.sdkLibraryName = sForInternedString.unparcel(in);
         this.sdkLibVersionMajor = in.readInt();
-        this.staticSharedLibName = sForInternedString.unparcel(in);
+        this.staticSharedLibraryName = sForInternedString.unparcel(in);
         this.staticSharedLibVersion = in.readLong();
         this.libraryNames = sForInternedStringList.unparcel(in);
         this.usesLibraries = sForInternedStringList.unparcel(in);
diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java
index e07b77e..5108fcd 100644
--- a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java
+++ b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java
@@ -63,6 +63,69 @@
 @Immutable
 public interface AndroidPackage {
 
+    /**
+     * Library names this package is declared as, for use by other packages with "uses-library".
+     *
+     * @see R.styleable#AndroidManifestLibrary
+     */
+    @NonNull
+    List<String> getLibraryNames();
+
+    /**
+     * @see R.styleable#AndroidManifestSdkLibrary_name
+     */
+    @Nullable
+    String getSdkLibraryName();
+
+    /**
+     * @return List of all splits for a package. Note that base.apk is considered a
+     * split and will be provided as index 0 of the list.
+     */
+    @NonNull
+    List<AndroidPackageSplit> getSplits();
+
+    /**
+     * @see R.styleable#AndroidManifestStaticLibrary_name
+     */
+    @Nullable
+    String getStaticSharedLibraryName();
+
+    /**
+     * @see ApplicationInfo#targetSdkVersion
+     * @see R.styleable#AndroidManifestUsesSdk_targetSdkVersion
+     */
+    int getTargetSdkVersion();
+
+    /**
+     * @see ApplicationInfo#FLAG_DEBUGGABLE
+     */
+    boolean isDebuggable();
+
+    /**
+     * @see ApplicationInfo#PRIVATE_FLAG_ISOLATED_SPLIT_LOADING
+     */
+    boolean isIsolatedSplitLoading();
+
+    /**
+     * @see ApplicationInfo#PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY
+     */
+    boolean isSignedWithPlatformKey();
+
+    /**
+     * @see ApplicationInfo#PRIVATE_FLAG_USE_EMBEDDED_DEX
+     */
+    boolean isUseEmbeddedDex();
+
+    /**
+     * @see ApplicationInfo#PRIVATE_FLAG_USES_NON_SDK_API
+     */
+    boolean isUsesNonSdkApi();
+
+    /**
+     * @see ApplicationInfo#FLAG_VM_SAFE_MODE
+     */
+    boolean isVmSafeMode();
+
     // Methods below this comment are not yet exposed as API
 
     /**
@@ -317,15 +380,6 @@
     int getLargestWidthLimitDp();
 
     /**
-     * Library names this package is declared as, for use by other packages with "uses-library".
-     *
-     * @see R.styleable#AndroidManifestLibrary
-     * @hide
-     */
-    @NonNull
-    List<String> getLibraryNames();
-
-    /**
      * The resource ID used to provide the application's locales configuration.
      *
      * @see R.styleable#AndroidManifestApplication_localeConfig
@@ -730,13 +784,6 @@
     int getRoundIconRes();
 
     /**
-     * @see R.styleable#AndroidManifestSdkLibrary_name
-     * @hide
-     */
-    @Nullable
-    String getSdkLibName();
-
-    /**
      * @see R.styleable#AndroidManifestSdkLibrary_versionMajor
      * @hide
      */
@@ -844,13 +891,6 @@
     int[] getSplitRevisionCodes();
 
     /**
-     * @see R.styleable#AndroidManifestStaticLibrary_name
-     * @hide
-     */
-    @Nullable
-    String getStaticSharedLibName();
-
-    /**
      * @see R.styleable#AndroidManifestStaticLibrary_version
      * @hide
      */
@@ -864,13 +904,6 @@
     int getTargetSandboxVersion();
 
     /**
-     * @see ApplicationInfo#targetSdkVersion
-     * @see R.styleable#AndroidManifestUsesSdk_targetSdkVersion
-     * @hide
-     */
-    int getTargetSdkVersion();
-
-    /**
      * @see ApplicationInfo#taskAffinity
      * @see R.styleable#AndroidManifestApplication_taskAffinity
      * @hide
@@ -1118,12 +1151,6 @@
     boolean isCrossProfile();
 
     /**
-     * @see ApplicationInfo#FLAG_DEBUGGABLE
-     * @hide
-     */
-    boolean isDebuggable();
-
-    /**
      * @see ApplicationInfo#PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE
      * @hide
      */
@@ -1198,12 +1225,6 @@
     boolean isHasFragileUserData();
 
     /**
-     * @see ApplicationInfo#PRIVATE_FLAG_ISOLATED_SPLIT_LOADING
-     * @hide
-     */
-    boolean isIsolatedSplitLoading();
-
-    /**
      * @see ApplicationInfo#FLAG_KILL_AFTER_RESTORE
      * @hide
      */
@@ -1354,12 +1375,6 @@
     boolean isSdkLibrary();
 
     /**
-     * @see ApplicationInfo#PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY
-     * @hide
-     */
-    boolean isSignedWithPlatformKey();
-
-    /**
      * @see ApplicationInfo#PRIVATE_FLAG_STATIC_SHARED_LIBRARY
      * @hide
      */
@@ -1445,24 +1460,12 @@
     boolean isUse32BitAbi();
 
     /**
-     * @see ApplicationInfo#PRIVATE_FLAG_USE_EMBEDDED_DEX
-     * @hide
-     */
-    boolean isUseEmbeddedDex();
-
-    /**
      * @see ApplicationInfo#FLAG_USES_CLEARTEXT_TRAFFIC
      * @hide
      */
     boolean isUsesCleartextTraffic();
 
     /**
-     * @see ApplicationInfo#PRIVATE_FLAG_USES_NON_SDK_API
-     * @hide
-     */
-    boolean isUsesNonSdkApi();
-
-    /**
      * @see ApplicationInfo#PRIVATE_FLAG_VENDOR
      * @hide
      */
@@ -1477,10 +1480,4 @@
      * @hide
      */
     boolean isVisibleToInstantApps();
-
-    /**
-     * @see ApplicationInfo#FLAG_VM_SAFE_MODE
-     * @hide
-     */
-    boolean isVmSafeMode();
 }
diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackageSplit.java b/services/core/java/com/android/server/pm/pkg/AndroidPackageSplit.java
new file mode 100644
index 0000000..a17ecc3
--- /dev/null
+++ b/services/core/java/com/android/server/pm/pkg/AndroidPackageSplit.java
@@ -0,0 +1,44 @@
+/*
+ * 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.pm.pkg;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.processor.immutability.Immutable;
+
+import java.util.List;
+
+/** @hide */
+@Immutable
+public interface AndroidPackageSplit {
+
+    @Nullable
+    String getName();
+
+    @NonNull
+    String getPath();
+
+    int getRevisionCode();
+
+    boolean isHasCode();
+
+    @Nullable
+    String getClassLoaderName();
+
+    @NonNull
+    List<AndroidPackageSplit> getDependencies();
+}
diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackageSplitImpl.java b/services/core/java/com/android/server/pm/pkg/AndroidPackageSplitImpl.java
new file mode 100644
index 0000000..9aac8a8
--- /dev/null
+++ b/services/core/java/com/android/server/pm/pkg/AndroidPackageSplitImpl.java
@@ -0,0 +1,126 @@
+/*
+ * 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.pm.pkg;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.ApplicationInfo;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class AndroidPackageSplitImpl implements AndroidPackageSplit {
+
+    @Nullable
+    private final String mName;
+    @NonNull
+    private final String mPath;
+    private final int mRevisionCode;
+    private final int mFlags;
+    @Nullable
+    private final String mClassLoaderName;
+
+    @NonNull
+    private List<AndroidPackageSplit> mDependencies = Collections.emptyList();
+
+    public AndroidPackageSplitImpl(@Nullable String name, @NonNull String path, int revisionCode,
+            int flags, @Nullable String classLoaderName) {
+        mName = name;
+        mPath = path;
+        mRevisionCode = revisionCode;
+        mFlags = flags;
+        mClassLoaderName = classLoaderName;
+    }
+
+    public void fillDependencies(@NonNull List<AndroidPackageSplit> splits) {
+        if (!mDependencies.isEmpty()) {
+            throw new IllegalStateException("Cannot fill split dependencies more than once");
+        }
+        mDependencies = splits;
+    }
+
+    @Nullable
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    @NonNull
+    @Override
+    public String getPath() {
+        return mPath;
+    }
+
+    @Override
+    public int getRevisionCode() {
+        return mRevisionCode;
+    }
+
+    @Override
+    public boolean isHasCode() {
+        return (mFlags & ApplicationInfo.FLAG_HAS_CODE) != 0;
+    }
+
+    @Nullable
+    @Override
+    public String getClassLoaderName() {
+        return mClassLoaderName;
+    }
+
+    @NonNull
+    @Override
+    public List<AndroidPackageSplit> getDependencies() {
+        return mDependencies;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof AndroidPackageSplitImpl)) return false;
+        AndroidPackageSplitImpl that = (AndroidPackageSplitImpl) o;
+        var fieldsEqual = mRevisionCode == that.mRevisionCode && mFlags == that.mFlags && Objects.equals(
+                mName, that.mName) && Objects.equals(mPath, that.mPath)
+                && Objects.equals(mClassLoaderName, that.mClassLoaderName);
+
+        if (!fieldsEqual) return false;
+        if (mDependencies.size() != that.mDependencies.size()) return false;
+
+        // Should be impossible, but to avoid circular dependencies,
+        // only search 1 level deep using split name
+        for (int index = 0; index < mDependencies.size(); index++) {
+            if (!Objects.equals(mDependencies.get(index).getName(),
+                    that.mDependencies.get(index).getName())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        // Should be impossible, but to avoid circular dependencies,
+        // only search 1 level deep using split name
+        var dependenciesHash = Objects.hash(mName, mPath, mRevisionCode, mFlags, mClassLoaderName);
+        for (int index = 0; index < mDependencies.size(); index++) {
+            var name = mDependencies.get(index).getName();
+            dependenciesHash = 31 * dependenciesHash + (name == null ? 0 : name.hashCode());
+        }
+        return dependenciesHash;
+    }
+}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index c0e063d..a6e6016 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -22,8 +22,8 @@
 import android.annotation.UserIdInt;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.SharedLibraryInfo;
 import android.content.pm.SigningInfo;
+import android.os.UserHandle;
 import android.processor.immutability.Immutable;
 import android.util.SparseArray;
 
@@ -42,8 +42,6 @@
 @Immutable
 public interface PackageState {
 
-    // Methods below this comment are not yet exposed as API
-
     /*
      * Until immutability or read-only caching is enabled, {@link PackageSetting} cannot be
      * returned directly, so {@link PackageStateImpl} is used to temporarily copy the data.
@@ -83,11 +81,9 @@
      * Re-attaching the storage device to make the APK available should allow the user to use the
      * app once the device reboots or otherwise re-scans it.
      * <p/>
-     * This can also occur in an device OTA situation where the package is no longer parseable on
-     * an updated SDK version, causing it to be rejectd, but the state associated with it retained,
+     * This can also occur in an device OTA situation where the package is no longer parsable on
+     * an updated SDK version, causing it to be rejected, but the state associated with it retained,
      * similarly to if the package had been uninstalled with the --keep-data option.
-     *
-     * @hide
      */
     @Nullable
     AndroidPackage getAndroidPackage();
@@ -95,12 +91,58 @@
     /**
      * The non-user-specific UID, or the UID if the user ID is
      * {@link android.os.UserHandle#SYSTEM}.
-     *
-     * @hide
      */
     int getAppId();
 
     /**
+     * @see AndroidPackage#getPackageName()
+     */
+    @NonNull
+    String getPackageName();
+
+    /**
+     * @see ApplicationInfo#primaryCpuAbi
+     */
+    @Nullable
+    String getPrimaryCpuAbi();
+
+    /**
+     * @see ApplicationInfo#secondaryCpuAbi
+     */
+    @Nullable
+    String getSecondaryCpuAbi();
+
+    /**
+     * @see AndroidPackage#isPrivileged()
+     */
+    boolean isPrivileged();
+
+    /**
+     * @see AndroidPackage#isSystem()
+     */
+    boolean isSystem();
+
+    /**
+     * Whether this app is on the /data partition having been upgraded from a preinstalled app on a
+     * system partition.
+     */
+    boolean isUpdatedSystemApp();
+
+    /**
+     * @return State for a user or {@link PackageUserState#DEFAULT} if the state doesn't exist.
+     */
+    @NonNull
+    PackageUserState getStateForUser(@NonNull UserHandle user);
+
+    /**
+     * @see R.styleable#AndroidManifestUsesLibrary
+     */
+    @NonNull
+    List<SharedLibrary> getUsesLibraries();
+
+    // Methods below this comment are not yet exposed as API
+
+    /**
      * Value set through {@link PackageManager#setApplicationCategoryHint(String, int)}. Only
      * applied if the application itself does not declare a category.
      *
@@ -165,13 +207,6 @@
     Map<String, Set<String>> getMimeGroups();
 
     /**
-     * @see AndroidPackage#getPackageName()
-     * @hide
-     */
-    @NonNull
-    String getPackageName();
-
-    /**
      * @see AndroidPackage#getPath()
      * @hide
      */
@@ -179,20 +214,6 @@
     File getPath();
 
     /**
-     * @see ApplicationInfo#primaryCpuAbi
-     * @hide
-     */
-    @Nullable
-    String getPrimaryCpuAbi();
-
-    /**
-     * @see ApplicationInfo#secondaryCpuAbi
-     * @hide
-     */
-    @Nullable
-    String getSecondaryCpuAbi();
-
-    /**
      * Whether the package shares the same user ID as other packages
      * @hide
      */
@@ -239,14 +260,6 @@
     List<String> getUsesLibraryFiles();
 
     /**
-     * @see R.styleable#AndroidManifestUsesLibrary
-     * @hide
-     */
-    @Immutable.Ignore
-    @NonNull
-    List<SharedLibraryInfo> getUsesLibraryInfos();
-
-    /**
      * @see R.styleable#AndroidManifestUsesSdkLibrary
      * @hide
      */
@@ -327,12 +340,6 @@
     boolean isOem();
 
     /**
-     * @see AndroidPackage#isPrivileged()
-     * @hide
-     */
-    boolean isPrivileged();
-
-    /**
      * @see AndroidPackage#isProduct()
      * @hide
      */
@@ -345,12 +352,6 @@
     boolean isRequiredForSystemUser();
 
     /**
-     * @see AndroidPackage#isSystem()
-     * @hide
-     */
-    boolean isSystem();
-
-    /**
      * @see AndroidPackage#isSystemExt()
      * @hide
      */
@@ -363,14 +364,6 @@
     boolean isUpdateAvailable();
 
     /**
-     * Whether this app is on the /data partition having been upgraded from a preinstalled app on a
-     * system partition.
-     *
-     * @hide
-     */
-    boolean isUpdatedSystemApp();
-
-    /**
      * Whether this app is packaged in an updated apex.
      *
      * @hide
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
index 28309c7..c6ce40e 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java
@@ -21,9 +21,9 @@
 import android.annotation.Nullable;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
-import android.content.pm.SharedLibraryInfo;
 import android.content.pm.SigningInfo;
 import android.content.pm.overlay.OverlayPaths;
+import android.os.UserHandle;
 import android.util.ArraySet;
 import android.util.SparseArray;
 
@@ -140,7 +140,7 @@
     @NonNull
     private final long[] mUsesStaticLibrariesVersions;
     @NonNull
-    private final List<SharedLibraryInfo> mUsesLibraryInfos;
+    private final List<SharedLibrary> mUsesLibraries;
     @NonNull
     private final List<String> mUsesLibraryFiles;
     @NonNull
@@ -181,7 +181,7 @@
         mUsesSdkLibrariesVersionsMajor = pkgState.getUsesSdkLibrariesVersionsMajor();
         mUsesStaticLibraries = pkgState.getUsesStaticLibraries();
         mUsesStaticLibrariesVersions = pkgState.getUsesStaticLibrariesVersions();
-        mUsesLibraryInfos = Collections.unmodifiableList(pkgState.getUsesLibraryInfos());
+        mUsesLibraries = Collections.unmodifiableList(pkgState.getUsesLibraries());
         mUsesLibraryFiles = Collections.unmodifiableList(pkgState.getUsesLibraryFiles());
         setBoolean(Booleans.FORCE_QUERYABLE_OVERRIDE, pkgState.isForceQueryableOverride());
         setBoolean(Booleans.HIDDEN_UNTIL_INSTALLED, pkgState.isHiddenUntilInstalled());
@@ -201,6 +201,13 @@
         }
     }
 
+    @NonNull
+    @Override
+    public PackageUserState getStateForUser(@NonNull UserHandle user) {
+        PackageUserState userState = getUserStates().get(user.getIdentifier());
+        return userState == null ? PackageUserState.DEFAULT : userState;
+    }
+
     @Override
     public boolean isExternalStorage() {
         return getBoolean(Booleans.EXTERNAL_STORAGE);
@@ -469,8 +476,7 @@
         }
 
         @DataClass.Generated.Member
-        public @NonNull
-        ArraySet<String> getDisabledComponents() {
+        public @NonNull ArraySet<String> getDisabledComponents() {
             return mDisabledComponents;
         }
 
@@ -536,10 +542,10 @@
         }
 
         @DataClass.Generated(
-                time = 1644270981508L,
+                time = 1661977809886L,
                 codegenVersion = "1.0.23",
                 sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
-                inputSignatures = "private  int mBooleans\nprivate final  long mCeDataInode\nprivate final @android.annotation.NonNull java.util.Set<java.lang.String> mDisabledComponents\nprivate final @android.content.pm.PackageManager.DistractionRestriction int mDistractionFlags\nprivate final @android.annotation.NonNull java.util.Set<java.lang.String> mEnabledComponents\nprivate final  int mEnabledState\nprivate final @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate final @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate final @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate final @android.annotation.NonNull android.content.pm.overlay.OverlayPaths mOverlayPaths\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate final @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate final @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate final  long mFirstInstallTime\npublic static  com.android.server.pm.pkg.PackageUserState copy(com.android.server.pm.pkg.PackageUserState)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isSuspended()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\nclass UserStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageUserState]\nprivate static final  int HIDDEN\nprivate static final  int INSTALLED\nprivate static final  int INSTANT_APP\nprivate static final  int NOT_LAUNCHED\nprivate static final  int STOPPED\nprivate static final  int SUSPENDED\nprivate static final  int VIRTUAL_PRELOAD\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
+                inputSignatures = "private  int mBooleans\nprivate final  long mCeDataInode\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mDisabledComponents\nprivate final @android.content.pm.PackageManager.DistractionRestriction int mDistractionFlags\nprivate final @android.annotation.NonNull android.util.ArraySet<java.lang.String> mEnabledComponents\nprivate final  int mEnabledState\nprivate final @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate final @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate final @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate final @android.annotation.NonNull android.content.pm.overlay.OverlayPaths mOverlayPaths\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate final @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate final @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate final  long mFirstInstallTime\npublic static  com.android.server.pm.pkg.PackageUserState copy(com.android.server.pm.pkg.PackageUserState)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isSuspended()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\nclass UserStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageUserState]\nprivate static final  int HIDDEN\nprivate static final  int INSTALLED\nprivate static final  int INSTANT_APP\nprivate static final  int NOT_LAUNCHED\nprivate static final  int STOPPED\nprivate static final  int SUSPENDED\nprivate static final  int VIRTUAL_PRELOAD\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
         @Deprecated
         private void __metadata() {}
 
@@ -660,8 +666,8 @@
     }
 
     @DataClass.Generated.Member
-    public @NonNull List<SharedLibraryInfo> getUsesLibraryInfos() {
-        return mUsesLibraryInfos;
+    public @NonNull List<SharedLibrary> getUsesLibraries() {
+        return mUsesLibraries;
     }
 
     @DataClass.Generated.Member
@@ -691,10 +697,10 @@
     }
 
     @DataClass.Generated(
-            time = 1644270981543L,
+            time = 1661977809932L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateImpl.java",
-            inputSignatures = "private  int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final  int mAppId\nprivate final  int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final  long mLastModifiedTime\nprivate final  long mLastUpdateTime\nprivate final  long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final  boolean mHasSharedUser\nprivate final  int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mUsesLibraryInfos\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\npublic static  com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override int getSharedUserAppId()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final  int SYSTEM\nprivate static final  int EXTERNAL_STORAGE\nprivate static final  int PRIVILEGED\nprivate static final  int OEM\nprivate static final  int VENDOR\nprivate static final  int PRODUCT\nprivate static final  int SYSTEM_EXT\nprivate static final  int REQUIRED_FOR_SYSTEM_USER\nprivate static final  int ODM\nprivate static final  int FORCE_QUERYABLE_OVERRIDE\nprivate static final  int HIDDEN_UNTIL_INSTALLED\nprivate static final  int INSTALL_PERMISSIONS_FIXED\nprivate static final  int UPDATE_AVAILABLE\nprivate static final  int UPDATED_SYSTEM_APP\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
+            inputSignatures = "private  int mBooleans\nprivate final @android.annotation.Nullable com.android.server.pm.pkg.AndroidPackage mAndroidPackage\nprivate final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable java.lang.String mVolumeUuid\nprivate final  int mAppId\nprivate final  int mCategoryOverride\nprivate final @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate final  long mLastModifiedTime\nprivate final  long mLastUpdateTime\nprivate final  long mLongVersionCode\nprivate final @android.annotation.NonNull java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mMimeGroups\nprivate final @android.annotation.NonNull java.io.File mPath\nprivate final @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate final @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate final  boolean mHasSharedUser\nprivate final  int mSharedUserAppId\nprivate final @android.annotation.NonNull java.lang.String[] mUsesSdkLibraries\nprivate final @android.annotation.NonNull long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.NonNull java.lang.String[] mUsesStaticLibraries\nprivate final @android.annotation.NonNull long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibrary> mUsesLibraries\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesLibraryFiles\nprivate final @android.annotation.NonNull long[] mLastPackageUsageTime\nprivate final @android.annotation.NonNull android.content.pm.SigningInfo mSigningInfo\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserState> mUserStates\npublic static  com.android.server.pm.pkg.PackageState copy(com.android.server.pm.pkg.PackageStateInternal)\nprivate  void setBoolean(int,boolean)\nprivate  boolean getBoolean(int)\npublic @java.lang.Override boolean isExternalStorage()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isOdm()\npublic @java.lang.Override boolean isOem()\npublic @java.lang.Override boolean isPrivileged()\npublic @java.lang.Override boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic @java.lang.Override boolean isSystem()\npublic @java.lang.Override boolean isSystemExt()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @java.lang.Override boolean isVendor()\npublic @java.lang.Override long getVersionCode()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override int getSharedUserAppId()\nclass PackageStateImpl extends java.lang.Object implements [com.android.server.pm.pkg.PackageState]\nprivate static final  int SYSTEM\nprivate static final  int EXTERNAL_STORAGE\nprivate static final  int PRIVILEGED\nprivate static final  int OEM\nprivate static final  int VENDOR\nprivate static final  int PRODUCT\nprivate static final  int SYSTEM_EXT\nprivate static final  int REQUIRED_FOR_SYSTEM_USER\nprivate static final  int ODM\nprivate static final  int FORCE_QUERYABLE_OVERRIDE\nprivate static final  int HIDDEN_UNTIL_INSTALLED\nprivate static final  int INSTALL_PERMISSIONS_FIXED\nprivate static final  int UPDATE_AVAILABLE\nprivate static final  int UPDATED_SYSTEM_APP\nprivate static final  int APK_IN_UPDATED_APEX\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java b/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
index 1ae00d3..b22c038 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java
@@ -29,7 +29,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * For use by {@link PackageSetting} to maintain functionality that used to exist in PackageParser.
@@ -42,13 +41,13 @@
  * @hide
  */
 @DataClass(genSetters = true, genConstructor = false, genBuilder = false)
-@DataClass.Suppress({"setLastPackageUsageTimeInMills", "setPackageSetting"})
+@DataClass.Suppress({"setLastPackageUsageTimeInMills", "setPackageSetting", "setUsesLibraryInfos"})
 public class PackageStateUnserialized {
 
     private boolean hiddenUntilInstalled;
 
     @NonNull
-    private List<SharedLibraryInfo> usesLibraryInfos = emptyList();
+    private List<SharedLibraryWrapper> usesLibraryInfos = emptyList();
 
     @NonNull
     private List<String> usesLibraryFiles = emptyList();
@@ -72,7 +71,7 @@
     }
 
     @NonNull
-    public PackageStateUnserialized addUsesLibraryInfo(@NonNull SharedLibraryInfo value) {
+    public PackageStateUnserialized addUsesLibraryInfo(@NonNull SharedLibraryWrapper value) {
         usesLibraryInfos = CollectionUtils.add(usesLibraryInfos, value);
         return this;
     }
@@ -143,8 +142,16 @@
     }
 
     public @NonNull List<SharedLibraryInfo> getNonNativeUsesLibraryInfos() {
-        return getUsesLibraryInfos().stream()
-                .filter((l) -> !l.isNative()).collect(Collectors.toList());
+        var list = new ArrayList<SharedLibraryInfo>();
+        usesLibraryInfos = getUsesLibraryInfos();
+        for (int index = 0; index < usesLibraryInfos.size(); index++) {
+            var library = usesLibraryInfos.get(index);
+            if (!library.isNative()) {
+                list.add(library.getInfo());
+            }
+
+        }
+        return list;
     }
 
     public PackageStateUnserialized setHiddenUntilInstalled(boolean value) {
@@ -154,7 +161,11 @@
     }
 
     public PackageStateUnserialized setUsesLibraryInfos(@NonNull List<SharedLibraryInfo> value) {
-        usesLibraryInfos = value;
+        var list = new ArrayList<SharedLibraryWrapper>();
+        for (int index = 0; index < value.size(); index++) {
+            list.add(new SharedLibraryWrapper(value.get(index)));
+        }
+        usesLibraryInfos = list;
         mPackageSetting.onChanged();
         return this;
     }
@@ -216,7 +227,7 @@
     }
 
     @DataClass.Generated.Member
-    public @NonNull List<SharedLibraryInfo> getUsesLibraryInfos() {
+    public @NonNull List<SharedLibraryWrapper> getUsesLibraryInfos() {
         return usesLibraryInfos;
     }
 
@@ -265,10 +276,10 @@
     }
 
     @DataClass.Generated(
-            time = 1646203523807L,
+            time = 1661373697219L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java",
-            inputSignatures = "private  boolean hiddenUntilInstalled\nprivate @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> usesLibraryInfos\nprivate @android.annotation.NonNull java.util.List<java.lang.String> usesLibraryFiles\nprivate  boolean updatedSystemApp\nprivate  boolean apkInApex\nprivate  boolean apkInUpdatedApex\nprivate volatile @android.annotation.NonNull long[] lastPackageUsageTimeInMills\nprivate @android.annotation.Nullable java.lang.String overrideSeInfo\nprivate final @android.annotation.NonNull com.android.server.pm.PackageSetting mPackageSetting\nprivate  long[] lazyInitLastPackageUsageTimeInMills()\npublic  com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(int,long)\npublic  long getLatestPackageUseTimeInMills()\npublic  long getLatestForegroundPackageUseTimeInMills()\npublic  void updateFrom(com.android.server.pm.pkg.PackageStateUnserialized)\npublic @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> getNonNativeUsesLibraryInfos()\npublic  com.android.server.pm.pkg.PackageStateUnserialized setHiddenUntilInstalled(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryInfos(java.util.List<android.content.pm.SharedLibraryInfo>)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryFiles(java.util.List<java.lang.String>)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUpdatedSystemApp(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setApkInApex(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setApkInUpdatedApex(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(long)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setOverrideSeInfo(java.lang.String)\nclass PackageStateUnserialized extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=true, genConstructor=false, genBuilder=false)")
+            inputSignatures = "private  boolean hiddenUntilInstalled\nprivate @android.annotation.NonNull java.util.List<com.android.server.pm.pkg.SharedLibraryWrapper> usesLibraryInfos\nprivate @android.annotation.NonNull java.util.List<java.lang.String> usesLibraryFiles\nprivate  boolean updatedSystemApp\nprivate  boolean apkInApex\nprivate  boolean apkInUpdatedApex\nprivate volatile @android.annotation.NonNull long[] lastPackageUsageTimeInMills\nprivate @android.annotation.Nullable java.lang.String overrideSeInfo\nprivate final @android.annotation.NonNull com.android.server.pm.PackageSetting mPackageSetting\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryInfo(com.android.server.pm.pkg.SharedLibraryWrapper)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized addUsesLibraryFile(java.lang.String)\nprivate  long[] lazyInitLastPackageUsageTimeInMills()\npublic  com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(int,long)\npublic  long getLatestPackageUseTimeInMills()\npublic  long getLatestForegroundPackageUseTimeInMills()\npublic  void updateFrom(com.android.server.pm.pkg.PackageStateUnserialized)\npublic @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> getNonNativeUsesLibraryInfos()\npublic  com.android.server.pm.pkg.PackageStateUnserialized setHiddenUntilInstalled(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryInfos(java.util.List<android.content.pm.SharedLibraryInfo>)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUsesLibraryFiles(java.util.List<java.lang.String>)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setUpdatedSystemApp(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setApkInApex(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setApkInUpdatedApex(boolean)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setLastPackageUsageTimeInMills(long)\npublic  com.android.server.pm.pkg.PackageStateUnserialized setOverrideSeInfo(java.lang.String)\nclass PackageStateUnserialized extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genSetters=true, genConstructor=false, genBuilder=false)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserState.java b/services/core/java/com/android/server/pm/pkg/PackageUserState.java
index a1b6f1d..a68e59b 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserState.java
@@ -33,12 +33,18 @@
  *
  * @hide
  */
-// TODO(b/173807334): Expose API
 //@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
 @Immutable
 public interface PackageUserState {
 
     /**
+     * @return whether the package is marked as installed
+     */
+    boolean isInstalled();
+
+    // Methods below this comment are not yet exposed as API
+
+    /**
      * @hide
      */
     @NonNull
@@ -150,12 +156,6 @@
     boolean isHidden();
 
     /**
-     * @return whether the package is marked as installed for all users
-     * @hide
-     */
-    boolean isInstalled();
-
-    /**
      * @return whether the package is marked as an ephemeral app, which restricts permissions,
      * features, visibility
      * @hide
diff --git a/services/core/java/com/android/server/pm/pkg/SharedLibrary.java b/services/core/java/com/android/server/pm/pkg/SharedLibrary.java
new file mode 100644
index 0000000..20f05f6
--- /dev/null
+++ b/services/core/java/com/android/server/pm/pkg/SharedLibrary.java
@@ -0,0 +1,92 @@
+/*
+ * 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.pm.pkg;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.VersionedPackage;
+import android.processor.immutability.Immutable;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+//@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+@Immutable
+public interface SharedLibrary {
+
+    /**
+     * @see SharedLibraryInfo#getPath()
+     */
+    @Nullable
+    String getPath();
+
+    /**
+     * @see SharedLibraryInfo#getPackageName()
+     */
+    @Nullable
+    String getPackageName();
+
+    /**
+     * @see SharedLibraryInfo#getName()
+     */
+    @Nullable
+    String getName();
+
+    /**
+     * @see SharedLibraryInfo#getAllCodePaths()
+     */
+    @NonNull
+    List<String> getAllCodePaths();
+
+    /**
+     * @see SharedLibraryInfo#getLongVersion()
+     */
+    long getVersion();
+
+    /**
+     * @see SharedLibraryInfo#getType()
+     */
+    int getType();
+
+    /**
+     * @see SharedLibraryInfo#isNative()
+     */
+    boolean isNative();
+
+    /**
+     * @see SharedLibraryInfo#getDeclaringPackage()
+     */
+    @Immutable.Policy(exceptions = {Immutable.Policy.Exception.FINAL_CLASSES_WITH_FINAL_FIELDS})
+    @NonNull
+    VersionedPackage getDeclaringPackage();
+
+    /**
+     * @see SharedLibraryInfo#getDependentPackages()
+     */
+    @Immutable.Policy(exceptions = {Immutable.Policy.Exception.FINAL_CLASSES_WITH_FINAL_FIELDS})
+    @NonNull
+    List<VersionedPackage> getDependentPackages();
+
+    /**
+     * @see SharedLibraryInfo#getDependencies()
+     */
+    @NonNull
+    List<SharedLibrary> getDependencies();
+}
diff --git a/services/core/java/com/android/server/pm/pkg/SharedLibraryWrapper.java b/services/core/java/com/android/server/pm/pkg/SharedLibraryWrapper.java
new file mode 100644
index 0000000..2f1fe1a
--- /dev/null
+++ b/services/core/java/com/android/server/pm/pkg/SharedLibraryWrapper.java
@@ -0,0 +1,109 @@
+/*
+ * 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.pm.pkg;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.VersionedPackage;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** @hide */
+public class SharedLibraryWrapper implements SharedLibrary {
+
+    private final SharedLibraryInfo mInfo;
+
+    @Nullable
+    private List<SharedLibrary> cachedDependenciesList;
+
+    public SharedLibraryWrapper(@NonNull SharedLibraryInfo info) {
+        mInfo = info;
+    }
+
+    @NonNull
+    public SharedLibraryInfo getInfo() {
+        return mInfo;
+    }
+
+    @Override
+    public String getPath() {
+        return mInfo.getPath();
+    }
+
+    @Override
+    public String getPackageName() {
+        return mInfo.getPackageName();
+    }
+
+    @Override
+    public String getName() {
+        return mInfo.getName();
+    }
+
+    @Override
+    public List<String> getAllCodePaths() {
+        return Collections.unmodifiableList(mInfo.getAllCodePaths());
+    }
+
+    @Override
+    public long getVersion() {
+        return mInfo.getLongVersion();
+    }
+
+    @Override
+    public int getType() {
+        return mInfo.getType();
+    }
+
+    @Override
+    public boolean isNative() {
+        return mInfo.isNative();
+    }
+
+    @NonNull
+    @Override
+    public VersionedPackage getDeclaringPackage() {
+        return mInfo.getDeclaringPackage();
+    }
+
+    @NonNull
+    @Override
+    public List<VersionedPackage> getDependentPackages() {
+        return Collections.unmodifiableList(mInfo.getDependentPackages());
+    }
+
+    @NonNull
+    @Override
+    public List<SharedLibrary> getDependencies() {
+        if (cachedDependenciesList == null) {
+            var dependencies = mInfo.getDependencies();
+            if (dependencies == null) {
+                cachedDependenciesList = Collections.emptyList();
+            } else {
+                var list = new ArrayList<SharedLibrary>();
+                for (int index = 0; index < dependencies.size(); index++) {
+                    list.add(new SharedLibraryWrapper(dependencies.get(index)));
+                }
+                cachedDependenciesList = Collections.unmodifiableList(list);
+            }
+        }
+        return cachedDependenciesList;
+    }
+}
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
index 1a46e20..2626bb4 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java
@@ -147,7 +147,7 @@
 
     ParsingPackage setSharedUserId(String sharedUserId);
 
-    ParsingPackage setStaticSharedLibName(String staticSharedLibName);
+    ParsingPackage setStaticSharedLibraryName(String staticSharedLibName);
 
     ParsingPackage setTaskAffinity(String taskAffinity);
 
@@ -221,7 +221,7 @@
 
     ParsingPackage setRestoreAnyVersion(boolean restoreAnyVersion);
 
-    ParsingPackage setSdkLibName(String sdkLibName);
+    ParsingPackage setSdkLibraryName(String sdkLibName);
 
     ParsingPackage setSdkLibVersionMajor(int sdkLibVersionMajor);
 
@@ -458,7 +458,7 @@
     Boolean getResizeableActivity();
 
     @Nullable
-    String getSdkLibName();
+    String getSdkLibraryName();
 
     @NonNull
     List<ParsedService> getServices();
@@ -473,7 +473,7 @@
     String[] getSplitNames();
 
     @Nullable
-    String getStaticSharedLibName();
+    String getStaticSharedLibraryName();
 
     int getTargetSdkVersion();
 
diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
index a8d48ae..952adda 100644
--- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java
@@ -2164,8 +2164,8 @@
             }
         }
 
-        if (TextUtils.isEmpty(pkg.getStaticSharedLibName()) && TextUtils.isEmpty(
-                pkg.getSdkLibName())) {
+        if (TextUtils.isEmpty(pkg.getStaticSharedLibraryName()) && TextUtils.isEmpty(
+                pkg.getSdkLibraryName())) {
             // Add a hidden app detail activity to normal apps which forwards user to App Details
             // page.
             ParseResult<ParsedActivity> a = generateAppDetailsHiddenActivity(input, pkg);
@@ -2355,12 +2355,12 @@
                         PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID,
                         "sharedUserId not allowed in SDK library"
                 );
-            } else if (pkg.getSdkLibName() != null) {
+            } else if (pkg.getSdkLibraryName() != null) {
                 return input.error("Multiple SDKs for package "
                         + pkg.getPackageName());
             }
 
-            return input.success(pkg.setSdkLibName(lname.intern())
+            return input.success(pkg.setSdkLibraryName(lname.intern())
                     .setSdkLibVersionMajor(versionMajor)
                     .setSdkLibrary(true));
         } finally {
@@ -2393,12 +2393,12 @@
                         PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID,
                         "sharedUserId not allowed in static shared library"
                 );
-            } else if (pkg.getStaticSharedLibName() != null) {
+            } else if (pkg.getStaticSharedLibraryName() != null) {
                 return input.error("Multiple static-shared libs for package "
                         + pkg.getPackageName());
             }
 
-            return input.success(pkg.setStaticSharedLibName(lname.intern())
+            return input.success(pkg.setStaticSharedLibraryName(lname.intern())
                     .setStaticSharedLibVersion(
                             PackageInfo.composeLongVersionCode(versionMajor, version))
                     .setStaticSharedLibrary(true));
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index dad9584..9336be5 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -20,10 +20,12 @@
 import android.annotation.UserIdInt;
 import android.app.ActivityManagerInternal;
 import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.input.InputManagerInternal;
 import android.media.AudioManager;
@@ -32,6 +34,7 @@
 import android.metrics.LogMaker;
 import android.net.Uri;
 import android.os.BatteryStats;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IWakeLockCallback;
 import android.os.Looper;
@@ -137,7 +140,9 @@
     private final NotifierHandler mHandler;
     private final Executor mBackgroundExecutor;
     private final Intent mScreenOnIntent;
+    private final Bundle mScreenOnOptions;
     private final Intent mScreenOffIntent;
+    private final Bundle mScreenOffOptions;
 
     // True if the device should suspend when the screen is off due to proximity.
     private final boolean mSuspendWhenScreenOffDueToProximityConfig;
@@ -199,10 +204,14 @@
         mScreenOnIntent.addFlags(
                 Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND
                 | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+        mScreenOnOptions = BroadcastOptions.makeRemovingMatchingFilter(
+                new IntentFilter(Intent.ACTION_SCREEN_OFF)).toBundle();
         mScreenOffIntent = new Intent(Intent.ACTION_SCREEN_OFF);
         mScreenOffIntent.addFlags(
                 Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND
                 | Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
+        mScreenOffOptions = BroadcastOptions.makeRemovingMatchingFilter(
+                new IntentFilter(Intent.ACTION_SCREEN_ON)).toBundle();
 
         mSuspendWhenScreenOffDueToProximityConfig = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_suspendWhenScreenOffDueToProximity);
@@ -788,7 +797,8 @@
 
         if (mActivityManagerInternal.isSystemReady()) {
             mContext.sendOrderedBroadcastAsUser(mScreenOnIntent, UserHandle.ALL, null,
-                    mWakeUpBroadcastDone, mHandler, 0, null, null);
+                    AppOpsManager.OP_NONE, mScreenOnOptions, mWakeUpBroadcastDone, mHandler,
+                    0, null, null);
         } else {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 2, 1);
             sendNextBroadcast();
@@ -811,7 +821,8 @@
 
         if (mActivityManagerInternal.isSystemReady()) {
             mContext.sendOrderedBroadcastAsUser(mScreenOffIntent, UserHandle.ALL, null,
-                    mGoToSleepBroadcastDone, mHandler, 0, null, null);
+                    AppOpsManager.OP_NONE, mScreenOffOptions, mGoToSleepBroadcastDone, mHandler,
+                    0, null, null);
         } else {
             EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 3, 1);
             sendNextBroadcast();
diff --git a/services/core/java/com/android/server/resources/OWNERS b/services/core/java/com/android/server/resources/OWNERS
new file mode 100644
index 0000000..7460a14
--- /dev/null
+++ b/services/core/java/com/android/server/resources/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 568761
+
+patb@google.com
+zyy@google.com
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 25ff023..0b28ba2 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -228,8 +228,8 @@
                 SurfaceControl dragSurface = null;
                 if (!mDragResult && (ws.mSession.mPid == mPid)) {
                     // Report unconsumed drop location back to the app that started the drag.
-                    x = mCurrentX;
-                    y = mCurrentY;
+                    x = ws.translateToWindowX(mCurrentX);
+                    y = ws.translateToWindowY(mCurrentY);
                     if (relinquishDragSurfaceToDragSource()) {
                         // If requested (and allowed), report the drag surface back to the app
                         // starting the drag to handle the return animation
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index b9739f03..e1a1f57 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -291,7 +291,11 @@
     public void finishDrawing(IWindow window,
             @Nullable SurfaceControl.Transaction postDrawTransaction, int seqId) {
         if (DEBUG) Slog.v(TAG_WM, "IWindow finishDrawing called for " + window);
+        if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "finishDrawing: " + mPackageName);
+        }
         mService.finishDrawingWindow(this, window, postDrawTransaction, seqId);
+        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4004d65..6074dc8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -4568,7 +4568,7 @@
     float translateToWindowX(float x) {
         float winX = x - mWindowFrames.mFrame.left;
         if (mGlobalScale != 1f) {
-            winX *= mGlobalScale;
+            winX *= mInvGlobalScale;
         }
         return winX;
     }
@@ -4576,7 +4576,7 @@
     float translateToWindowY(float y) {
         float winY = y - mWindowFrames.mFrame.top;
         if (mGlobalScale != 1f) {
-            winY *= mGlobalScale;
+            winY *= mInvGlobalScale;
         }
         return winY;
     }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
index b41fd39..1f66a11 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt
@@ -17,12 +17,7 @@
 package com.android.server.pm.test.parsing.parcelling
 
 import android.content.Intent
-import android.content.pm.ApplicationInfo
-import android.content.pm.ConfigurationInfo
-import android.content.pm.FeatureGroupInfo
-import android.content.pm.FeatureInfo
-import android.content.pm.PackageManager
-import android.content.pm.SigningDetails
+import android.content.pm.*
 import android.net.Uri
 import android.os.Bundle
 import android.os.Parcelable
@@ -32,18 +27,7 @@
 import com.android.internal.R
 import com.android.server.pm.parsing.pkg.PackageImpl
 import com.android.server.pm.pkg.AndroidPackage
-import com.android.server.pm.pkg.component.ParsedActivityImpl
-import com.android.server.pm.pkg.component.ParsedApexSystemServiceImpl
-import com.android.server.pm.pkg.component.ParsedAttributionImpl
-import com.android.server.pm.pkg.component.ParsedComponentImpl
-import com.android.server.pm.pkg.component.ParsedInstrumentationImpl
-import com.android.server.pm.pkg.component.ParsedIntentInfoImpl
-import com.android.server.pm.pkg.component.ParsedPermissionGroupImpl
-import com.android.server.pm.pkg.component.ParsedPermissionImpl
-import com.android.server.pm.pkg.component.ParsedProcessImpl
-import com.android.server.pm.pkg.component.ParsedProviderImpl
-import com.android.server.pm.pkg.component.ParsedServiceImpl
-import com.android.server.pm.pkg.component.ParsedUsesPermissionImpl
+import com.android.server.pm.pkg.component.*
 import com.android.server.testutils.mockThrowOnUnmocked
 import com.android.server.testutils.whenever
 import java.security.KeyPairGenerator
@@ -103,6 +87,7 @@
         "getRequestedPermissions",
         // Tested through asSplit
         "asSplit",
+        "getSplits",
         "getSplitNames",
         "getSplitCodePaths",
         "getSplitRevisionCodes",
@@ -175,9 +160,9 @@
         AndroidPackage::getSecondaryNativeLibraryDir,
         AndroidPackage::getSharedUserId,
         AndroidPackage::getSharedUserLabel,
-        AndroidPackage::getSdkLibName,
+        AndroidPackage::getSdkLibraryName,
         AndroidPackage::getSdkLibVersionMajor,
-        AndroidPackage::getStaticSharedLibName,
+        AndroidPackage::getStaticSharedLibraryName,
         AndroidPackage::getStaticSharedLibVersion,
         AndroidPackage::getTargetSandboxVersion,
         AndroidPackage::getTargetSdkVersion,
@@ -550,6 +535,7 @@
             SparseArray<IntArray>().apply {
                 put(0, intArrayOf(-1))
                 put(1, intArrayOf(0))
+                put(2, intArrayOf(1))
             }
         )
         .setSplitHasCode(0, true)
@@ -599,9 +585,10 @@
 
         expect.that(after.splitDependencies).isNotNull()
         after.splitDependencies?.let {
-            expect.that(it.size()).isEqualTo(2)
+            expect.that(it.size()).isEqualTo(3)
             expect.that(it.get(0)).asList().containsExactly(-1)
             expect.that(it.get(1)).asList().containsExactly(0)
+            expect.that(it.get(2)).asList().containsExactly(1)
         }
 
         expect.that(after.usesSdkLibraries).containsExactly("testSdk")
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
index 7e9e433..8a855e5 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
@@ -95,8 +95,6 @@
             ParsedProvider::getUriPermissionPatterns,
             ParsedService::getIntents,
             ParsedService::getProperties,
-            SharedLibraryInfo::getAllCodePaths,
-            SharedLibraryInfo::getDependencies,
             Intent::getCategories,
             PackageUserState::getDisabledComponents,
             PackageUserState::getEnabledComponents,
@@ -149,6 +147,20 @@
      */
     private fun fillMissingData(pkgSetting: PackageSetting, pkg: PackageImpl) {
         pkgSetting.addUsesLibraryFile("usesLibraryFile")
+
+        val sharedLibraryDependency = listOf(SharedLibraryInfo(
+            "pathDependency",
+            "packageNameDependency",
+            listOf(tempFolder.newFile().path),
+            "nameDependency",
+            1,
+            0,
+            VersionedPackage("versionedPackage0Dependency", 1),
+            listOf(VersionedPackage("versionedPackage1Dependency", 2)),
+            emptyList(),
+            false
+        ))
+
         pkgSetting.addUsesLibraryInfo(SharedLibraryInfo(
             "path",
             "packageName",
@@ -158,7 +170,7 @@
             0,
             VersionedPackage("versionedPackage0", 1),
             listOf(VersionedPackage("versionedPackage1", 2)),
-            emptyList(),
+            sharedLibraryDependency,
             false
         ))
         pkgSetting.addMimeTypes("mimeGroup", setOf("mimeType"))
@@ -233,7 +245,13 @@
                     }
 
                     val value = try {
-                        collection.stream().findFirst().get()!!
+                        if (AndroidPackage::getSplits == it) {
+                            // The base split is defined to never have any dependencies,
+                            // so force the visitor to use the split at index 1 instead of 0.
+                            collection.last()
+                        } else {
+                            collection.first()
+                        }
                     } catch (e: Exception) {
                         if (enforceNonEmpty) {
                             expect.withMessage("Method $newChainText ${it.name} returns empty")
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
index af96346..e09b80e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java
@@ -18,17 +18,33 @@
 
 import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList;
 import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList;
+import static com.android.server.am.BroadcastQueueTest.CLASS_GREEN;
+import static com.android.server.am.BroadcastQueueTest.PACKAGE_BLUE;
+import static com.android.server.am.BroadcastQueueTest.PACKAGE_GREEN;
+import static com.android.server.am.BroadcastQueueTest.PACKAGE_RED;
+import static com.android.server.am.BroadcastQueueTest.PACKAGE_YELLOW;
+import static com.android.server.am.BroadcastQueueTest.getUidForPackage;
+import static com.android.server.am.BroadcastQueueTest.makeManifestReceiver;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.doReturn;
 
 import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.app.BroadcastOptions;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.HandlerThread;
+import android.os.UserHandle;
 import android.provider.Settings;
 
 import androidx.test.filters.SmallTest;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,6 +57,8 @@
 @SmallTest
 @RunWith(MockitoJUnitRunner.class)
 public class BroadcastQueueModernImplTest {
+    private static final int TEST_UID = android.os.Process.FIRST_APPLICATION_UID;
+
     @Mock ActivityManagerService mAms;
 
     @Mock BroadcastProcessQueue mQueue1;
@@ -49,6 +67,8 @@
     @Mock BroadcastProcessQueue mQueue4;
 
     HandlerThread mHandlerThread;
+
+    BroadcastConstants mConstants;
     BroadcastQueueModernImpl mImpl;
 
     BroadcastProcessQueue mHead;
@@ -59,9 +79,10 @@
 
         mHandlerThread = new HandlerThread(getClass().getSimpleName());
         mHandlerThread.start();
+
+        mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS);
         mImpl = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(),
-                new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS),
-                new BroadcastConstants(Settings.Global.BROADCAST_BG_CONSTANTS));
+                mConstants, mConstants);
 
         doReturn(1L).when(mQueue1).getRunnableAt();
         doReturn(2L).when(mQueue2).getRunnableAt();
@@ -69,6 +90,11 @@
         doReturn(4L).when(mQueue4).getRunnableAt();
     }
 
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+    }
+
     private static void assertOrphan(BroadcastProcessQueue queue) {
         assertNull(queue.runnableAtNext);
         assertNull(queue.runnableAtPrev);
@@ -94,8 +120,31 @@
         }
     }
 
+    private BroadcastRecord makeBroadcastRecord(Intent intent) {
+        return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(),
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), false);
+    }
+
+    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent) {
+        return makeBroadcastRecord(intent, BroadcastOptions.makeBasic(),
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), true);
+    }
+
+    private BroadcastRecord makeBroadcastRecord(Intent intent, BroadcastOptions options) {
+        return makeBroadcastRecord(intent, options,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), false);
+    }
+
+    private BroadcastRecord makeBroadcastRecord(Intent intent, BroadcastOptions options,
+            List receivers, boolean ordered) {
+        return new BroadcastRecord(mImpl, intent, null, PACKAGE_RED, null, 21, 42, false, null,
+                null, null, null, AppOpsManager.OP_NONE, options, receivers, null,
+                Activity.RESULT_OK, null, null, ordered, false, false, UserHandle.USER_SYSTEM,
+                false, null, false, null);
+    }
+
     @Test
-    public void testRunnableAt_Simple() {
+    public void testRunnableList_Simple() {
         assertRunnableList(List.of(), mHead);
 
         mHead = insertIntoRunnableList(mHead, mQueue1);
@@ -106,7 +155,7 @@
     }
 
     @Test
-    public void testRunnableAt_InsertLast() {
+    public void testRunnableList_InsertLast() {
         mHead = insertIntoRunnableList(mHead, mQueue1);
         mHead = insertIntoRunnableList(mHead, mQueue2);
         mHead = insertIntoRunnableList(mHead, mQueue3);
@@ -115,7 +164,7 @@
     }
 
     @Test
-    public void testRunnableAt_InsertFirst() {
+    public void testRunnableList_InsertFirst() {
         mHead = insertIntoRunnableList(mHead, mQueue4);
         mHead = insertIntoRunnableList(mHead, mQueue3);
         mHead = insertIntoRunnableList(mHead, mQueue2);
@@ -124,7 +173,7 @@
     }
 
     @Test
-    public void testRunnableAt_InsertMiddle() {
+    public void testRunnableList_InsertMiddle() {
         mHead = insertIntoRunnableList(mHead, mQueue1);
         mHead = insertIntoRunnableList(mHead, mQueue3);
         mHead = insertIntoRunnableList(mHead, mQueue2);
@@ -132,7 +181,7 @@
     }
 
     @Test
-    public void testRunnableAt_Remove() {
+    public void testRunnableList_Remove() {
         mHead = insertIntoRunnableList(mHead, mQueue1);
         mHead = insertIntoRunnableList(mHead, mQueue2);
         mHead = insertIntoRunnableList(mHead, mQueue3);
@@ -156,4 +205,128 @@
         assertOrphan(mQueue3);
         assertOrphan(mQueue4);
     }
+
+    @Test
+    public void testProcessQueue_Complex() {
+        BroadcastProcessQueue red = mImpl.getOrCreateProcessQueue(PACKAGE_RED, TEST_UID);
+        BroadcastProcessQueue green = mImpl.getOrCreateProcessQueue(PACKAGE_GREEN, TEST_UID);
+        BroadcastProcessQueue blue = mImpl.getOrCreateProcessQueue(PACKAGE_BLUE, TEST_UID);
+
+        assertEquals(PACKAGE_RED, red.processName);
+        assertEquals(PACKAGE_GREEN, green.processName);
+        assertEquals(PACKAGE_BLUE, blue.processName);
+
+        // Verify that removing middle queue works
+        mImpl.removeProcessQueue(PACKAGE_GREEN, TEST_UID);
+        assertEquals(red, mImpl.getProcessQueue(PACKAGE_RED, TEST_UID));
+        assertNull(mImpl.getProcessQueue(PACKAGE_GREEN, TEST_UID));
+        assertEquals(blue, mImpl.getProcessQueue(PACKAGE_BLUE, TEST_UID));
+        assertNull(mImpl.getProcessQueue(PACKAGE_YELLOW, TEST_UID));
+
+        // Verify that removing head queue works
+        mImpl.removeProcessQueue(PACKAGE_RED, TEST_UID);
+        assertNull(mImpl.getProcessQueue(PACKAGE_RED, TEST_UID));
+        assertNull(mImpl.getProcessQueue(PACKAGE_GREEN, TEST_UID));
+        assertEquals(blue, mImpl.getProcessQueue(PACKAGE_BLUE, TEST_UID));
+        assertNull(mImpl.getProcessQueue(PACKAGE_YELLOW, TEST_UID));
+
+        // Verify that removing last queue works
+        mImpl.removeProcessQueue(PACKAGE_BLUE, TEST_UID);
+        assertNull(mImpl.getProcessQueue(PACKAGE_RED, TEST_UID));
+        assertNull(mImpl.getProcessQueue(PACKAGE_GREEN, TEST_UID));
+        assertNull(mImpl.getProcessQueue(PACKAGE_BLUE, TEST_UID));
+        assertNull(mImpl.getProcessQueue(PACKAGE_YELLOW, TEST_UID));
+
+        // Verify that removing missing doesn't crash
+        mImpl.removeProcessQueue(PACKAGE_YELLOW, TEST_UID);
+
+        // Verify that we can start all over again safely
+        BroadcastProcessQueue yellow = mImpl.getOrCreateProcessQueue(PACKAGE_YELLOW, TEST_UID);
+        assertEquals(yellow, mImpl.getProcessQueue(PACKAGE_YELLOW, TEST_UID));
+    }
+
+    /**
+     * Empty queue isn't runnable.
+     */
+    @Test
+    public void testRunnableAt_Empty() {
+        BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
+                PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+        assertFalse(queue.isRunnable());
+        assertEquals(Long.MAX_VALUE, queue.getRunnableAt());
+    }
+
+    /**
+     * Queue with a "normal" broadcast is runnable at different times depending
+     * on process cached state; when cached it's delayed by some amount.
+     */
+    @Test
+    public void testRunnableAt_Normal() {
+        BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
+                PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
+        queue.enqueueBroadcast(airplaneRecord, 0);
+
+        queue.setProcessCached(false);
+        final long notCachedRunnableAt = queue.getRunnableAt();
+        queue.setProcessCached(true);
+        final long cachedRunnableAt = queue.getRunnableAt();
+        assertTrue(cachedRunnableAt > notCachedRunnableAt);
+    }
+
+    /**
+     * Queue with foreground broadcast is always runnable immediately,
+     * regardless of process cached state.
+     */
+    @Test
+    public void testRunnableAt_Foreground() {
+        BroadcastProcessQueue queue = new BroadcastProcessQueue(mConstants,
+                PACKAGE_GREEN, getUidForPackage(PACKAGE_GREEN));
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane);
+        queue.enqueueBroadcast(airplaneRecord, 0);
+
+        queue.setProcessCached(false);
+        assertTrue(queue.isRunnable());
+        assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
+
+        queue.setProcessCached(true);
+        assertTrue(queue.isRunnable());
+        assertEquals(airplaneRecord.enqueueTime, queue.getRunnableAt());
+    }
+
+    /**
+     * Verify that sending a broadcast that removes any matching pending
+     * broadcasts is applied as expected.
+     */
+    @Test
+    public void testRemoveMatchingFilter() {
+        final Intent screenOn = new Intent(Intent.ACTION_SCREEN_ON);
+        final BroadcastOptions optionsOn = BroadcastOptions.makeBasic();
+        optionsOn.setRemoveMatchingFilter(new IntentFilter(Intent.ACTION_SCREEN_OFF));
+
+        final Intent screenOff = new Intent(Intent.ACTION_SCREEN_OFF);
+        final BroadcastOptions optionsOff = BroadcastOptions.makeBasic();
+        optionsOff.setRemoveMatchingFilter(new IntentFilter(Intent.ACTION_SCREEN_ON));
+
+        // Halt all processing so that we get a consistent view
+        mHandlerThread.getLooper().getQueue().postSyncBarrier();
+
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, optionsOn));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, optionsOff));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOn, optionsOn));
+        mImpl.enqueueBroadcastLocked(makeBroadcastRecord(screenOff, optionsOff));
+
+        // Marching through the queue we should only have one SCREEN_OFF
+        // broadcast, since that's the last state we dispatched
+        final BroadcastProcessQueue queue = mImpl.getProcessQueue(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN));
+        queue.makeActiveNextPending();
+        assertEquals(Intent.ACTION_SCREEN_OFF, queue.getActive().intent.getAction());
+        assertTrue(queue.isEmpty());
+    }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index d3ceec8..cf5d113 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -16,22 +16,34 @@
 
 package com.android.server.am;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Activity;
+import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.BroadcastOptions;
 import android.app.IApplicationThread;
+import android.app.usage.UsageEvents.Event;
+import android.app.usage.UsageStatsManagerInternal;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.IIntentReceiver;
@@ -39,12 +51,15 @@
 import android.content.IntentFilter;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.ResolveInfo;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.PowerExemptionManager;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.Log;
@@ -58,6 +73,7 @@
 import com.android.server.appop.AppOpsService;
 import com.android.server.wm.ActivityTaskManagerService;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -65,14 +81,23 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 import org.mockito.ArgumentMatcher;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.verification.VerificationMode;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.UnaryOperator;
 
 /**
  * Common tests for {@link BroadcastQueue} implementations.
@@ -104,6 +129,8 @@
     private ProcessList mProcessList;
     @Mock
     private PackageManagerInternal mPackageManagerInt;
+    @Mock
+    private UsageStatsManagerInternal mUsageStatsManagerInt;
 
     private ActivityManagerService mAms;
     private BroadcastQueue mQueue;
@@ -113,6 +140,11 @@
      */
     private SparseArray<ReceiverList> mRegisteredReceivers = new SparseArray<>();
 
+    /**
+     * Collection of all active processes during current test run.
+     */
+    private List<ProcessRecord> mActiveProcesses = new ArrayList<>();
+
     @Parameters(name = "impl={0}")
     public static Collection<Object[]> data() {
         return Arrays.asList(new Object[][] { {Impl.DEFAULT}, {Impl.MODERN} });
@@ -136,13 +168,18 @@
         LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
         doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
         doNothing().when(mPackageManagerInt).setPackageStoppedState(any(), anyBoolean(), anyInt());
+        doAnswer((invocation) -> {
+            return getUidForPackage(invocation.getArgument(0));
+        }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
 
         final ActivityManagerService realAms = new ActivityManagerService(
                 new TestInjector(mContext), mServiceThreadRule.getThread());
         realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
         realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
         realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
+        realAms.mOomAdjuster.mCachedAppOptimizer = spy(realAms.mOomAdjuster.mCachedAppOptimizer);
         realAms.mPackageManagerInt = mPackageManagerInt;
+        realAms.mUsageStatsService = mUsageStatsManagerInt;
         realAms.mProcessesReady = true;
         mAms = spy(realAms);
         doAnswer((invocation) -> {
@@ -150,7 +187,8 @@
                     + Arrays.toString(invocation.getArguments()));
             final String processName = invocation.getArgument(0);
             final ApplicationInfo ai = invocation.getArgument(1);
-            final ProcessRecord res = makeActiveProcessRecord(ai, processName, false);
+            final ProcessRecord res = makeActiveProcessRecord(ai, processName, false,
+                    false, UnaryOperator.identity());
             mHandlerThread.getThreadHandler().post(() -> {
                 synchronized (mAms) {
                     mQueue.onApplicationAttachedLocked(res);
@@ -164,6 +202,7 @@
         final BroadcastConstants constants = new BroadcastConstants(
                 Settings.Global.BROADCAST_FG_CONSTANTS);
         constants.TIMEOUT = 100;
+        constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0;
         final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) {
             public boolean shouldSkip(BroadcastRecord r, ResolveInfo info) {
                 return false;
@@ -189,6 +228,18 @@
         }
     }
 
+    @After
+    public void tearDown() throws Exception {
+        mHandlerThread.quit();
+
+        // Verify that all processes have finished handling broadcasts
+        for (ProcessRecord app : mActiveProcesses) {
+            assertTrue(app.toShortString(), app.mReceivers.numberOfCurReceivers() == 0);
+            assertTrue(app.toShortString(), mQueue.getPreferredSchedulingGroupLocked(app)
+                    == ProcessList.SCHED_GROUP_UNDEFINED);
+        }
+    }
+
     private class TestInjector extends Injector {
         TestInjector(Context context) {
             super(context);
@@ -210,20 +261,40 @@
         }
     }
 
-    private ProcessRecord makeActiveProcessRecord(String packageName) throws Exception {
-        final ApplicationInfo ai = makeApplicationInfo(packageName);
-        return makeActiveProcessRecord(ai, ai.processName, false);
+    /**
+     * Helper that leverages try-with-resources to pause dispatch of
+     * {@link #mHandlerThread} until released.
+     */
+    private class SyncBarrier implements AutoCloseable {
+        private final int mToken;
+
+        public SyncBarrier() {
+            mToken = mHandlerThread.getLooper().getQueue().postSyncBarrier();
+        }
+
+        @Override
+        public void close() throws Exception {
+            mHandlerThread.getLooper().getQueue().removeSyncBarrier(mToken);
+        }
     }
 
-    private ProcessRecord makeActiveProcessRecordWedged(String packageName) throws Exception {
+    private ProcessRecord makeActiveProcessRecord(String packageName) throws Exception {
         final ApplicationInfo ai = makeApplicationInfo(packageName);
-        return makeActiveProcessRecord(ai, ai.processName, true);
+        return makeActiveProcessRecord(ai, ai.processName, false, false,
+                UnaryOperator.identity());
+    }
+
+    private ProcessRecord makeWedgedActiveProcessRecord(String packageName) throws Exception {
+        final ApplicationInfo ai = makeApplicationInfo(packageName);
+        return makeActiveProcessRecord(ai, ai.processName, true, false,
+                UnaryOperator.identity());
     }
 
     private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai, String processName,
-            boolean wedged) throws Exception {
-        final ProcessRecord r = new ProcessRecord(mAms, ai, processName, ai.uid);
+            boolean wedged, boolean abort, UnaryOperator<Bundle> extrasOperator) throws Exception {
+        final ProcessRecord r = spy(new ProcessRecord(mAms, ai, processName, ai.uid));
         r.setPid(mNextPid.getAndIncrement());
+        mActiveProcesses.add(r);
 
         final IApplicationThread thread = mock(IApplicationThread.class);
         final IBinder threadBinder = new Binder();
@@ -241,11 +312,15 @@
         doAnswer((invocation) -> {
             Log.v(TAG, "Intercepting scheduleReceiver() for "
                     + Arrays.toString(invocation.getArguments()));
+            final Bundle extras = invocation.getArgument(5);
             if (!wedged) {
+                assertTrue(r.mReceivers.numberOfCurReceivers() > 0);
+                assertTrue(mQueue.getPreferredSchedulingGroupLocked(r)
+                        != ProcessList.SCHED_GROUP_UNDEFINED);
                 mHandlerThread.getThreadHandler().post(() -> {
                     synchronized (mAms) {
                         mQueue.finishReceiverLocked(r, Activity.RESULT_OK,
-                                null, null, false, false);
+                                null, extrasOperator.apply(extras), abort, false);
                     }
                 });
             }
@@ -256,12 +331,16 @@
         doAnswer((invocation) -> {
             Log.v(TAG, "Intercepting scheduleRegisteredReceiver() for "
                     + Arrays.toString(invocation.getArguments()));
+            final Bundle extras = invocation.getArgument(4);
             final boolean ordered = invocation.getArgument(5);
             if (!wedged && ordered) {
+                assertTrue(r.mReceivers.numberOfCurReceivers() > 0);
+                assertTrue(mQueue.getPreferredSchedulingGroupLocked(r)
+                        != ProcessList.SCHED_GROUP_UNDEFINED);
                 mHandlerThread.getThreadHandler().post(() -> {
                     synchronized (mAms) {
                         mQueue.finishReceiverLocked(r, Activity.RESULT_OK,
-                                null, null, false, false);
+                                null, extrasOperator.apply(extras), abort, false);
                     }
                 });
             }
@@ -272,7 +351,7 @@
         return r;
     }
 
-    private ApplicationInfo makeApplicationInfo(String packageName) {
+    static ApplicationInfo makeApplicationInfo(String packageName) {
         final ApplicationInfo ai = new ApplicationInfo();
         ai.packageName = packageName;
         ai.processName = packageName;
@@ -280,7 +359,7 @@
         return ai;
     }
 
-    private ResolveInfo makeManifestReceiver(String packageName, String name) {
+    static ResolveInfo makeManifestReceiver(String packageName, String name) {
         final ResolveInfo ri = new ResolveInfo();
         ri.activityInfo = new ActivityInfo();
         ri.activityInfo.packageName = packageName;
@@ -302,15 +381,35 @@
 
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             List receivers) {
-        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(), receivers);
+        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
+                receivers, false, null, null);
+    }
+
+    private BroadcastRecord makeOrderedBroadcastRecord(Intent intent, ProcessRecord callerApp,
+            List receivers, IIntentReceiver orderedResultTo, Bundle orderedExtras) {
+        return makeBroadcastRecord(intent, callerApp, BroadcastOptions.makeBasic(),
+                receivers, true, orderedResultTo, orderedExtras);
     }
 
     private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
             BroadcastOptions options, List receivers) {
+        return makeBroadcastRecord(intent, callerApp, options, receivers, false, null, null);
+    }
+
+    private BroadcastRecord makeBroadcastRecord(Intent intent, ProcessRecord callerApp,
+            BroadcastOptions options, List receivers, boolean ordered,
+            IIntentReceiver orderedResultTo, Bundle orderedExtras) {
         return new BroadcastRecord(mQueue, intent, callerApp, callerApp.info.packageName, null,
                 callerApp.getPid(), callerApp.info.uid, false, null, null, null, null,
-                AppOpsManager.OP_NONE, options, receivers, null, Activity.RESULT_OK, null, null,
-                false, false, false, UserHandle.USER_SYSTEM, false, null, false, null);
+                AppOpsManager.OP_NONE, options, receivers, orderedResultTo, Activity.RESULT_OK,
+                null, orderedExtras, ordered, false, false, UserHandle.USER_SYSTEM, false, null,
+                false, null);
+    }
+
+    private ArgumentMatcher<Intent> filterEquals(Intent intent) {
+        return (test) -> {
+            return intent.filterEquals(test);
+        };
     }
 
     private ArgumentMatcher<Intent> filterEqualsIgnoringComponent(Intent intent) {
@@ -323,6 +422,17 @@
         };
     }
 
+    private ArgumentMatcher<Bundle> bundleEquals(Bundle bundle) {
+        return (test) -> {
+            // TODO: check values in addition to keys
+            return Objects.equals(test.keySet(), bundle.keySet());
+        };
+    }
+
+    private @NonNull Bundle clone(@Nullable Bundle b) {
+        return (b != null) ? new Bundle(b) : new Bundle();
+    }
+
     private void enqueueBroadcast(BroadcastRecord r) {
         synchronized (mAms) {
             mQueue.enqueueBroadcastLocked(r);
@@ -339,6 +449,15 @@
                 any(), eq(false), eq(UserHandle.USER_SYSTEM), anyInt());
     }
 
+    private void verifyScheduleReceiver(VerificationMode mode, ProcessRecord app, Intent intent,
+            ComponentName component) throws Exception {
+        final Intent targetedIntent = new Intent(intent);
+        targetedIntent.setComponent(component);
+        verify(app.getThread(), mode).scheduleReceiver(
+                argThat(filterEquals(targetedIntent)), any(), any(), anyInt(), any(),
+                any(), eq(false), eq(UserHandle.USER_SYSTEM), anyInt());
+    }
+
     private void verifyScheduleRegisteredReceiver(ProcessRecord app, Intent intent)
             throws Exception {
         verify(app.getThread()).scheduleRegisteredReceiver(any(),
@@ -346,17 +465,19 @@
                 anyBoolean(), anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt());
     }
 
-    private static final String PACKAGE_RED = "com.example.red";
-    private static final String PACKAGE_GREEN = "com.example.green";
-    private static final String PACKAGE_BLUE = "com.example.blue";
-    private static final String PACKAGE_YELLOW = "com.example.yellow";
+    static final int USER_GUEST = 11;
 
-    private static final String CLASS_RED = "com.example.red.Red";
-    private static final String CLASS_GREEN = "com.example.green.Green";
-    private static final String CLASS_BLUE = "com.example.blue.Blue";
-    private static final String CLASS_YELLOW = "com.example.yellow.Yellow";
+    static final String PACKAGE_RED = "com.example.red";
+    static final String PACKAGE_GREEN = "com.example.green";
+    static final String PACKAGE_BLUE = "com.example.blue";
+    static final String PACKAGE_YELLOW = "com.example.yellow";
 
-    private static int getUidForPackage(String packageName) {
+    static final String CLASS_RED = "com.example.red.Red";
+    static final String CLASS_GREEN = "com.example.green.Green";
+    static final String CLASS_BLUE = "com.example.blue.Blue";
+    static final String CLASS_YELLOW = "com.example.yellow.Yellow";
+
+    static int getUidForPackage(@NonNull String packageName) {
         switch (packageName) {
             case PACKAGE_RED: return android.os.Process.FIRST_APPLICATION_UID + 1;
             case PACKAGE_GREEN: return android.os.Process.FIRST_APPLICATION_UID + 2;
@@ -366,6 +487,14 @@
         }
     }
 
+    @Test
+    public void testDump() throws Exception {
+        // To maximize test coverage, dump current state; we're not worried
+        // about the actual output, just that we don't crash
+        mQueue.dumpLocked(FileDescriptor.err, new PrintWriter(new ByteArrayOutputStream()),
+                null, 0, true, null, false);
+    }
+
     /**
      * Verify dispatch of simple broadcast to single manifest receiver in
      * already-running warm app.
@@ -501,7 +630,10 @@
                         makeRegisteredReceiver(receiverYellowApp))));
 
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
-        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+        airplane.setComponent(new ComponentName(PACKAGE_YELLOW, CLASS_YELLOW));
+        final BroadcastOptions options = BroadcastOptions.makeBasic();
+        options.recordResponseEventWhileInBackground(42L);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, options,
                 List.of(makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW))));
 
         waitForIdle();
@@ -512,6 +644,43 @@
         verifyScheduleReceiver(receiverBlueApp, timezone);
         verifyScheduleRegisteredReceiver(receiverYellowApp, timezone);
         verifyScheduleReceiver(receiverYellowApp, airplane);
+
+        for (ProcessRecord receiverApp : new ProcessRecord[] {
+                receiverGreenApp, receiverBlueApp, receiverYellowApp
+        }) {
+            // Confirm expected OOM adjustments; we were invoked once to upgrade
+            // and once to downgrade
+            assertEquals(ActivityManager.PROCESS_STATE_RECEIVER,
+                    receiverApp.mState.getReportedProcState());
+            verify(mAms, times(2)).enqueueOomAdjTargetLocked(eq(receiverApp));
+
+            if ((mImpl == Impl.DEFAULT) && (receiverApp == receiverBlueApp)) {
+                // Nuance: the default implementation doesn't ask for manifest
+                // cold-started apps to be thawed, but the modern stack does
+            } else {
+                // Confirm that app was thawed
+                verify(mAms.mOomAdjuster.mCachedAppOptimizer).unfreezeTemporarily(eq(receiverApp),
+                        eq(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER));
+
+                // Confirm that we added package to process
+                verify(receiverApp, atLeastOnce()).addPackage(eq(receiverApp.info.packageName),
+                        anyLong(), any());
+            }
+
+            // Confirm that we've reported package as being used
+            verify(mAms, atLeastOnce()).notifyPackageUse(eq(receiverApp.info.packageName),
+                    eq(PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER));
+
+            // Confirm that we unstopped manifest receivers
+            verify(mAms.mPackageManagerInt, atLeastOnce()).setPackageStoppedState(
+                    eq(receiverApp.info.packageName), eq(false), eq(UserHandle.USER_SYSTEM));
+        }
+
+        // Confirm that we've reported expected usage events
+        verify(mAms.mUsageStatsService).reportBroadcastDispatched(eq(callerApp.uid),
+                eq(PACKAGE_YELLOW), eq(UserHandle.SYSTEM), eq(42L), anyLong(), anyInt());
+        verify(mAms.mUsageStatsService).reportEvent(eq(PACKAGE_YELLOW), eq(UserHandle.USER_SYSTEM),
+                eq(Event.APP_COMPONENT_USED));
     }
 
     /**
@@ -520,7 +689,7 @@
     @Test
     public void testWedged() throws Exception {
         final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
-        final ProcessRecord receiverApp = makeActiveProcessRecordWedged(PACKAGE_GREEN);
+        final ProcessRecord receiverApp = makeWedgedActiveProcessRecord(PACKAGE_GREEN);
 
         final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
         enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
@@ -529,4 +698,250 @@
         waitForIdle();
         verify(mAms).appNotResponding(eq(receiverApp), any());
     }
+
+    /**
+     * Verify that we cleanup a disabled component, skipping a pending dispatch
+     * of broadcast to that component.
+     */
+    @Test
+    public void testCleanup() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        try (SyncBarrier b = new SyncBarrier()) {
+            enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, new ArrayList<>(
+                    List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_RED),
+                            makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
+                            makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE)))));
+
+            synchronized (mAms) {
+                mQueue.cleanupDisabledPackageReceiversLocked(PACKAGE_GREEN, Set.of(CLASS_GREEN),
+                        UserHandle.USER_SYSTEM);
+
+                // Also try clearing out other unrelated things that should leave
+                // the final receiver intact
+                mQueue.cleanupDisabledPackageReceiversLocked(PACKAGE_RED, null,
+                        UserHandle.USER_SYSTEM);
+                mQueue.cleanupDisabledPackageReceiversLocked(null, null, USER_GUEST);
+            }
+
+            // To maximize test coverage, dump current state; we're not worried
+            // about the actual output, just that we don't crash
+            mQueue.dumpLocked(FileDescriptor.err, new PrintWriter(new ByteArrayOutputStream()),
+                    null, 0, true, null, false);
+        }
+
+        waitForIdle();
+        verifyScheduleReceiver(times(1), receiverApp, airplane,
+                new ComponentName(PACKAGE_GREEN, CLASS_RED));
+        verifyScheduleReceiver(never(), receiverApp, airplane,
+                new ComponentName(PACKAGE_GREEN, CLASS_GREEN));
+        verifyScheduleReceiver(times(1), receiverApp, airplane,
+                new ComponentName(PACKAGE_GREEN, CLASS_BLUE));
+    }
+
+    /**
+     * Verify that we skip broadcasts to an app being backed up.
+     */
+    @Test
+    public void testBackup() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
+        receiverApp.setInFullBackup(true);
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN))));
+
+        waitForIdle();
+        verifyScheduleReceiver(never(), receiverApp, airplane,
+                new ComponentName(PACKAGE_GREEN, CLASS_GREEN));
+    }
+
+    /**
+     * Verify that an ordered broadcast collects results from everyone along the
+     * chain, and is delivered to final destination.
+     */
+    @Test
+    public void testOrdered() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+
+        // Purposefully warm-start the middle apps to make sure we dispatch to
+        // both cold and warm apps in expected order
+        makeActiveProcessRecord(makeApplicationInfo(PACKAGE_BLUE), PACKAGE_BLUE,
+                false, false, (extras) -> {
+                    extras = clone(extras);
+                    extras.putBoolean(PACKAGE_BLUE, true);
+                    return extras;
+                });
+        makeActiveProcessRecord(makeApplicationInfo(PACKAGE_YELLOW), PACKAGE_YELLOW,
+                false, false, (extras) -> {
+                    extras = clone(extras);
+                    extras.putBoolean(PACKAGE_YELLOW, true);
+                    return extras;
+                });
+
+        final IIntentReceiver orderedResultTo = mock(IIntentReceiver.class);
+        final Bundle orderedExtras = new Bundle();
+        orderedExtras.putBoolean(PACKAGE_RED, true);
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        enqueueBroadcast(makeOrderedBroadcastRecord(airplane, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE),
+                        makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW)),
+                orderedResultTo, orderedExtras));
+
+        waitForIdle();
+        final IApplicationThread greenThread = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN)).getThread();
+        final IApplicationThread blueThread = mAms.getProcessRecordLocked(PACKAGE_BLUE,
+                getUidForPackage(PACKAGE_BLUE)).getThread();
+        final IApplicationThread yellowThread = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+                getUidForPackage(PACKAGE_YELLOW)).getThread();
+        final IApplicationThread redThread = mAms.getProcessRecordLocked(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED)).getThread();
+
+        // Verify that we called everyone in specific order, and that each of
+        // them observed the expected extras at that stage
+        final InOrder inOrder = inOrder(greenThread, blueThread, yellowThread, redThread);
+        final Bundle expectedExtras = new Bundle();
+        expectedExtras.putBoolean(PACKAGE_RED, true);
+        inOrder.verify(greenThread).scheduleReceiver(
+                argThat(filterEqualsIgnoringComponent(airplane)), any(), any(),
+                eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), eq(true),
+                eq(UserHandle.USER_SYSTEM), anyInt());
+        inOrder.verify(blueThread).scheduleReceiver(
+                argThat(filterEqualsIgnoringComponent(airplane)), any(), any(),
+                eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), eq(true),
+                eq(UserHandle.USER_SYSTEM), anyInt());
+        expectedExtras.putBoolean(PACKAGE_BLUE, true);
+        inOrder.verify(yellowThread).scheduleReceiver(
+                argThat(filterEqualsIgnoringComponent(airplane)), any(), any(),
+                eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), eq(true),
+                eq(UserHandle.USER_SYSTEM), anyInt());
+        expectedExtras.putBoolean(PACKAGE_YELLOW, true);
+        inOrder.verify(redThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(airplane)),
+                eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)), eq(false),
+                anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt());
+
+        // Finally, verify that we thawed the final receiver
+        verify(mAms.mOomAdjuster.mCachedAppOptimizer).unfreezeTemporarily(eq(callerApp),
+                eq(OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER));
+    }
+
+    /**
+     * Verify that an ordered broadcast can be aborted partially through
+     * dispatch, and is then delivered to final destination.
+     */
+    @Test
+    public void testOrdered_Aborting() throws Exception {
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        doOrdered_Aborting(airplane);
+    }
+
+    /**
+     * Verify that an ordered broadcast marked with
+     * {@link Intent#FLAG_RECEIVER_NO_ABORT} cannot be aborted partially through
+     * dispatch, and is delivered to everyone in order.
+     */
+    @Test
+    public void testOrdered_Aborting_NoAbort() throws Exception {
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        airplane.addFlags(Intent.FLAG_RECEIVER_NO_ABORT);
+        doOrdered_Aborting(airplane);
+    }
+
+    public void doOrdered_Aborting(@NonNull Intent intent) throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+
+        // Create a process that aborts any ordered broadcasts
+        makeActiveProcessRecord(makeApplicationInfo(PACKAGE_GREEN), PACKAGE_GREEN,
+                false, true, (extras) -> {
+                    extras = clone(extras);
+                    extras.putBoolean(PACKAGE_GREEN, true);
+                    return extras;
+                });
+        makeActiveProcessRecord(PACKAGE_BLUE);
+
+        final IIntentReceiver orderedResultTo = mock(IIntentReceiver.class);
+
+        enqueueBroadcast(makeOrderedBroadcastRecord(intent, callerApp,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN),
+                        makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)),
+                orderedResultTo, null));
+
+        waitForIdle();
+        final IApplicationThread greenThread = mAms.getProcessRecordLocked(PACKAGE_GREEN,
+                getUidForPackage(PACKAGE_GREEN)).getThread();
+        final IApplicationThread blueThread = mAms.getProcessRecordLocked(PACKAGE_BLUE,
+                getUidForPackage(PACKAGE_BLUE)).getThread();
+        final IApplicationThread redThread = mAms.getProcessRecordLocked(PACKAGE_RED,
+                getUidForPackage(PACKAGE_RED)).getThread();
+
+        final Bundle expectedExtras = new Bundle();
+        expectedExtras.putBoolean(PACKAGE_GREEN, true);
+
+        // Verify that we always invoke the first receiver, but then we might
+        // have invoked or skipped the second receiver depending on the intent
+        // flag policy; we always deliver to final receiver regardless of abort
+        final InOrder inOrder = inOrder(greenThread, blueThread, redThread);
+        inOrder.verify(greenThread).scheduleReceiver(
+                argThat(filterEqualsIgnoringComponent(intent)), any(), any(),
+                eq(Activity.RESULT_OK), any(), any(), eq(true), eq(UserHandle.USER_SYSTEM),
+                anyInt());
+        if ((intent.getFlags() & Intent.FLAG_RECEIVER_NO_ABORT) != 0) {
+            inOrder.verify(blueThread).scheduleReceiver(
+                    argThat(filterEqualsIgnoringComponent(intent)), any(), any(),
+                    eq(Activity.RESULT_OK), any(), any(), eq(true), eq(UserHandle.USER_SYSTEM),
+                    anyInt());
+        } else {
+            inOrder.verify(blueThread, never()).scheduleReceiver(any(), any(), any(), anyInt(),
+                    any(), any(), anyBoolean(), anyInt(), anyInt());
+        }
+        inOrder.verify(redThread).scheduleRegisteredReceiver(any(), argThat(filterEquals(intent)),
+                eq(Activity.RESULT_OK), any(), argThat(bundleEquals(expectedExtras)),
+                eq(false), anyBoolean(), eq(UserHandle.USER_SYSTEM), anyInt());
+    }
+
+    @Test
+    public void testBackgroundActivityStarts() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        final Binder backgroundActivityStartsToken = new Binder();
+        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        final BroadcastRecord r = new BroadcastRecord(mQueue, intent, callerApp,
+                callerApp.info.packageName, null, callerApp.getPid(), callerApp.info.uid, false,
+                null, null, null, null, AppOpsManager.OP_NONE, BroadcastOptions.makeBasic(),
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN)), null, Activity.RESULT_OK,
+                null, null, false, false, false, UserHandle.USER_SYSTEM, true,
+                backgroundActivityStartsToken, false, null);
+        enqueueBroadcast(r);
+
+        waitForIdle();
+        verify(receiverApp).addOrUpdateAllowBackgroundActivityStartsToken(eq(r),
+                eq(backgroundActivityStartsToken));
+        verify(receiverApp).removeAllowBackgroundActivityStartsToken(eq(r));
+    }
+
+    @Test
+    public void testOptions_TemporaryAppAllowlist() throws Exception {
+        final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+        final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN);
+
+        final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        final BroadcastOptions options = BroadcastOptions.makeBasic();
+        options.setTemporaryAppAllowlist(1_000,
+                PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
+                PowerExemptionManager.REASON_VPN, TAG);
+        enqueueBroadcast(makeBroadcastRecord(airplane, callerApp, options,
+                List.of(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN))));
+
+        waitForIdle();
+        verify(mAms).tempAllowlistUidLocked(eq(receiverApp.uid), eq(1_000L),
+                eq(options.getTemporaryAppAllowlistReasonCode()), any(),
+                eq(options.getTemporaryAppAllowlistType()), eq(callerApp.uid));
+    }
 }
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/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
index 8744f32..e28d331 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt
@@ -386,7 +386,7 @@
             pkg.setTargetSdkVersion(Build.VERSION_CODES.S)
             libraries?.forEach { pkg.addLibraryName(it) }
             staticLibrary?.let {
-                pkg.setStaticSharedLibName(it)
+                pkg.setStaticSharedLibraryName(it)
                 pkg.setStaticSharedLibVersion(staticLibraryVersion)
                 pkg.setStaticSharedLibrary(true)
             }
@@ -430,7 +430,7 @@
             setTargetSdkVersion(Build.VERSION_CODES.S)
             libraries?.forEach { addLibraryName(it) }
             staticLibrary?.let {
-                setStaticSharedLibName(it)
+                setStaticSharedLibraryName(it)
                 setStaticSharedLibVersion(staticLibraryVersion)
                 setStaticSharedLibrary(true)
             }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java
index 245b4dc..278e04a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerInternalTest.java
@@ -19,8 +19,6 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
-import static com.android.server.pm.UserManagerInternal.PARENT_DISPLAY;
-
 import static com.google.common.truth.Truth.assertWithMessage;
 
 import static org.junit.Assert.assertThrows;
@@ -185,10 +183,11 @@
         addDefaultProfileAndParent();
 
         mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
-        mUmi.assignUserToDisplay(PROFILE_USER_ID, PARENT_DISPLAY);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
 
-        assertUsersAssignedToDisplays(PARENT_USER_ID, SECONDARY_DISPLAY_ID,
-                pair(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+        Log.v(TAG, "Exception: " + e);
+        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
     }
 
     @Test
@@ -198,7 +197,20 @@
 
         mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
         IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
-                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID));
+                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, OTHER_SECONDARY_DISPLAY_ID));
+
+        Log.v(TAG, "Exception: " + e);
+        assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+    }
+
+    @Test
+    public void testAssignUserToDisplay_profileDefaultDisplayParentOnSecondaryDisplay() {
+        enableUsersOnSecondaryDisplays();
+        addDefaultProfileAndParent();
+
+        mUmi.assignUserToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
+        IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+                () -> mUmi.assignUserToDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY));
 
         Log.v(TAG, "Exception: " + e);
         assertUserAssignedToDisplay(PARENT_USER_ID, SECONDARY_DISPLAY_ID);
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java
index 6f0efb0..90a5fa0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceOrInternalTestCase.java
@@ -33,7 +33,6 @@
 import android.content.pm.UserInfo;
 import android.os.UserManager;
 import android.util.Log;
-import android.util.Pair;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 
@@ -614,24 +613,6 @@
                 .containsExactly(userId, displayId);
     }
 
-    @SafeVarargs
-    protected final void assertUsersAssignedToDisplays(@UserIdInt int userId, int displayId,
-            @SuppressWarnings("unchecked") Pair<Integer, Integer>... others) {
-        Object[] otherObjects = new Object[others.length * 2];
-        for (int i = 0; i < others.length; i++) {
-            Pair<Integer, Integer> other = others[i];
-            otherObjects[i * 2] = other.first;
-            otherObjects[i * 2 + 1] = other.second;
-
-        }
-        assertWithMessage("mUsersOnSecondaryDisplays()").that(usersOnSecondaryDisplaysAsMap())
-                .containsExactly(userId, displayId, otherObjects);
-    }
-
-    protected static Pair<Integer, Integer> pair(@UserIdInt int userId, int secondaryDisplayId) {
-        return new Pair<>(userId, secondaryDisplayId);
-    }
-
     ///////////////////
     // Private infra //
     ///////////////////
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 81f899c..96c3823 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -679,7 +679,7 @@
         setUpAndStartProfileInBackground(TEST_USER_ID1);
 
         startBackgroundUserAssertions();
-        verifyUserAssignedToDisplay(TEST_USER_ID1, UserManagerInternal.PARENT_DISPLAY);
+        verifyUserAssignedToDisplay(TEST_USER_ID1, Display.DEFAULT_DISPLAY);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
index 67eeb4e..68310f4 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java
@@ -1011,10 +1011,10 @@
                 .addUsesPermission(new ParsedUsesPermissionImpl("foo7", 0))
                 .addImplicitPermission("foo25")
                 .addProtectedBroadcast("foo8")
-                .setSdkLibName("sdk12")
+                .setSdkLibraryName("sdk12")
                 .setSdkLibVersionMajor(42)
                 .addUsesSdkLibrary("sdk23", 200, new String[]{"digest2"})
-                .setStaticSharedLibName("foo23")
+                .setStaticSharedLibraryName("foo23")
                 .setStaticSharedLibVersion(100)
                 .addUsesStaticLibrary("foo23", 100, new String[]{"digest"})
                 .addLibraryName("foo10")
diff --git a/services/tests/servicestests/src/com/android/server/pm/ScanTests.java b/services/tests/servicestests/src/com/android/server/pm/ScanTests.java
index 084f4f1..6f3249e 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ScanTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ScanTests.java
@@ -241,7 +241,7 @@
     @Test
     public void installSdkLibrary() throws Exception {
         final ParsedPackage pkg = ((ParsedPackage) createBasicPackage("ogl.sdk_123")
-                .setSdkLibName("ogl.sdk")
+                .setSdkLibraryName("ogl.sdk")
                 .setSdkLibVersionMajor(123)
                 .hideAsParsed())
                 .setPackageName("ogl.sdk_123")
@@ -272,7 +272,7 @@
     @Test
     public void installStaticSharedLibrary() throws Exception {
         final ParsedPackage pkg = ((ParsedPackage) createBasicPackage("static.lib.pkg")
-                .setStaticSharedLibName("static.lib")
+                .setStaticSharedLibraryName("static.lib")
                 .setStaticSharedLibVersion(123L)
                 .hideAsParsed())
                 .setPackageName("static.lib.pkg.123")
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());
+    }
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
index 4d801c90..8d4da8a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt
@@ -24,6 +24,7 @@
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.Rect
 import com.android.server.wm.traces.common.WindowManagerConditionsFactory
+import com.android.server.wm.traces.common.region.Region
 import com.android.server.wm.traces.parser.toFlickerComponent
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 
@@ -178,6 +179,20 @@
         wmHelper.StateSyncBuilder()
             .withAppTransitionIdle()
             .waitForAndVerify()
+        waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect))
+    }
+
+    private fun waitForPipWindowToExpandFrom(
+        wmHelper: WindowManagerStateHelper,
+        windowRect: Region
+    ) {
+        wmHelper.StateSyncBuilder().add("pipWindowExpanded") {
+            val pipAppWindow = it.wmState.visibleWindows.firstOrNull { window ->
+                this.windowMatchesAnyOf(window)
+            } ?: return@add false
+            val pipRegion = pipAppWindow.frameRegion
+            return@add pipRegion.coversMoreThan(windowRect)
+        }.waitForAndVerify()
     }
 
     companion object {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt
index 77f28f6..2babf1c8e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt
@@ -19,6 +19,7 @@
 import android.platform.test.annotations.FlakyTest
 import android.platform.test.annotations.Postsubmit
 import android.platform.test.annotations.RequiresDevice
+import android.view.Surface
 import android.view.WindowInsets
 import android.view.WindowManager
 import androidx.test.uiautomator.By
@@ -74,6 +75,13 @@
                     .withFullScreenApp(testApp)
                     .waitForAndVerify()
                 testApp.postNotification(wmHelper)
+
+                if (testSpec.isTablet) {
+                    tapl.setExpectedRotation(testSpec.startRotation)
+                } else {
+                    tapl.setExpectedRotation(Surface.ROTATION_0)
+                }
+
                 tapl.goHome()
                 wmHelper.StateSyncBuilder()
                     .withHomeActivityVisible()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
index 4f21412..d362c7d 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OverrideTaskTransitionTest.kt
@@ -31,11 +31,13 @@
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.StandardAppHelper
+import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen
 import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
 import com.android.server.wm.traces.common.ComponentNameMatcher
 import com.android.server.wm.traces.common.WindowManagerConditionsFactory
+import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -86,6 +88,7 @@
     @Presubmit
     @Test
     fun testSimpleActivityIsShownDirectly() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
         testSpec.assertLayers {
             isVisible(ComponentNameMatcher.LAUNCHER)
                 .isInvisible(ComponentNameMatcher.SPLASH_SCREEN)
diff --git a/tests/benchmarks/internal/src/com/android/internal/LambdaPerfTest.java b/tests/benchmarks/internal/src/com/android/internal/LambdaPerfTest.java
index 3885486..2001c04 100644
--- a/tests/benchmarks/internal/src/com/android/internal/LambdaPerfTest.java
+++ b/tests/benchmarks/internal/src/com/android/internal/LambdaPerfTest.java
@@ -1,454 +1,458 @@
-/*

- * Copyright (C) 2020 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.internal;

-

-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

-

-import android.app.Activity;

-import android.graphics.Rect;

-import android.os.Bundle;

-import android.os.Message;

-import android.os.ParcelFileDescriptor;

-import android.os.Process;

-import android.os.SystemClock;

-import android.util.Log;

-

-import androidx.test.filters.LargeTest;

-

-import com.android.internal.util.function.pooled.PooledConsumer;

-import com.android.internal.util.function.pooled.PooledLambda;

-import com.android.internal.util.function.pooled.PooledPredicate;

-

-import org.junit.Assume;

-import org.junit.Rule;

-import org.junit.Test;

-import org.junit.rules.TestRule;

-import org.junit.runners.model.Statement;

-

-import java.io.BufferedReader;

-import java.io.IOException;

-import java.io.InputStreamReader;

-import java.util.ArrayList;

-import java.util.Arrays;

-import java.util.List;

-import java.util.concurrent.CountDownLatch;

-import java.util.function.Consumer;

-import java.util.function.Predicate;

-import java.util.regex.Matcher;

-import java.util.regex.Pattern;

-

-/** Compares the performance of regular lambda and pooled lambda. */

-@LargeTest

-public class LambdaPerfTest {

-    private static final boolean DEBUG = false;

-    private static final String TAG = LambdaPerfTest.class.getSimpleName();

-

-    private static final String LAMBDA_FORM_REGULAR = "regular";

-    private static final String LAMBDA_FORM_POOLED = "pooled";

-

-    private static final int WARMUP_ITERATIONS = 1000;

-    private static final int TEST_ITERATIONS = 3000000;

-    private static final int TASK_COUNT = 10;

-    private static final long DELAY_AFTER_BENCH_MS = 1000;

-

-    private String mMethodName;

-

-    private final Bundle mTestResults = new Bundle();

-    private final ArrayList<Task> mTasks = new ArrayList<>();

-

-    // The member fields are used to ensure lambda capturing. They don't have the actual meaning.

-    private final Task mTask = new Task();

-    private final Rect mBounds = new Rect();

-    private int mTaskId;

-    private long mTime;

-    private boolean mTop;

-

-    @Rule

-    public final TestRule mRule = (base, description) -> new Statement() {

-        @Override

-        public void evaluate() throws Throwable {

-            mMethodName = description.getMethodName();

-            mTasks.clear();

-            for (int i = 0; i < TASK_COUNT; i++) {

-                final Task t = new Task();

-                mTasks.add(t);

-            }

-            base.evaluate();

-

-            getInstrumentation().sendStatus(Activity.RESULT_OK, mTestResults);

-        }

-    };

-

-    @Test

-    public void test1ParamConsumer() {

-        evaluate(LAMBDA_FORM_REGULAR, () -> forAllTask(t -> t.doSomething(mTask)));

-        evaluate(LAMBDA_FORM_POOLED, () -> {

-            final PooledConsumer c = PooledLambda.obtainConsumer(Task::doSomething,

-                    PooledLambda.__(Task.class), mTask);

-            forAllTask(c);

-            c.recycle();

-        });

-    }

-

-    @Test

-    public void test2PrimitiveParamsConsumer() {

-        // Not in Integer#IntegerCache (-128~127) for autoboxing, that will create new object.

-        mTaskId = 12345;

-        mTime = 54321;

-

-        evaluate(LAMBDA_FORM_REGULAR, () -> forAllTask(t -> t.doSomething(mTaskId, mTime)));

-        evaluate(LAMBDA_FORM_POOLED, () -> {

-            final PooledConsumer c = PooledLambda.obtainConsumer(Task::doSomething,

-                    PooledLambda.__(Task.class), mTaskId, mTime);

-            forAllTask(c);

-            c.recycle();

-        });

-    }

-

-    @Test

-    public void test3ParamsPredicate() {

-        mTop = true;

-        // In Integer#IntegerCache.

-        mTaskId = 10;

-

-        evaluate(LAMBDA_FORM_REGULAR, () -> handleTask(t -> t.doSomething(mBounds, mTop, mTaskId)));

-        evaluate(LAMBDA_FORM_POOLED, () -> {

-            final PooledPredicate c = PooledLambda.obtainPredicate(Task::doSomething,

-                    PooledLambda.__(Task.class), mBounds, mTop, mTaskId);

-            handleTask(c);

-            c.recycle();

-        });

-    }

-

-    @Test

-    public void testMessage() {

-        evaluate(LAMBDA_FORM_REGULAR, () -> {

-            final Message m = Message.obtain().setCallback(() -> mTask.doSomething(mTaskId, mTime));

-            m.getCallback().run();

-            m.recycle();

-        });

-        evaluate(LAMBDA_FORM_POOLED, () -> {

-            final Message m = PooledLambda.obtainMessage(Task::doSomething, mTask, mTaskId, mTime);

-            m.getCallback().run();

-            m.recycle();

-        });

-    }

-

-    @Test

-    public void testRunnable() {

-        evaluate(LAMBDA_FORM_REGULAR, () -> {

-            final Runnable r = mTask::doSomething;

-            r.run();

-        });

-        evaluate(LAMBDA_FORM_POOLED, () -> {

-            final Runnable r = PooledLambda.obtainRunnable(Task::doSomething, mTask).recycleOnUse();

-            r.run();

-        });

-    }

-

-    @Test

-    public void testMultiThread() {

-        final int numThread = 3;

-

-        final Runnable regularAction = () -> forAllTask(t -> t.doSomething(mTask));

-        final Runnable[] regularActions = new Runnable[numThread];

-        Arrays.fill(regularActions, regularAction);

-        evaluateMultiThread(LAMBDA_FORM_REGULAR, regularActions);

-

-        final Runnable pooledAction = () -> {

-            final PooledConsumer c = PooledLambda.obtainConsumer(Task::doSomething,

-                    PooledLambda.__(Task.class), mTask);

-            forAllTask(c);

-            c.recycle();

-        };

-        final Runnable[] pooledActions = new Runnable[numThread];

-        Arrays.fill(pooledActions, pooledAction);

-        evaluateMultiThread(LAMBDA_FORM_POOLED, pooledActions);

-    }

-

-    private void forAllTask(Consumer<Task> callback) {

-        for (int i = mTasks.size() - 1; i >= 0; i--) {

-            callback.accept(mTasks.get(i));

-        }

-    }

-

-    private void handleTask(Predicate<Task> callback) {

-        for (int i = mTasks.size() - 1; i >= 0; i--) {

-            final Task task = mTasks.get(i);

-            if (callback.test(task)) {

-                return;

-            }

-        }

-    }

-

-    private void evaluate(String title, Runnable action) {

-        for (int i = 0; i < WARMUP_ITERATIONS; i++) {

-            action.run();

-        }

-        performGc();

-

-        final GcStatus startGcStatus = getGcStatus();

-        final long startTime = SystemClock.elapsedRealtime();

-        for (int i = 0; i < TEST_ITERATIONS; i++) {

-            action.run();

-        }

-        evaluateResult(title, startGcStatus, startTime);

-    }

-

-    private void evaluateMultiThread(String title, Runnable[] actions) {

-        performGc();

-

-        final CountDownLatch latch = new CountDownLatch(actions.length);

-        final GcStatus startGcStatus = getGcStatus();

-        final long startTime = SystemClock.elapsedRealtime();

-        for (Runnable action : actions) {

-            new Thread() {

-                @Override

-                public void run() {

-                    for (int i = 0; i < TEST_ITERATIONS; i++) {

-                        action.run();

-                    }

-                    latch.countDown();

-                };

-            }.start();

-        }

-        try {

-            latch.await();

-        } catch (InterruptedException ignored) {

-        }

-        evaluateResult(title, startGcStatus, startTime);

-    }

-

-    private void evaluateResult(String title, GcStatus startStatus, long startTime) {

-        final float elapsed = SystemClock.elapsedRealtime() - startTime;

-        // Sleep a while to see if GC may happen.

-        SystemClock.sleep(DELAY_AFTER_BENCH_MS);

-        final GcStatus endStatus = getGcStatus();

-        final GcInfo info = startStatus.calculateGcTime(endStatus, title, mTestResults);

-        Log.i(TAG, mMethodName + "_" + title + " execution time: "

-                + elapsed + "ms (avg=" + String.format("%.5f", elapsed / TEST_ITERATIONS) + "ms)"

-                + " GC time: " + String.format("%.3f", info.mTotalGcTime) + "ms"

-                + " GC paused time: " + String.format("%.3f", info.mTotalGcPausedTime) + "ms");

-    }

-

-    /** Cleans the test environment. */

-    private static void performGc() {

-        System.gc();

-        System.runFinalization();

-        System.gc();

-    }

-

-    private static GcStatus getGcStatus() {

-        if (DEBUG) {

-            Log.i(TAG, "===== Read GC dump =====");

-        }

-        final GcStatus status = new GcStatus();

-        final List<String> vmDump = getVmDump();

-        Assume.assumeFalse("VM dump is empty", vmDump.isEmpty());

-        for (String line : vmDump) {

-            status.visit(line);

-            if (line.startsWith("DALVIK THREADS")) {

-                break;

-            }

-        }

-        return status;

-    }

-

-    private static List<String> getVmDump() {

-        final int myPid = Process.myPid();

-        // Another approach Debug#dumpJavaBacktraceToFileTimeout requires setenforce 0.

-        Process.sendSignal(myPid, Process.SIGNAL_QUIT);

-        // Give a chance to handle the signal.

-        SystemClock.sleep(100);

-

-        String dump = null;

-        final String pattern = myPid + " written to: ";

-        final List<String> logs = shell("logcat -v brief -d tombstoned:I *:S");

-        for (int i = logs.size() - 1; i >= 0; i--) {

-            final String log = logs.get(i);

-            // Log pattern: Traces for pid 9717 written to: /data/anr/trace_07

-            final int pos = log.indexOf(pattern);

-            if (pos > 0) {

-                dump = log.substring(pattern.length() + pos);

-                break;

-            }

-        }

-

-        Assume.assumeNotNull("Unable to find VM dump", dump);

-        // It requires system or root uid to read the trace.

-        return shell("cat " + dump);

-    }

-

-    private static List<String> shell(String command) {

-        final ParcelFileDescriptor.AutoCloseInputStream stream =

-                new ParcelFileDescriptor.AutoCloseInputStream(

-                getInstrumentation().getUiAutomation().executeShellCommand(command));

-        final ArrayList<String> lines = new ArrayList<>();

-        try (BufferedReader br = new BufferedReader(new InputStreamReader(stream))) {

-            String line;

-            while ((line = br.readLine()) != null) {

-                lines.add(line);

-            }

-        } catch (IOException e) {

-            throw new RuntimeException(e);

-        }

-        return lines;

-    }

-

-    /** An empty class which provides some methods with different type arguments. */

-    static class Task {

-        void doSomething() {

-        }

-

-        void doSomething(Task t) {

-        }

-

-        void doSomething(int taskId, long time) {

-        }

-

-        boolean doSomething(Rect bounds, boolean top, int taskId) {

-            return false;

-        }

-    }

-

-    static class ValPattern {

-        static final int TYPE_COUNT = 0;

-        static final int TYPE_TIME = 1;

-        static final String PATTERN_COUNT = "(\\d+)";

-        static final String PATTERN_TIME = "(\\d+\\.?\\d+)(\\w+)";

-        final String mRawPattern;

-        final Pattern mPattern;

-        final int mType;

-

-        int mIntValue;

-        float mFloatValue;

-

-        ValPattern(String p, int type) {

-            mRawPattern = p;

-            mPattern = Pattern.compile(

-                    p + (type == TYPE_TIME ? PATTERN_TIME : PATTERN_COUNT) + ".*");

-            mType = type;

-        }

-

-        boolean visit(String line) {

-            final Matcher matcher = mPattern.matcher(line);

-            if (!matcher.matches()) {

-                return false;

-            }

-            final String value = matcher.group(1);

-            if (value == null) {

-                return false;

-            }

-            if (mType == TYPE_COUNT) {

-                mIntValue = Integer.parseInt(value);

-                return true;

-            }

-            final float time = Float.parseFloat(value);

-            final String unit = matcher.group(2);

-            if (unit == null) {

-                return false;

-            }

-            // Refer to art/libartbase/base/time_utils.cc

-            switch (unit) {

-                case "s":

-                    mFloatValue = time * 1000;

-                    break;

-                case "ms":

-                    mFloatValue = time;

-                    break;

-                case "us":

-                    mFloatValue = time / 1000;

-                    break;

-                case "ns":

-                    mFloatValue = time / 1000 / 1000;

-                    break;

-                default:

-                    throw new IllegalArgumentException();

-            }

-

-            return true;

-        }

-

-        @Override

-        public String toString() {

-            return mRawPattern + (mType == TYPE_TIME ? (mFloatValue + "ms") : mIntValue);

-        }

-    }

-

-    /** Parses the dump pattern of Heap::DumpGcPerformanceInfo. */

-    private static class GcStatus {

-        private static final int TOTAL_GC_TIME_INDEX = 1;

-        private static final int TOTAL_GC_PAUSED_TIME_INDEX = 5;

-

-        // Refer to art/runtime/gc/heap.cc

-        final ValPattern[] mPatterns = {

-                new ValPattern("Total GC count: ", ValPattern.TYPE_COUNT),

-                new ValPattern("Total GC time: ", ValPattern.TYPE_TIME),

-                new ValPattern("Total time waiting for GC to complete: ", ValPattern.TYPE_TIME),

-                new ValPattern("Total blocking GC count: ", ValPattern.TYPE_COUNT),

-                new ValPattern("Total blocking GC time: ", ValPattern.TYPE_TIME),

-                new ValPattern("Total mutator paused time: ", ValPattern.TYPE_TIME),

-                new ValPattern("Total number of allocations ", ValPattern.TYPE_COUNT),

-                new ValPattern("concurrent copying paused:  Sum: ", ValPattern.TYPE_TIME),

-                new ValPattern("concurrent copying total time: ", ValPattern.TYPE_TIME),

-                new ValPattern("concurrent copying freed: ", ValPattern.TYPE_COUNT),

-                new ValPattern("Peak regions allocated ", ValPattern.TYPE_COUNT),

-        };

-

-        void visit(String dumpLine) {

-            for (ValPattern p : mPatterns) {

-                if (p.visit(dumpLine)) {

-                    if (DEBUG) {

-                        Log.i(TAG, "  " + p);

-                    }

-                }

-            }

-        }

-

-        GcInfo calculateGcTime(GcStatus newStatus, String title, Bundle result) {

-            Log.i(TAG, "===== GC status of " + title + " =====");

-            final GcInfo info = new GcInfo();

-            for (int i = 0; i < mPatterns.length; i++) {

-                final ValPattern p = mPatterns[i];

-                if (p.mType == ValPattern.TYPE_COUNT) {

-                    final int diff = newStatus.mPatterns[i].mIntValue - p.mIntValue;

-                    Log.i(TAG, "  " + p.mRawPattern + diff);

-                    if (diff > 0) {

-                        result.putInt("[" + title + "] " + p.mRawPattern, diff);

-                    }

-                    continue;

-                }

-                final float diff = newStatus.mPatterns[i].mFloatValue - p.mFloatValue;

-                Log.i(TAG, "  " + p.mRawPattern + diff + "ms");

-                if (diff > 0) {

-                    result.putFloat("[" + title + "] " + p.mRawPattern + "(ms)", diff);

-                }

-                if (i == TOTAL_GC_TIME_INDEX) {

-                    info.mTotalGcTime = diff;

-                } else if (i == TOTAL_GC_PAUSED_TIME_INDEX) {

-                    info.mTotalGcPausedTime = diff;

-                }

-            }

-            return info;

-        }

-    }

-

-    private static class GcInfo {

-        float mTotalGcTime;

-        float mTotalGcPausedTime;

-    }

-}

+/*
+ * Copyright (C) 2020 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.internal;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.app.Activity;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.test.filters.LargeTest;
+
+import com.android.internal.util.function.pooled.PooledConsumer;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.internal.util.function.pooled.PooledPredicate;
+
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runners.model.Statement;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Compares the performance of regular lambda and pooled lambda. */
+@LargeTest
+public class LambdaPerfTest {
+    private static final boolean DEBUG = false;
+    private static final String TAG = LambdaPerfTest.class.getSimpleName();
+
+    private static final String LAMBDA_FORM_REGULAR = "regular";
+    private static final String LAMBDA_FORM_POOLED = "pooled";
+
+    private static final int WARMUP_ITERATIONS = 1000;
+    private static final int TEST_ITERATIONS = 3000000;
+    private static final int TASK_COUNT = 10;
+    private static final long DELAY_AFTER_BENCH_MS = 1000;
+
+    private String mMethodName;
+
+    private final Bundle mTestResults = new Bundle();
+    private final ArrayList<Task> mTasks = new ArrayList<>();
+
+    // The member fields are used to ensure lambda capturing. They don't have the actual meaning.
+    private final Task mTask = new Task();
+    private final Rect mBounds = new Rect();
+    private int mTaskId;
+    private long mTime;
+    private boolean mTop;
+
+    @Rule
+    public final TestRule mRule = (base, description) -> new Statement() {
+        @Override
+        public void evaluate() throws Throwable {
+            mMethodName = description.getMethodName();
+            mTasks.clear();
+            for (int i = 0; i < TASK_COUNT; i++) {
+                final Task t = new Task();
+                mTasks.add(t);
+            }
+            base.evaluate();
+
+            getInstrumentation().sendStatus(Activity.RESULT_OK, mTestResults);
+        }
+    };
+
+    @Test
+    public void test1ParamConsumer() {
+        evaluate(LAMBDA_FORM_REGULAR, () -> forAllTask(t -> t.doSomething(mTask)));
+        evaluate(LAMBDA_FORM_POOLED, () -> {
+            final PooledConsumer c = PooledLambda.obtainConsumer(Task::doSomething,
+                    PooledLambda.__(Task.class), mTask);
+            forAllTask(c);
+            c.recycle();
+        });
+    }
+
+    @Test
+    public void test2PrimitiveParamsConsumer() {
+        // Not in Integer#IntegerCache (-128~127) for autoboxing, that may create new object.
+        mTaskId = 12345;
+        mTime = 54321;
+
+        evaluate(LAMBDA_FORM_REGULAR, () -> forAllTask(t -> t.doSomething(mTaskId, mTime)));
+        evaluate(LAMBDA_FORM_POOLED, () -> {
+            final PooledConsumer c = PooledLambda.obtainConsumer(Task::doSomething,
+                    PooledLambda.__(Task.class), mTaskId, mTime);
+            forAllTask(c);
+            c.recycle();
+        });
+    }
+
+    @Test
+    public void test3ParamsPredicate() {
+        mTop = true;
+        // In Integer#IntegerCache.
+        mTaskId = 10;
+
+        evaluate(LAMBDA_FORM_REGULAR, () -> handleTask(t -> t.doSomething(mBounds, mTop, mTaskId)));
+        evaluate(LAMBDA_FORM_POOLED, () -> {
+            final PooledPredicate c = PooledLambda.obtainPredicate(Task::doSomething,
+                    PooledLambda.__(Task.class), mBounds, mTop, mTaskId);
+            handleTask(c);
+            c.recycle();
+        });
+    }
+
+    @Test
+    public void testMessage() {
+        evaluate(LAMBDA_FORM_REGULAR, () -> {
+            final Message m = Message.obtain().setCallback(() -> mTask.doSomething(mTaskId, mTime));
+            m.getCallback().run();
+            m.recycle();
+        });
+        evaluate(LAMBDA_FORM_POOLED, () -> {
+            final Message m = PooledLambda.obtainMessage(Task::doSomething, mTask, mTaskId, mTime);
+            m.getCallback().run();
+            m.recycle();
+        });
+    }
+
+    @Test
+    public void testRunnable() {
+        evaluate(LAMBDA_FORM_REGULAR, () -> {
+            final Runnable r = mTask::doSomething;
+            r.run();
+        });
+        evaluate(LAMBDA_FORM_POOLED, () -> {
+            final Runnable r = PooledLambda.obtainRunnable(Task::doSomething, mTask).recycleOnUse();
+            r.run();
+        });
+    }
+
+    @Test
+    public void testMultiThread() {
+        final int numThread = 3;
+
+        final Runnable regularAction = () -> forAllTask(t -> t.doSomething(mTask));
+        final Runnable[] regularActions = new Runnable[numThread];
+        Arrays.fill(regularActions, regularAction);
+        evaluateMultiThread(LAMBDA_FORM_REGULAR, regularActions);
+
+        final Runnable pooledAction = () -> {
+            final PooledConsumer c = PooledLambda.obtainConsumer(Task::doSomething,
+                    PooledLambda.__(Task.class), mTask);
+            forAllTask(c);
+            c.recycle();
+        };
+        final Runnable[] pooledActions = new Runnable[numThread];
+        Arrays.fill(pooledActions, pooledAction);
+        evaluateMultiThread(LAMBDA_FORM_POOLED, pooledActions);
+    }
+
+    private void forAllTask(Consumer<Task> callback) {
+        for (int i = mTasks.size() - 1; i >= 0; i--) {
+            callback.accept(mTasks.get(i));
+        }
+    }
+
+    private void handleTask(Predicate<Task> callback) {
+        for (int i = mTasks.size() - 1; i >= 0; i--) {
+            final Task task = mTasks.get(i);
+            if (callback.test(task)) {
+                return;
+            }
+        }
+    }
+
+    private void evaluate(String title, Runnable action) {
+        for (int i = 0; i < WARMUP_ITERATIONS; i++) {
+            action.run();
+        }
+        performGc();
+
+        final GcStatus startGcStatus = getGcStatus();
+        final long startTime = SystemClock.elapsedRealtime();
+        for (int i = 0; i < TEST_ITERATIONS; i++) {
+            action.run();
+        }
+        evaluateResult(title, startGcStatus, startTime);
+    }
+
+    private void evaluateMultiThread(String title, Runnable[] actions) {
+        performGc();
+
+        final CountDownLatch latch = new CountDownLatch(actions.length);
+        final GcStatus startGcStatus = getGcStatus();
+        final long startTime = SystemClock.elapsedRealtime();
+        for (Runnable action : actions) {
+            new Thread() {
+                @Override
+                public void run() {
+                    for (int i = 0; i < TEST_ITERATIONS; i++) {
+                        action.run();
+                    }
+                    latch.countDown();
+                };
+            }.start();
+        }
+        try {
+            latch.await();
+        } catch (InterruptedException ignored) {
+        }
+        evaluateResult(title, startGcStatus, startTime);
+    }
+
+    private void evaluateResult(String title, GcStatus startStatus, long startTime) {
+        final float elapsed = SystemClock.elapsedRealtime() - startTime;
+        // Sleep a while to see if GC may happen.
+        SystemClock.sleep(DELAY_AFTER_BENCH_MS);
+        final GcStatus endStatus = getGcStatus();
+        final GcInfo info = startStatus.calculateGcTime(endStatus, title, mTestResults);
+        mTestResults.putFloat("[" + title + "-execution-time]", elapsed);
+        Log.i(TAG, mMethodName + "_" + title + " execution time: "
+                + elapsed + "ms (avg=" + String.format("%.5f", elapsed / TEST_ITERATIONS) + "ms)"
+                + " GC time: " + String.format("%.3f", info.mTotalGcTime) + "ms"
+                + " GC paused time: " + String.format("%.3f", info.mTotalGcPausedTime) + "ms");
+    }
+
+    /** Cleans the test environment. */
+    private static void performGc() {
+        System.gc();
+        System.runFinalization();
+        System.gc();
+    }
+
+    private static GcStatus getGcStatus() {
+        if (DEBUG) {
+            Log.i(TAG, "===== Read GC dump =====");
+        }
+        final GcStatus status = new GcStatus();
+        final List<String> vmDump = getVmDump();
+        Assume.assumeFalse("VM dump is empty", vmDump.isEmpty());
+        for (String line : vmDump) {
+            status.visit(line);
+            if (line.startsWith("DALVIK THREADS")) {
+                break;
+            }
+        }
+        return status;
+    }
+
+    private static List<String> getVmDump() {
+        final int myPid = Process.myPid();
+        // Another approach Debug#dumpJavaBacktraceToFileTimeout requires setenforce 0.
+        Process.sendSignal(myPid, Process.SIGNAL_QUIT);
+        // Give a chance to handle the signal.
+        SystemClock.sleep(100);
+
+        String dump = null;
+        final String pattern = myPid + " written to: ";
+        final List<String> logs = shell("logcat -v brief -d tombstoned:I *:S");
+        for (int i = logs.size() - 1; i >= 0; i--) {
+            final String log = logs.get(i);
+            // Log pattern: Traces for pid 9717 written to: /data/anr/trace_07
+            final int pos = log.indexOf(pattern);
+            if (pos > 0) {
+                dump = log.substring(pattern.length() + pos);
+                if (!dump.startsWith("/data/anr/")) {
+                    dump = "/data/anr/" + dump;
+                }
+                break;
+            }
+        }
+
+        Assume.assumeNotNull("Unable to find VM dump", dump);
+        // It requires system or root uid to read the trace.
+        return shell("cat " + dump);
+    }
+
+    private static List<String> shell(String command) {
+        final ParcelFileDescriptor.AutoCloseInputStream stream =
+                new ParcelFileDescriptor.AutoCloseInputStream(
+                getInstrumentation().getUiAutomation().executeShellCommand(command));
+        final ArrayList<String> lines = new ArrayList<>();
+        try (BufferedReader br = new BufferedReader(new InputStreamReader(stream))) {
+            String line;
+            while ((line = br.readLine()) != null) {
+                lines.add(line);
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return lines;
+    }
+
+    /** An empty class which provides some methods with different type arguments. */
+    static class Task {
+        void doSomething() {
+        }
+
+        void doSomething(Task t) {
+        }
+
+        void doSomething(int taskId, long time) {
+        }
+
+        boolean doSomething(Rect bounds, boolean top, int taskId) {
+            return false;
+        }
+    }
+
+    static class ValPattern {
+        static final int TYPE_COUNT = 0;
+        static final int TYPE_TIME = 1;
+        static final String PATTERN_COUNT = "(\\d+)";
+        static final String PATTERN_TIME = "(\\d+\\.?\\d+)(\\w+)";
+        final String mRawPattern;
+        final Pattern mPattern;
+        final int mType;
+
+        int mIntValue;
+        float mFloatValue;
+
+        ValPattern(String p, int type) {
+            mRawPattern = p;
+            mPattern = Pattern.compile(
+                    p + (type == TYPE_TIME ? PATTERN_TIME : PATTERN_COUNT) + ".*");
+            mType = type;
+        }
+
+        boolean visit(String line) {
+            final Matcher matcher = mPattern.matcher(line);
+            if (!matcher.matches()) {
+                return false;
+            }
+            final String value = matcher.group(1);
+            if (value == null) {
+                return false;
+            }
+            if (mType == TYPE_COUNT) {
+                mIntValue = Integer.parseInt(value);
+                return true;
+            }
+            final float time = Float.parseFloat(value);
+            final String unit = matcher.group(2);
+            if (unit == null) {
+                return false;
+            }
+            // Refer to art/libartbase/base/time_utils.cc
+            switch (unit) {
+                case "s":
+                    mFloatValue = time * 1000;
+                    break;
+                case "ms":
+                    mFloatValue = time;
+                    break;
+                case "us":
+                    mFloatValue = time / 1000;
+                    break;
+                case "ns":
+                    mFloatValue = time / 1000 / 1000;
+                    break;
+                default:
+                    throw new IllegalArgumentException();
+            }
+
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            return mRawPattern + (mType == TYPE_TIME ? (mFloatValue + "ms") : mIntValue);
+        }
+    }
+
+    /** Parses the dump pattern of Heap::DumpGcPerformanceInfo. */
+    private static class GcStatus {
+        private static final int TOTAL_GC_TIME_INDEX = 1;
+        private static final int TOTAL_GC_PAUSED_TIME_INDEX = 5;
+
+        // Refer to art/runtime/gc/heap.cc
+        final ValPattern[] mPatterns = {
+                new ValPattern("Total GC count: ", ValPattern.TYPE_COUNT),
+                new ValPattern("Total GC time: ", ValPattern.TYPE_TIME),
+                new ValPattern("Total time waiting for GC to complete: ", ValPattern.TYPE_TIME),
+                new ValPattern("Total blocking GC count: ", ValPattern.TYPE_COUNT),
+                new ValPattern("Total blocking GC time: ", ValPattern.TYPE_TIME),
+                new ValPattern("Total mutator paused time: ", ValPattern.TYPE_TIME),
+                new ValPattern("Total number of allocations ", ValPattern.TYPE_COUNT),
+                new ValPattern("concurrent copying paused:  Sum: ", ValPattern.TYPE_TIME),
+                new ValPattern("concurrent copying total time: ", ValPattern.TYPE_TIME),
+                new ValPattern("concurrent copying freed: ", ValPattern.TYPE_COUNT),
+                new ValPattern("Peak regions allocated ", ValPattern.TYPE_COUNT),
+        };
+
+        void visit(String dumpLine) {
+            for (ValPattern p : mPatterns) {
+                if (p.visit(dumpLine)) {
+                    if (DEBUG) {
+                        Log.i(TAG, "  " + p);
+                    }
+                }
+            }
+        }
+
+        GcInfo calculateGcTime(GcStatus newStatus, String title, Bundle result) {
+            Log.i(TAG, "===== GC status of " + title + " =====");
+            final GcInfo info = new GcInfo();
+            for (int i = 0; i < mPatterns.length; i++) {
+                final ValPattern p = mPatterns[i];
+                if (p.mType == ValPattern.TYPE_COUNT) {
+                    final int diff = newStatus.mPatterns[i].mIntValue - p.mIntValue;
+                    Log.i(TAG, "  " + p.mRawPattern + diff);
+                    if (diff > 0) {
+                        result.putInt("[" + title + "] " + p.mRawPattern, diff);
+                    }
+                    continue;
+                }
+                final float diff = newStatus.mPatterns[i].mFloatValue - p.mFloatValue;
+                Log.i(TAG, "  " + p.mRawPattern + diff + "ms");
+                if (diff > 0) {
+                    result.putFloat("[" + title + "] " + p.mRawPattern + "(ms)", diff);
+                }
+                if (i == TOTAL_GC_TIME_INDEX) {
+                    info.mTotalGcTime = diff;
+                } else if (i == TOTAL_GC_PAUSED_TIME_INDEX) {
+                    info.mTotalGcPausedTime = diff;
+                }
+            }
+            return info;
+        }
+    }
+
+    private static class GcInfo {
+        float mTotalGcTime;
+        float mTotalGcPausedTime;
+    }
+}
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 7efe3c3..7ddbe95 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -130,7 +130,7 @@
         "optimize/MultiApkGenerator.cpp",
         "optimize/ResourceDeduper.cpp",
         "optimize/ResourceFilter.cpp",
-        "optimize/ResourcePathShortener.cpp",
+        "optimize/Obfuscator.cpp",
         "optimize/VersionCollapser.cpp",
         "process/SymbolTable.cpp",
         "split/TableSplitter.cpp",
@@ -161,6 +161,7 @@
         "ApkInfo.proto",
         "Configuration.proto",
         "Resources.proto",
+        "ResourceMetadata.proto",
         "ResourcesInternal.proto",
         "ValueTransformer.cpp",
     ],
@@ -218,6 +219,7 @@
     srcs: [
         "Configuration.proto",
         "ResourcesInternal.proto",
+        "ResourceMetadata.proto",
         "Resources.proto",
     ],
     out: ["aapt2-protos.zip"],
diff --git a/tools/aapt2/ResourceMetadata.proto b/tools/aapt2/ResourceMetadata.proto
new file mode 100644
index 0000000..8eca54c
--- /dev/null
+++ b/tools/aapt2/ResourceMetadata.proto
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+syntax = "proto3";
+
+package aapt.pb;
+
+option java_package = "com.android.aapt";
+option java_multiple_files = true;
+
+message ResourceMappings {
+  ShortenedPathsMap shortened_paths = 1;
+  CollapsedNamesMap collapsed_names = 2;
+}
+
+// Metadata relating to "aapt2 optimize --shorten-resource-paths"
+message ShortenedPathsMap {
+  // Maps shorted paths (e.g. "res/foo.xml") to their original names (e.g.
+  // "res/xml/file_with_long_name.xml").
+  message ResourcePathMapping {
+    string shortened_path = 1;
+    string original_path = 2;
+  }
+  repeated ResourcePathMapping resource_paths = 1;
+}
+
+// Metadata relating to "aapt2 optimize --collapse-resource-names"
+message CollapsedNamesMap {
+  // Maps resource IDs (e.g. 0x7f123456) to their original names (e.g.
+  // "package:type/entry").
+  message ResourceNameMapping {
+    uint32 id = 1;
+    string name = 2;
+  }
+  repeated ResourceNameMapping resource_names = 1;
+}
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index e37c2d4..9feaf52 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -16,7 +16,11 @@
 
 #include "Optimize.h"
 
+#include <map>
 #include <memory>
+#include <set>
+#include <string>
+#include <utility>
 #include <vector>
 
 #include "Diagnostics.h"
@@ -38,9 +42,9 @@
 #include "io/BigBufferStream.h"
 #include "io/Util.h"
 #include "optimize/MultiApkGenerator.h"
+#include "optimize/Obfuscator.h"
 #include "optimize/ResourceDeduper.h"
 #include "optimize/ResourceFilter.h"
-#include "optimize/ResourcePathShortener.h"
 #include "optimize/VersionCollapser.h"
 #include "split/TableSplitter.h"
 #include "util/Files.h"
@@ -114,11 +118,11 @@
   }
 
  private:
-  DISALLOW_COPY_AND_ASSIGN(OptimizeContext);
-
   StdErrDiagnostics diagnostics_;
   bool verbose_ = false;
   int sdk_version_ = 0;
+
+  DISALLOW_COPY_AND_ASSIGN(OptimizeContext);
 };
 
 class Optimizer {
@@ -151,8 +155,8 @@
     }
 
     if (options_.shorten_resource_paths) {
-      ResourcePathShortener shortener(options_.table_flattener_options.shortened_path_map);
-      if (!shortener.Consume(context_, apk->GetResourceTable())) {
+      Obfuscator obfuscator(options_.table_flattener_options.shortened_path_map);
+      if (!obfuscator.Consume(context_, apk->GetResourceTable())) {
         context_->GetDiagnostics()->Error(android::DiagMessage()
                                           << "failed shortening resource paths");
         return 1;
diff --git a/tools/aapt2/optimize/ResourcePathShortener.cpp b/tools/aapt2/optimize/Obfuscator.cpp
similarity index 83%
rename from tools/aapt2/optimize/ResourcePathShortener.cpp
rename to tools/aapt2/optimize/Obfuscator.cpp
index 7ff9bf5..f704f26 100644
--- a/tools/aapt2/optimize/ResourcePathShortener.cpp
+++ b/tools/aapt2/optimize/Obfuscator.cpp
@@ -14,28 +14,25 @@
  * limitations under the License.
  */
 
-#include "optimize/ResourcePathShortener.h"
+#include "optimize/Obfuscator.h"
 
 #include <set>
+#include <string>
 #include <unordered_set>
 
-#include "androidfw/StringPiece.h"
-
 #include "ResourceTable.h"
 #include "ValueVisitor.h"
+#include "androidfw/StringPiece.h"
 #include "util/Util.h"
 
-
-static const std::string base64_chars =
-             "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-             "abcdefghijklmnopqrstuvwxyz"
-             "0123456789-_";
+static const char base64_chars[] =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    "abcdefghijklmnopqrstuvwxyz"
+    "0123456789-_";
 
 namespace aapt {
 
-ResourcePathShortener::ResourcePathShortener(
-    std::map<std::string, std::string>& path_map_out)
-    : path_map_(path_map_out) {
+Obfuscator::Obfuscator(std::map<std::string, std::string>& path_map_out) : path_map_(path_map_out) {
 }
 
 std::string ShortenFileName(const android::StringPiece& file_path, int output_length) {
@@ -50,7 +47,6 @@
   return result;
 }
 
-
 // Return the optimal hash length such that at most 10% of resources collide in
 // their shortened path.
 // Reference: http://matt.might.net/articles/counting-hash-collisions/
@@ -63,7 +59,7 @@
 }
 
 std::string GetShortenedPath(const android::StringPiece& shortened_filename,
-    const android::StringPiece& extension, int collision_count) {
+                             const android::StringPiece& extension, int collision_count) {
   std::string shortened_path = "res/" + shortened_filename.to_string();
   if (collision_count > 0) {
     shortened_path += std::to_string(collision_count);
@@ -76,12 +72,12 @@
 // underlying filepath as key rather than the integer address. This is to ensure
 // determinism of output for colliding files.
 struct PathComparator {
-    bool operator() (const FileReference* lhs, const FileReference* rhs) const {
-        return lhs->path->compare(*rhs->path);
-    }
+  bool operator()(const FileReference* lhs, const FileReference* rhs) const {
+    return lhs->path->compare(*rhs->path);
+  }
 };
 
-bool ResourcePathShortener::Consume(IAaptContext* context, ResourceTable* table) {
+bool Obfuscator::Consume(IAaptContext* context, ResourceTable* table) {
   // used to detect collisions
   std::unordered_set<std::string> shortened_paths;
   std::set<FileReference*, PathComparator> file_refs;
@@ -103,8 +99,7 @@
     util::ExtractResFilePathParts(*file_ref->path, &res_subdir, &actual_filename, &extension);
 
     // Android detects ColorStateLists via pathname, skip res/color*
-    if (util::StartsWith(res_subdir, "res/color"))
-      continue;
+    if (util::StartsWith(res_subdir, "res/color")) continue;
 
     std::string shortened_filename = ShortenFileName(*file_ref->path, num_chars);
     int collision_count = 0;
diff --git a/tools/aapt2/optimize/ResourcePathShortener.h b/tools/aapt2/optimize/Obfuscator.h
similarity index 72%
rename from tools/aapt2/optimize/ResourcePathShortener.h
rename to tools/aapt2/optimize/Obfuscator.h
index f1074ef..1ea32db 100644
--- a/tools/aapt2/optimize/ResourcePathShortener.h
+++ b/tools/aapt2/optimize/Obfuscator.h
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-#ifndef AAPT_OPTIMIZE_RESOURCEPATHSHORTENER_H
-#define AAPT_OPTIMIZE_RESOURCEPATHSHORTENER_H
+#ifndef TOOLS_AAPT2_OPTIMIZE_OBFUSCATOR_H_
+#define TOOLS_AAPT2_OPTIMIZE_OBFUSCATOR_H_
 
 #include <map>
+#include <string>
 
 #include "android-base/macros.h"
-
 #include "process/IResourceTableConsumer.h"
 
 namespace aapt {
@@ -28,17 +28,17 @@
 class ResourceTable;
 
 // Maps resources in the apk to shortened paths.
-class ResourcePathShortener : public IResourceTableConsumer {
+class Obfuscator : public IResourceTableConsumer {
  public:
-  explicit ResourcePathShortener(std::map<std::string, std::string>& path_map_out);
+  explicit Obfuscator(std::map<std::string, std::string>& path_map_out);
 
   bool Consume(IAaptContext* context, ResourceTable* table) override;
 
  private:
-  DISALLOW_COPY_AND_ASSIGN(ResourcePathShortener);
   std::map<std::string, std::string>& path_map_;
+  DISALLOW_COPY_AND_ASSIGN(Obfuscator);
 };
 
-} // namespace aapt
+}  // namespace aapt
 
-#endif  // AAPT_OPTIMIZE_RESOURCEPATHSHORTENER_H
+#endif  // TOOLS_AAPT2_OPTIMIZE_OBFUSCATOR_H_
diff --git a/tools/aapt2/optimize/ResourcePathShortener_test.cpp b/tools/aapt2/optimize/Obfuscator_test.cpp
similarity index 80%
rename from tools/aapt2/optimize/ResourcePathShortener_test.cpp
rename to tools/aapt2/optimize/Obfuscator_test.cpp
index f5a02be..a3339d4 100644
--- a/tools/aapt2/optimize/ResourcePathShortener_test.cpp
+++ b/tools/aapt2/optimize/Obfuscator_test.cpp
@@ -14,15 +14,18 @@
  * limitations under the License.
  */
 
-#include "optimize/ResourcePathShortener.h"
+#include "optimize/Obfuscator.h"
+
+#include <memory>
+#include <string>
 
 #include "ResourceTable.h"
 #include "test/Test.h"
 
 using ::aapt::test::GetValue;
+using ::testing::Eq;
 using ::testing::Not;
 using ::testing::NotNull;
-using ::testing::Eq;
 
 android::StringPiece GetExtension(android::StringPiece path) {
   auto iter = std::find(path.begin(), path.end(), '.');
@@ -30,16 +33,15 @@
 }
 
 void FillTable(aapt::test::ResourceTableBuilder& builder, int start, int end) {
-  for (int i=start; i<end; i++) {
-    builder.AddFileReference(
-        "android:drawable/xmlfile" + std::to_string(i),
-        "res/drawable/xmlfile" + std::to_string(i) + ".xml");
+  for (int i = start; i < end; i++) {
+    builder.AddFileReference("android:drawable/xmlfile" + std::to_string(i),
+                             "res/drawable/xmlfile" + std::to_string(i) + ".xml");
   }
 }
 
 namespace aapt {
 
-TEST(ResourcePathShortenerTest, FileRefPathsChangedInResourceTable) {
+TEST(ObfuscatorTest, FileRefPathsChangedInResourceTable) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
 
   std::unique_ptr<ResourceTable> table =
@@ -50,7 +52,7 @@
           .Build();
 
   std::map<std::string, std::string> path_map;
-  ASSERT_TRUE(ResourcePathShortener(path_map).Consume(context.get(), table.get()));
+  ASSERT_TRUE(Obfuscator(path_map).Consume(context.get(), table.get()));
 
   // Expect that the path map is populated
   ASSERT_THAT(path_map.find("res/drawables/xmlfile.xml"), Not(Eq(path_map.end())));
@@ -64,39 +66,36 @@
   EXPECT_THAT(path_map["res/drawables/xmlfile.xml"],
               Not(Eq(path_map["res/drawables/xmlfile2.xml"])));
 
-  FileReference* ref =
-      GetValue<FileReference>(table.get(), "android:drawable/xmlfile");
+  FileReference* ref = GetValue<FileReference>(table.get(), "android:drawable/xmlfile");
   ASSERT_THAT(ref, NotNull());
   // The map correctly points to the new location of the file
   EXPECT_THAT(path_map["res/drawables/xmlfile.xml"], Eq(*ref->path));
 
   // Strings should not be affected, only file paths
-  EXPECT_THAT(
-      *GetValue<String>(table.get(), "android:string/string")->value,
+  EXPECT_THAT(*GetValue<String>(table.get(), "android:string/string")->value,
               Eq("res/should/still/be/the/same.png"));
   EXPECT_THAT(path_map.find("res/should/still/be/the/same.png"), Eq(path_map.end()));
 }
 
-TEST(ResourcePathShortenerTest, SkipColorFileRefPaths) {
+TEST(ObfuscatorTest, SkipColorFileRefPaths) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
 
   std::unique_ptr<ResourceTable> table =
       test::ResourceTableBuilder()
           .AddFileReference("android:color/colorlist", "res/color/colorlist.xml")
-          .AddFileReference("android:color/colorlist",
-                            "res/color-mdp-v21/colorlist.xml",
+          .AddFileReference("android:color/colorlist", "res/color-mdp-v21/colorlist.xml",
                             test::ParseConfigOrDie("mdp-v21"))
           .Build();
 
   std::map<std::string, std::string> path_map;
-  ASSERT_TRUE(ResourcePathShortener(path_map).Consume(context.get(), table.get()));
+  ASSERT_TRUE(Obfuscator(path_map).Consume(context.get(), table.get()));
 
   // Expect that the path map to not contain the ColorStateList
   ASSERT_THAT(path_map.find("res/color/colorlist.xml"), Eq(path_map.end()));
   ASSERT_THAT(path_map.find("res/color-mdp-v21/colorlist.xml"), Eq(path_map.end()));
 }
 
-TEST(ResourcePathShortenerTest, KeepExtensions) {
+TEST(ObfuscatorTest, KeepExtensions) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
 
   std::string original_xml_path = "res/drawable/xmlfile.xml";
@@ -109,7 +108,7 @@
           .Build();
 
   std::map<std::string, std::string> path_map;
-  ASSERT_TRUE(ResourcePathShortener(path_map).Consume(context.get(), table.get()));
+  ASSERT_TRUE(Obfuscator(path_map).Consume(context.get(), table.get()));
 
   // Expect that the path map is populated
   ASSERT_THAT(path_map.find("res/drawable/xmlfile.xml"), Not(Eq(path_map.end())));
@@ -122,7 +121,7 @@
   EXPECT_THAT(GetExtension(path_map[original_png_path]), Eq(android::StringPiece(".png")));
 }
 
-TEST(ResourcePathShortenerTest, DeterministicallyHandleCollisions) {
+TEST(ObfuscatorTest, DeterministicallyHandleCollisions) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
 
   // 4000 resources is the limit at which the hash space is expanded to 3
@@ -135,27 +134,27 @@
   FillTable(builder1, 0, kNumResources);
   std::unique_ptr<ResourceTable> table1 = builder1.Build();
   std::map<std::string, std::string> expected_mapping;
-  ASSERT_TRUE(ResourcePathShortener(expected_mapping).Consume(context.get(), table1.get()));
+  ASSERT_TRUE(Obfuscator(expected_mapping).Consume(context.get(), table1.get()));
 
   // We are trying to ensure lack of non-determinism, it is not simple to prove
   // a negative, thus we must try the test a few times so that the test itself
   // is non-flaky. Basically create the pathmap 5 times from the same set of
   // resources but a different order of addition and then ensure they are always
   // mapped to the same short path.
-  for (int i=0; i<kNumTries; i++) {
+  for (int i = 0; i < kNumTries; i++) {
     test::ResourceTableBuilder builder2;
     // This loop adds resources to the resource table in the range of
     // [0:kNumResources).  Adding the file references in different order makes
     // non-determinism more likely to surface. Thus we add resources
     // [start_index:kNumResources) first then [0:start_index). We also use a
     // different start_index each run.
-    int start_index = (kNumResources/kNumTries)*i;
+    int start_index = (kNumResources / kNumTries) * i;
     FillTable(builder2, start_index, kNumResources);
     FillTable(builder2, 0, start_index);
     std::unique_ptr<ResourceTable> table2 = builder2.Build();
 
     std::map<std::string, std::string> actual_mapping;
-    ASSERT_TRUE(ResourcePathShortener(actual_mapping).Consume(context.get(), table2.get()));
+    ASSERT_TRUE(Obfuscator(actual_mapping).Consume(context.get(), table2.get()));
 
     for (auto& item : actual_mapping) {
       ASSERT_THAT(expected_mapping[item.first], Eq(item.second));
@@ -163,4 +162,4 @@
   }
 }
 
-}   // namespace aapt
+}  // namespace aapt