Merge "Update device-entry docs + add glossary" into tm-dev
diff --git a/Android.bp b/Android.bp
index cd110de..8ac7de9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -324,6 +324,7 @@
         "av-types-aidl-java",
         "tv_tuner_resource_manager_aidl_interface-java",
         "soundtrigger_middleware-aidl-java",
+        "modules-utils-build",
         "modules-utils-preconditions",
         "modules-utils-synchronous-result-receiver",
         "modules-utils-os",
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index f26e051..c6ba1ea 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -307,7 +307,7 @@
     private final SparseBooleanArray mTempAllowlistCache = new SparseBooleanArray();
 
     /**
-     * Mapping of UIDs to the when their temp allowlist grace period ends (in the elapsed
+     * Mapping of UIDs to when their temp allowlist grace period ends (in the elapsed
      * realtime timebase).
      */
     private final SparseLongArray mTempAllowlistGraceCache = new SparseLongArray();
@@ -815,6 +815,19 @@
                 jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
     }
 
+    private boolean hasTempAllowlistExemptionLocked(int sourceUid, int standbyBucket,
+            long nowElapsed) {
+        if (standbyBucket == RESTRICTED_INDEX || standbyBucket == NEVER_INDEX) {
+            // Don't let RESTRICTED apps get free quota from the temp allowlist.
+            // TODO: consider granting the exemption to RESTRICTED apps if the temp allowlist allows
+            // them to start FGS
+            return false;
+        }
+        final long tempAllowlistGracePeriodEndElapsed = mTempAllowlistGraceCache.get(sourceUid);
+        return mTempAllowlistCache.get(sourceUid)
+                || nowElapsed < tempAllowlistGracePeriodEndElapsed;
+    }
+
     /** @return true if the job is within expedited job quota. */
     @GuardedBy("mLock")
     public boolean isWithinEJQuotaLocked(@NonNull final JobStatus jobStatus) {
@@ -833,11 +846,8 @@
         }
 
         final long nowElapsed = sElapsedRealtimeClock.millis();
-        final long tempAllowlistGracePeriodEndElapsed =
-                mTempAllowlistGraceCache.get(jobStatus.getSourceUid());
-        final boolean hasTempAllowlistExemption = mTempAllowlistCache.get(jobStatus.getSourceUid())
-                || nowElapsed < tempAllowlistGracePeriodEndElapsed;
-        if (hasTempAllowlistExemption) {
+        if (hasTempAllowlistExemptionLocked(jobStatus.getSourceUid(),
+                jobStatus.getEffectiveStandbyBucket(), nowElapsed)) {
             return true;
         }
 
@@ -2127,10 +2137,8 @@
             final long nowElapsed = sElapsedRealtimeClock.millis();
             final int standbyBucket = JobSchedulerService.standbyBucketForPackage(mPkg.packageName,
                     mPkg.userId, nowElapsed);
-            final long tempAllowlistGracePeriodEndElapsed = mTempAllowlistGraceCache.get(mUid);
             final boolean hasTempAllowlistExemption = !mRegularJobTimer
-                    && (mTempAllowlistCache.get(mUid)
-                    || nowElapsed < tempAllowlistGracePeriodEndElapsed);
+                    && hasTempAllowlistExemptionLocked(mUid, standbyBucket, nowElapsed);
             final long topAppGracePeriodEndElapsed = mTopAppGraceCache.get(mUid);
             final boolean hasTopAppExemption = !mRegularJobTimer
                     && (mTopAppCache.get(mUid) || nowElapsed < topAppGracePeriodEndElapsed);
diff --git a/cmds/locksettings/src/com/android/commands/locksettings/LockSettingsCmd.java b/cmds/locksettings/src/com/android/commands/locksettings/LockSettingsCmd.java
index 6a4a4be..7d9260a 100644
--- a/cmds/locksettings/src/com/android/commands/locksettings/LockSettingsCmd.java
+++ b/cmds/locksettings/src/com/android/commands/locksettings/LockSettingsCmd.java
@@ -28,43 +28,13 @@
 
 public final class LockSettingsCmd extends BaseCommand {
 
-    private static final String USAGE =
-            "usage: locksettings set-pattern [--old OLD_CREDENTIAL] NEW_PATTERN\n" +
-            "       locksettings set-pin [--old OLD_CREDENTIAL] NEW_PIN\n" +
-            "       locksettings set-password [--old OLD_CREDENTIAL] NEW_PASSWORD\n" +
-            "       locksettings clear [--old OLD_CREDENTIAL]\n" +
-            "       locksettings verify [--old OLD_CREDENTIAL]\n" +
-            "       locksettings set-disabled DISABLED\n" +
-            "       locksettings get-disabled\n" +
-            "\n" +
-            "flags: \n" +
-            "       --user USER_ID: specify the user, default value is current user\n" +
-            "\n" +
-            "locksettings set-pattern: sets a pattern\n" +
-            "    A pattern is specified by a non-separated list of numbers that index the cell\n" +
-            "    on the pattern in a 1-based manner in left to right and top to bottom order,\n" +
-            "    i.e. the top-left cell is indexed with 1, whereas the bottom-right cell\n" +
-            "    is indexed with 9. Example: 1234\n" +
-            "\n" +
-            "locksettings set-pin: sets a PIN\n" +
-            "\n" +
-            "locksettings set-password: sets a password\n" +
-            "\n" +
-            "locksettings clear: clears the unlock credential\n" +
-            "\n" +
-            "locksettings verify: verifies the credential and unlocks the user\n" +
-            "\n" +
-            "locksettings set-disabled: sets whether the lock screen should be disabled\n" +
-            "\n" +
-            "locksettings get-disabled: retrieves whether the lock screen is disabled\n";
-
     public static void main(String[] args) {
         (new LockSettingsCmd()).run(args);
     }
 
     @Override
     public void onShowUsage(PrintStream out) {
-        out.println(USAGE);
+        main(new String[] { "help" });
     }
 
     @Override
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index c022bf3..9c391ab 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -562,14 +562,15 @@
     public abstract void unregisterProcessObserver(IProcessObserver processObserver);
 
     /**
-     * Checks if there is an unfinished instrumentation that targets the given uid.
+     * Gets the uid of the instrumentation source if there is an unfinished instrumentation that
+     * targets the given uid.
      *
      * @param uid The uid to be checked for
      *
-     * @return True, if there is an instrumentation whose target application uid matches the given
-     * uid, false otherwise
+     * @return the uid of the instrumentation source, if there is an instrumentation whose target
+     * application uid matches the given uid, and {@link android.os.Process#INVALID_UID} otherwise.
      */
-    public abstract boolean isUidCurrentlyInstrumented(int uid);
+    public abstract int getInstrumentationSourceUid(int uid);
 
     /** Is this a device owner app? */
     public abstract boolean isDeviceOwner(int uid);
@@ -791,10 +792,11 @@
          *
          * @param packageName The package name of the process.
          * @param uid The UID of the process.
-         * @param foregroundId The current foreground service notification ID, a negative value
-         *                     means this notification is being removed.
+         * @param foregroundId The current foreground service notification ID.
+         * @param canceling The given notification is being canceled.
          */
-        void onForegroundServiceNotificationUpdated(String packageName, int uid, int foregroundId);
+        void onForegroundServiceNotificationUpdated(String packageName, int uid, int foregroundId,
+                boolean canceling);
     }
 
     /**
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 3b843a9..852dd97 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -536,6 +536,9 @@
         // A reusable token for other purposes, e.g. content capture, translation. It shouldn't be
         // used without security checks
         public IBinder shareableActivityToken;
+        // The token of the initial TaskFragment that embedded this activity. Do not rely on it
+        // after creation because the activity could be reparented.
+        @Nullable public IBinder mInitialTaskFragmentToken;
         int ident;
         @UnsupportedAppUsage
         Intent intent;
@@ -618,7 +621,8 @@
                 PersistableBundle persistentState, List<ResultInfo> pendingResults,
                 List<ReferrerIntent> pendingNewIntents, ActivityOptions activityOptions,
                 boolean isForward, ProfilerInfo profilerInfo, ClientTransactionHandler client,
-                IBinder assistToken, IBinder shareableActivityToken, boolean launchedFromBubble) {
+                IBinder assistToken, IBinder shareableActivityToken, boolean launchedFromBubble,
+                IBinder initialTaskFragmentToken) {
             this.token = token;
             this.assistToken = assistToken;
             this.shareableActivityToken = shareableActivityToken;
@@ -639,6 +643,7 @@
                     compatInfo);
             mActivityOptions = activityOptions;
             mLaunchedFromBubble = launchedFromBubble;
+            mInitialTaskFragmentToken = initialTaskFragmentToken;
             init();
         }
 
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 638b30e..e9df50f 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -1058,10 +1058,11 @@
     }
     
     /**
-     * Sends the key events corresponding to the text to the app being
-     * instrumented.
-     * 
-     * @param text The text to be sent. 
+     * Sends the key events that result in the given text being typed into the currently focused
+     * window, and waits for it to be processed.
+     *
+     * @param text The text to be sent.
+     * @see #sendKeySync(KeyEvent)
      */
     public void sendStringSync(String text) {
         if (text == null) {
@@ -1084,11 +1085,12 @@
     }
 
     /**
-     * Send a key event to the currently focused window/view and wait for it to
-     * be processed.  Finished at some point after the recipient has returned
-     * from its event processing, though it may <em>not</em> have completely
-     * finished reacting from the event -- for example, if it needs to update
-     * its display as a result, it may still be in the process of doing that.
+     * Sends a key event to the currently focused window, and waits for it to be processed.
+     * <p>
+     * This method blocks until the recipient has finished handling the event. Note that the
+     * recipient may <em>not</em> have completely finished reacting from the event when this method
+     * returns. For example, it may still be in the process of updating its display or UI contents
+     * upon reacting to the injected event.
      *
      * @param event The event to send to the current focus.
      */
@@ -1116,34 +1118,42 @@
     }
 
     /**
-     * Sends an up and down key event sync to the currently focused window.
+     * Sends up and down key events with the given key code to the currently focused window, and
+     * waits for it to be processed.
      * 
-     * @param key The integer keycode for the event.
+     * @param keyCode The key code for the events to send.
+     * @see #sendKeySync(KeyEvent)
      */
-    public void sendKeyDownUpSync(int key) {        
-        sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, key));
-        sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, key));
-    }
-
-    /**
-     * Higher-level method for sending both the down and up key events for a
-     * particular character key code.  Equivalent to creating both KeyEvent
-     * objects by hand and calling {@link #sendKeySync}.  The event appears
-     * as if it came from keyboard 0, the built in one.
-     * 
-     * @param keyCode The key code of the character to send.
-     */
-    public void sendCharacterSync(int keyCode) {
+    public void sendKeyDownUpSync(int keyCode) {
         sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
         sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
     }
-    
+
     /**
-     * Dispatch a pointer event. Finished at some point after the recipient has
-     * returned from its event processing, though it may <em>not</em> have
-     * completely finished reacting from the event -- for example, if it needs
-     * to update its display as a result, it may still be in the process of
-     * doing that.
+     * Sends up and down key events with the given key code to the currently focused window, and
+     * waits for it to be processed.
+     * <p>
+     * Equivalent to {@link #sendKeyDownUpSync(int)}.
+     *
+     * @param keyCode The key code of the character to send.
+     * @see #sendKeySync(KeyEvent)
+     */
+    public void sendCharacterSync(int keyCode) {
+        sendKeyDownUpSync(keyCode);
+    }
+
+    /**
+     * Dispatches a pointer event into a window owned by the instrumented application, and waits for
+     * it to be processed.
+     * <p>
+     * If the motion event being injected is targeted at a window that is not owned by the
+     * instrumented application, the input injection will fail. See {@link #getUiAutomation()} for
+     * injecting events into all windows.
+     * <p>
+     * This method blocks until the recipient has finished handling the event. Note that the
+     * recipient may <em>not</em> have completely finished reacting from the event when this method
+     * returns. For example, it may still be in the process of updating its display or UI contents
+     * upon reacting to the injected event.
      * 
      * @param event A motion event describing the pointer action.  (As noted in 
      * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use 
@@ -1155,10 +1165,10 @@
             event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
         }
 
-        syncInputTransactionsAndInjectEvent(event);
+        syncInputTransactionsAndInjectEventIntoSelf(event);
     }
 
-    private void syncInputTransactionsAndInjectEvent(MotionEvent event) {
+    private void syncInputTransactionsAndInjectEventIntoSelf(MotionEvent event) {
         final boolean syncBefore = event.getAction() == MotionEvent.ACTION_DOWN
                 || event.isFromSource(InputDevice.SOURCE_MOUSE);
         final boolean syncAfter = event.getAction() == MotionEvent.ACTION_UP;
@@ -1169,8 +1179,9 @@
                         .syncInputTransactions(true /*waitForAnimations*/);
             }
 
+            // Direct the injected event into windows owned by the instrumentation target.
             InputManager.getInstance().injectInputEvent(
-                    event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT);
+                    event, InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT, Process.myUid());
 
             if (syncAfter) {
                 WindowManagerGlobal.getWindowManagerService()
@@ -1182,19 +1193,26 @@
     }
 
     /**
-     * Dispatch a trackball event. Finished at some point after the recipient has
-     * returned from its event processing, though it may <em>not</em> have
-     * completely finished reacting from the event -- for example, if it needs
-     * to update its display as a result, it may still be in the process of
-     * doing that.
-     * 
+     * Dispatches a trackball event into the currently focused window, and waits for it to be
+     * processed.
+     * <p>
+     * This method blocks until the recipient has finished handling the event. Note that the
+     * recipient may <em>not</em> have completely finished reacting from the event when this method
+     * returns. For example, it may still be in the process of updating its display or UI contents
+     * upon reacting to the injected event.
+     *
      * @param event A motion event describing the trackball action.  (As noted in 
      * {@link MotionEvent#obtain(long, long, int, float, float, int)}, be sure to use 
      * {@link SystemClock#uptimeMillis()} as the timebase.
      */
     public void sendTrackballEventSync(MotionEvent event) {
         validateNotAppThread();
-        if ((event.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) == 0) {
+        if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+            throw new IllegalArgumentException(
+                    "Cannot inject pointer events from sendTrackballEventSync()."
+                            + " Use sendPointerSync() to inject pointer events.");
+        }
+        if (!event.isFromSource(InputDevice.SOURCE_CLASS_TRACKBALL)) {
             event.setSource(InputDevice.SOURCE_TRACKBALL);
         }
         InputManager.getInstance().injectInputEvent(event,
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 88ffdec..d375a9e 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -1682,7 +1682,7 @@
     public @interface ProvisioningConfiguration {}
 
     /**
-     * A String extra holding the provisioning trigger. It could be one of
+     * An int extra holding the provisioning trigger. It could be one of
      * {@link #PROVISIONING_TRIGGER_CLOUD_ENROLLMENT}, {@link #PROVISIONING_TRIGGER_QR_CODE},
      * {@link #PROVISIONING_TRIGGER_MANAGED_ACCOUNT} or {@link
      * #PROVISIONING_TRIGGER_UNSPECIFIED}.
@@ -3298,9 +3298,9 @@
      * Activity action: Starts the device policy management role holder updater.
      *
      * <p>The activity must handle the device policy management role holder update and set the
-     * intent result to either {@link Activity#RESULT_OK} if the update was successful, {@link
-     * #RESULT_UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER_RECOVERABLE_ERROR} if it encounters a
-     * problem that may be solved by relaunching it again, {@link
+     * intent result to either {@link Activity#RESULT_OK} if the update was successful or not
+     * necessary, {@link #RESULT_UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER_RECOVERABLE_ERROR} if
+     * it encounters a problem that may be solved by relaunching it again, {@link
      * #RESULT_UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER_PROVISIONING_DISABLED} if role holder
      * provisioning is disabled, or {@link
      * #RESULT_UPDATE_DEVICE_POLICY_MANAGEMENT_ROLE_HOLDER_UNRECOVERABLE_ERROR} if it encounters
diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java
index d7e0951..076dbef 100644
--- a/core/java/android/app/servertransaction/LaunchActivityItem.java
+++ b/core/java/android/app/servertransaction/LaunchActivityItem.java
@@ -72,6 +72,7 @@
     private IBinder mAssistToken;
     private IBinder mShareableActivityToken;
     private boolean mLaunchedFromBubble;
+    private IBinder mTaskFragmentToken;
     /**
      * It is only non-null if the process is the first time to launch activity. It is only an
      * optimization for quick look up of the interface so the field is ignored for comparison.
@@ -95,7 +96,8 @@
         ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
                 mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
                 mPendingResults, mPendingNewIntents, mActivityOptions, mIsForward, mProfilerInfo,
-                client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble);
+                client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,
+                mTaskFragmentToken);
         client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
         Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
     }
@@ -119,7 +121,7 @@
             List<ReferrerIntent> pendingNewIntents, ActivityOptions activityOptions,
             boolean isForward, ProfilerInfo profilerInfo, IBinder assistToken,
             IActivityClientController activityClientController, IBinder shareableActivityToken,
-            boolean launchedFromBubble) {
+            boolean launchedFromBubble, IBinder taskFragmentToken) {
         LaunchActivityItem instance = ObjectPool.obtain(LaunchActivityItem.class);
         if (instance == null) {
             instance = new LaunchActivityItem();
@@ -128,7 +130,7 @@
                 voiceInteractor, procState, state, persistentState, pendingResults,
                 pendingNewIntents, activityOptions, isForward, profilerInfo, assistToken,
                 activityClientController, shareableActivityToken,
-                launchedFromBubble);
+                launchedFromBubble, taskFragmentToken);
 
         return instance;
     }
@@ -136,7 +138,7 @@
     @Override
     public void recycle() {
         setValues(this, null, 0, null, null, null, null, null, null, 0, null, null, null, null,
-                null, false, null, null, null, null, false);
+                null, false, null, null, null, null, false, null);
         ObjectPool.recycle(this);
     }
 
@@ -166,6 +168,7 @@
         dest.writeStrongInterface(mActivityClientController);
         dest.writeStrongBinder(mShareableActivityToken);
         dest.writeBoolean(mLaunchedFromBubble);
+        dest.writeStrongBinder(mTaskFragmentToken);
     }
 
     /** Read from Parcel. */
@@ -184,7 +187,8 @@
                 in.readStrongBinder(),
                 IActivityClientController.Stub.asInterface(in.readStrongBinder()),
                 in.readStrongBinder(),
-                in.readBoolean());
+                in.readBoolean(),
+                in.readStrongBinder());
     }
 
     public static final @NonNull Creator<LaunchActivityItem> CREATOR =
@@ -222,7 +226,8 @@
                 && mIsForward == other.mIsForward
                 && Objects.equals(mProfilerInfo, other.mProfilerInfo)
                 && Objects.equals(mAssistToken, other.mAssistToken)
-                && Objects.equals(mShareableActivityToken, other.mShareableActivityToken);
+                && Objects.equals(mShareableActivityToken, other.mShareableActivityToken)
+                && Objects.equals(mTaskFragmentToken, other.mTaskFragmentToken);
     }
 
     @Override
@@ -244,6 +249,7 @@
         result = 31 * result + Objects.hashCode(mProfilerInfo);
         result = 31 * result + Objects.hashCode(mAssistToken);
         result = 31 * result + Objects.hashCode(mShareableActivityToken);
+        result = 31 * result + Objects.hashCode(mTaskFragmentToken);
         return result;
     }
 
@@ -291,7 +297,7 @@
             List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
             ActivityOptions activityOptions, boolean isForward, ProfilerInfo profilerInfo,
             IBinder assistToken, IActivityClientController activityClientController,
-            IBinder shareableActivityToken, boolean launchedFromBubble) {
+            IBinder shareableActivityToken, boolean launchedFromBubble, IBinder taskFragmentToken) {
         instance.mIntent = intent;
         instance.mIdent = ident;
         instance.mInfo = info;
@@ -312,5 +318,6 @@
         instance.mActivityClientController = activityClientController;
         instance.mShareableActivityToken = shareableActivityToken;
         instance.mLaunchedFromBubble = launchedFromBubble;
+        instance.mTaskFragmentToken = taskFragmentToken;
     }
 }
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 5680bcd..cb55e30 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -568,7 +568,8 @@
                 }
 
                 ParseResult<Integer> targetResult = FrameworkParsingPackageUtils.computeTargetSdkVersion(
-                        targetVer, targetCode, SDK_CODENAMES, input);
+                        targetVer, targetCode, SDK_CODENAMES, input,
+                        /* allowUnknownCodenames= */ false);
                 if (targetResult.isError()) {
                     return input.error(targetResult);
                 }
diff --git a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
index a65b681..6d74b81 100644
--- a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
+++ b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
@@ -36,6 +36,7 @@
 import android.util.apk.ApkSignatureVerifier;
 
 import com.android.internal.util.ArrayUtils;
+import com.android.modules.utils.build.UnboundedSdkLevel;
 
 import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
@@ -334,8 +335,9 @@
      * If {@code targetCode} is not specified, e.g. the value is {@code null}, then the {@code
      * targetVers} will be returned unmodified.
      * <p>
-     * Otherwise, the behavior varies based on whether the current platform is a pre-release
-     * version, e.g. the {@code platformSdkCodenames} array has length > 0:
+     * When {@code allowUnknownCodenames} is false, the behavior varies based on whether the
+     * current platform is a pre-release version, e.g. the {@code platformSdkCodenames} array has
+     * length > 0:
      * <ul>
      * <li>If this is a pre-release platform and the value specified by
      * {@code targetCode} is contained within the array of allowed pre-release
@@ -343,22 +345,32 @@
      * <li>If this is a released platform, this method will return -1 to
      * indicate that the package is not compatible with this platform.
      * </ul>
+     * <p>
+     * When {@code allowUnknownCodenames} is true, any codename that is not known (presumed to be
+     * a codename announced after the build of the current device) is allowed and this method will
+     * return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}.
      *
-     * @param targetVers           targetSdkVersion number, if specified in the application
-     *                             manifest, or 0 otherwise
-     * @param targetCode           targetSdkVersion code, if specified in the application manifest,
-     *                             or {@code null} otherwise
-     * @param platformSdkCodenames array of allowed pre-release SDK codenames for this platform
+     * @param targetVers            targetSdkVersion number, if specified in the application
+     *                              manifest, or 0 otherwise
+     * @param targetCode            targetSdkVersion code, if specified in the application manifest,
+     *                              or {@code null} otherwise
+     * @param platformSdkCodenames  array of allowed pre-release SDK codenames for this platform
+     * @param allowUnknownCodenames allow unknown codenames, if true this method will accept unknown
+     *                              (presumed to be future) codenames
      * @return the targetSdkVersion to use at runtime if successful
      */
     public static ParseResult<Integer> computeTargetSdkVersion(@IntRange(from = 0) int targetVers,
             @Nullable String targetCode, @NonNull String[] platformSdkCodenames,
-            @NonNull ParseInput input) {
+            @NonNull ParseInput input, boolean allowUnknownCodenames) {
         // If it's a release SDK, return the version number unmodified.
         if (targetCode == null) {
             return input.success(targetVers);
         }
 
+        if (allowUnknownCodenames && UnboundedSdkLevel.isAtMost(targetCode)) {
+            return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT);
+        }
+
         // If it's a pre-release SDK and the codename matches this platform, it
         // definitely targets this SDK.
         if (matchTargetCode(platformSdkCodenames, targetCode)) {
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index e1ffd4a..57e84bd 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -57,10 +57,11 @@
     // Temporarily changes the pointer speed.
     void tryPointerSpeed(int speed);
 
-    // Injects an input event into the system.  To inject into windows owned by other
-    // applications, the caller must have the INJECT_EVENTS permission.
+    // Injects an input event into the system. The caller must have the INJECT_EVENTS permission.
+    // The caller can target windows owned by a certain UID by providing a valid UID, or by
+    // providing {@link android.os.Process#INVALID_UID} to target all windows.
     @UnsupportedAppUsage
-    boolean injectInputEvent(in InputEvent ev, int mode);
+    boolean injectInputEvent(in InputEvent ev, int mode, int targetUid);
 
     VerifiedInputEvent verifyInputEvent(in InputEvent ev);
 
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index c38a847..0bcabdd 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -45,6 +45,7 @@
 import android.os.InputEventInjectionSync;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
@@ -1107,14 +1108,58 @@
         }
     }
 
+    /**
+     * Injects an input event into the event system, targeting windows owned by the provided uid.
+     *
+     * If a valid targetUid is provided, the system will only consider injecting the input event
+     * into windows owned by the provided uid. If the input event is targeted at a window that is
+     * not owned by the provided uid, input injection will fail and a RemoteException will be
+     * thrown.
+     *
+     * The synchronization mode determines whether the method blocks while waiting for
+     * input injection to proceed.
+     * <p>
+     * Requires the {@link android.Manifest.permission.INJECT_EVENTS} permission.
+     * </p><p>
+     * Make sure you correctly set the event time and input source of the event
+     * before calling this method.
+     * </p>
+     *
+     * @param event The event to inject.
+     * @param mode The synchronization mode.  One of:
+     * {@link android.os.InputEventInjectionSync.NONE},
+     * {@link android.os.InputEventInjectionSync.WAIT_FOR_RESULT}, or
+     * {@link android.os.InputEventInjectionSync.WAIT_FOR_FINISHED}.
+     * @param targetUid The uid to target, or {@link android.os.Process#INVALID_UID} to target all
+     *                 windows.
+     * @return True if input event injection succeeded.
+     *
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.INJECT_EVENTS)
+    public boolean injectInputEvent(InputEvent event, int mode, int targetUid) {
+        if (event == null) {
+            throw new IllegalArgumentException("event must not be null");
+        }
+        if (mode != InputEventInjectionSync.NONE
+                && mode != InputEventInjectionSync.WAIT_FOR_FINISHED
+                && mode != InputEventInjectionSync.WAIT_FOR_RESULT) {
+            throw new IllegalArgumentException("mode is invalid");
+        }
+
+        try {
+            return mIm.injectInputEvent(event, mode, targetUid);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
 
     /**
      * Injects an input event into the event system on behalf of an application.
      * The synchronization mode determines whether the method blocks while waiting for
      * input injection to proceed.
      * <p>
-     * Requires {@link android.Manifest.permission.INJECT_EVENTS} to inject into
-     * windows that are owned by other applications.
+     * Requires the {@link android.Manifest.permission.INJECT_EVENTS} permission.
      * </p><p>
      * Make sure you correctly set the event time and input source of the event
      * before calling this method.
@@ -1129,22 +1174,10 @@
      *
      * @hide
      */
+    @RequiresPermission(Manifest.permission.INJECT_EVENTS)
     @UnsupportedAppUsage
     public boolean injectInputEvent(InputEvent event, int mode) {
-        if (event == null) {
-            throw new IllegalArgumentException("event must not be null");
-        }
-        if (mode != InputEventInjectionSync.NONE
-                && mode != InputEventInjectionSync.WAIT_FOR_FINISHED
-                && mode != InputEventInjectionSync.WAIT_FOR_RESULT) {
-            throw new IllegalArgumentException("mode is invalid");
-        }
-
-        try {
-            return mIm.injectInputEvent(event, mode);
-        } catch (RemoteException ex) {
-            throw ex.rethrowFromSystemServer();
-        }
+        return injectInputEvent(event, mode, Process.INVALID_UID);
     }
 
     /**
@@ -1421,8 +1454,11 @@
             }
 
             mInputDevices = new SparseArray<InputDevice>();
-            for (int i = 0; i < ids.length; i++) {
-                mInputDevices.put(ids[i], null);
+            // TODO(b/223905476): remove when the rootcause is fixed.
+            if (ids != null) {
+                for (int i = 0; i < ids.length; i++) {
+                    mInputDevices.put(ids[i], null);
+                }
             }
         }
     }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index c4cb319..a64e63e 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -300,6 +300,10 @@
      * When it is set by any of these owners, it prevents all users from using
      * Wi-Fi tethering. Other forms of tethering are not affected.
      *
+     * This user restriction disables only Wi-Fi tethering.
+     * Use {@link #DISALLOW_CONFIG_TETHERING} to limit all forms of tethering.
+     * When {@link #DISALLOW_CONFIG_TETHERING} is set, this user restriction becomes obsolete.
+     *
      * <p>The default value is <code>false</code>.
      *
      * <p>Key for user restrictions.
@@ -735,7 +739,7 @@
     public static final String DISALLOW_CONFIG_DATE_TIME = "no_config_date_time";
 
     /**
-     * Specifies if a user is disallowed from configuring Tethering and portable hotspots
+     * Specifies if a user is disallowed from using and configuring Tethering and portable hotspots
      * via Settings.
      *
      * <p>This restriction can only be set by a device owner, a profile owner on the primary
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 001707d..d4f8a3b 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -1038,14 +1038,14 @@
         }
         mFinished = true;
 
+        mOverlayConnection.unbind(this);
+
         if (mDreamToken == null) {
             Slog.w(mTag, "Finish was called before the dream was attached.");
             stopSelf();
             return;
         }
 
-        mOverlayConnection.unbind(this);
-
         try {
             // finishSelf will unbind the dream controller from the dream service. This will
             // trigger DreamService.this.onDestroy and DreamService.this will die.
diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java
index 01a037a..4e7b3a5 100644
--- a/core/java/android/util/NtpTrustedTime.java
+++ b/core/java/android/util/NtpTrustedTime.java
@@ -193,6 +193,16 @@
             }
             final Network network = connectivityManager.getActiveNetwork();
             final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
+
+            // This connectivity check is to avoid performing a DNS lookup for the time server on a
+            // unconnected network. There are races to obtain time in Android when connectivity
+            // changes, which means that forceRefresh() can be called by various components before
+            // the network is actually available. This led in the past to DNS lookup failures being
+            // cached (~2 seconds) thereby preventing the device successfully making an NTP request
+            // when connectivity had actually been established.
+            // A side effect of check is that tests that run a fake NTP server on the device itself
+            // will only be able to use it if the active network is connected, even though loopback
+            // addresses are actually reachable.
             if (ni == null || !ni.isConnected()) {
                 if (LOGD) Log.d(TAG, "forceRefresh: no connectivity");
                 return false;
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 5f0098c..0c4d9bf 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1584,10 +1584,10 @@
             return false;
         }
         final Configuration config = mResources.getConfiguration();
-        // TODO(b/179308296) Temporarily - never report max bounds to only Launcher if the feature
-        // is disabled.
+        // TODO(b/179308296) Temporarily exclude Launcher from being given max bounds, by checking
+        // if the caller is the recents component.
         return config != null && !config.windowConfiguration.getMaxBounds().isEmpty()
-                && (mDisplayInfo.shouldConstrainMetricsForLauncher || !isRecentsComponent());
+                && !isRecentsComponent();
     }
 
     /**
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 6917d66..9264d2e 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -306,13 +306,6 @@
     public float brightnessDefault;
 
     /**
-     * @hide
-     * True if Display#getRealSize and getRealMetrics should be constrained for Launcher, false
-     * otherwise.
-     */
-    public boolean shouldConstrainMetricsForLauncher = false;
-
-    /**
      * The {@link RoundedCorners} if present, otherwise {@code null}.
      */
     @Nullable
@@ -395,7 +388,6 @@
                 && brightnessMaximum == other.brightnessMaximum
                 && brightnessDefault == other.brightnessDefault
                 && Objects.equals(roundedCorners, other.roundedCorners)
-                && shouldConstrainMetricsForLauncher == other.shouldConstrainMetricsForLauncher
                 && installOrientation == other.installOrientation;
     }
 
@@ -447,7 +439,6 @@
         brightnessMaximum = other.brightnessMaximum;
         brightnessDefault = other.brightnessDefault;
         roundedCorners = other.roundedCorners;
-        shouldConstrainMetricsForLauncher = other.shouldConstrainMetricsForLauncher;
         installOrientation = other.installOrientation;
     }
 
@@ -505,7 +496,6 @@
         for (int i = 0; i < numUserDisabledFormats; i++) {
             userDisabledHdrTypes[i] = source.readInt();
         }
-        shouldConstrainMetricsForLauncher = source.readBoolean();
         installOrientation = source.readInt();
     }
 
@@ -561,7 +551,6 @@
         for (int i = 0; i < userDisabledHdrTypes.length; i++) {
             dest.writeInt(userDisabledHdrTypes[i]);
         }
-        dest.writeBoolean(shouldConstrainMetricsForLauncher);
         dest.writeInt(installOrientation);
     }
 
@@ -817,8 +806,6 @@
         sb.append(brightnessMaximum);
         sb.append(", brightnessDefault ");
         sb.append(brightnessDefault);
-        sb.append(", shouldConstrainMetricsForLauncher ");
-        sb.append(shouldConstrainMetricsForLauncher);
         sb.append(", installOrientation ");
         sb.append(Surface.rotationToString(installOrientation));
         sb.append("}");
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 88089b5..d4a8a16 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -167,16 +167,6 @@
     public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
             = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
 
-    /**
-     * Boolean extra added to "unbundled Sharesheet" delegation intents to signal whether the app
-     * prediction service is available. Our query of the service <em>availability</em> depends on
-     * privileges that are only available in the system, even though the service itself would then
-     * be available to the unbundled component. For now, we just include the query result as part of
-     * the handover intent.
-     * TODO: investigate whether the privileged query is necessary to determine the availability.
-     */
-    public static final String EXTRA_IS_APP_PREDICTION_SERVICE_AVAILABLE =
-            "com.android.internal.app.ChooserActivity.EXTRA_IS_APP_PREDICTION_SERVICE_AVAILABLE";
 
     /**
      * Transition name for the first image preview.
@@ -985,6 +975,12 @@
     }
 
     @Override
+    protected void onResume() {
+        super.onResume();
+        Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
+    }
+
+    @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         ViewPager viewPager = findViewById(R.id.profile_pager);
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index ea8589b..bfd8ff9 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -157,8 +157,6 @@
     /** See {@link #setRetainInOnStop}. */
     private boolean mRetainInOnStop;
 
-    protected static final int REQUEST_CODE_RETURN_FROM_DELEGATE_CHOOSER = 20;
-
     private static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
     private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
     private static final String OPEN_LINKS_COMPONENT_KEY = "app_link_state";
@@ -1374,18 +1372,6 @@
                 .write();
     }
 
-    @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent data) {
-        switch (requestCode) {
-            case REQUEST_CODE_RETURN_FROM_DELEGATE_CHOOSER:
-                // Repeat the delegate's result as our own.
-                setResult(resultCode, data);
-                finish();
-                break;
-            default:
-                super.onActivityResult(requestCode, resultCode, data);
-        }
-    }
 
     public void onActivityStarted(TargetInfo cti) {
         // Do nothing
diff --git a/core/java/com/android/server/SystemConfig.java b/core/java/com/android/server/SystemConfig.java
index db41d33..1feb5d4 100644
--- a/core/java/com/android/server/SystemConfig.java
+++ b/core/java/com/android/server/SystemConfig.java
@@ -88,8 +88,8 @@
     private static final int ALLOW_HIDDENAPI_WHITELISTING = 0x040;
     private static final int ALLOW_ASSOCIATIONS = 0x080;
     // ALLOW_OVERRIDE_APP_RESTRICTIONS allows to use "allow-in-power-save-except-idle",
-    // "allow-in-power-save", "allow-in-data-usage-save", "allow-unthrottled-location",
-    // and "allow-ignore-location-settings".
+    // "allow-in-power-save", "allow-in-data-usage-save","allow-unthrottled-location",
+    // "allow-ignore-location-settings" and "allow-adas-location-settings".
     private static final int ALLOW_OVERRIDE_APP_RESTRICTIONS = 0x100;
     private static final int ALLOW_IMPLICIT_BROADCASTS = 0x200;
     private static final int ALLOW_VENDOR_APEX = 0x400;
@@ -234,6 +234,10 @@
     // without throttling, as read from the configuration files.
     final ArraySet<String> mAllowUnthrottledLocation = new ArraySet<>();
 
+    // These are the packages that are allow-listed to be able to retrieve location when
+    // the location state is driver assistance only.
+    final ArrayMap<String, ArraySet<String>> mAllowAdasSettings = new ArrayMap<>();
+
     // These are the packages that are white-listed to be able to retrieve location even when user
     // location settings are off, for emergency purposes, as read from the configuration files.
     final ArrayMap<String, ArraySet<String>> mAllowIgnoreLocationSettings = new ArrayMap<>();
@@ -394,6 +398,10 @@
         return mAllowUnthrottledLocation;
     }
 
+    public ArrayMap<String, ArraySet<String>> getAllowAdasLocationSettings() {
+        return mAllowAdasSettings;
+    }
+
     public ArrayMap<String, ArraySet<String>> getAllowIgnoreLocationSettings() {
         return mAllowIgnoreLocationSettings;
     }
@@ -1007,6 +1015,34 @@
                         }
                         XmlUtils.skipCurrentTag(parser);
                     } break;
+                    case "allow-adas-location-settings" : {
+                        if (allowOverrideAppRestrictions) {
+                            String pkgname = parser.getAttributeValue(null, "package");
+                            String attributionTag = parser.getAttributeValue(null,
+                                    "attributionTag");
+                            if (pkgname == null) {
+                                Slog.w(TAG, "<" + name + "> without package in "
+                                        + permFile + " at " + parser.getPositionDescription());
+                            } else {
+                                ArraySet<String> tags = mAllowAdasSettings.get(pkgname);
+                                if (tags == null || !tags.isEmpty()) {
+                                    if (tags == null) {
+                                        tags = new ArraySet<>(1);
+                                        mAllowAdasSettings.put(pkgname, tags);
+                                    }
+                                    if (!"*".equals(attributionTag)) {
+                                        if ("null".equals(attributionTag)) {
+                                            attributionTag = null;
+                                        }
+                                        tags.add(attributionTag);
+                                    }
+                                }
+                            }
+                        } else {
+                            logNotAllowedInPartition(name, permFile, parser);
+                        }
+                        XmlUtils.skipCurrentTag(parser);
+                    } break;
                     case "allow-ignore-location-settings": {
                         if (allowOverrideAppRestrictions) {
                             String pkgname = parser.getAttributeValue(null, "package");
diff --git a/core/proto/android/os/appbackgroundrestrictioninfo.proto b/core/proto/android/os/appbackgroundrestrictioninfo.proto
new file mode 100644
index 0000000..8445641
--- /dev/null
+++ b/core/proto/android/os/appbackgroundrestrictioninfo.proto
@@ -0,0 +1,199 @@
+/*
+ * 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 = "proto2";
+package android.os;
+
+option java_multiple_files = true;
+
+// This message is used for statsd logging and should be kept in sync with
+// frameworks/proto_logging/stats/atoms.proto
+/**
+ * Logs information about app background restrictions.
+ *
+ * Logged from:
+ *      frameworks/base/services/core/java/com/android/server/am/AppRestrictionController.java
+ */
+message AppBackgroundRestrictionsInfo {
+    // the uid of the app.
+    optional int32 uid = 1;
+
+    enum RestrictionLevel {
+        LEVEL_UNKNOWN = 0;
+        LEVEL_UNRESTRICTED = 1;
+        LEVEL_EXEMPTED = 2;
+        LEVEL_ADAPTIVE_BUCKET = 3;
+        LEVEL_RESTRICTED_BUCKET = 4;
+        LEVEL_BACKGROUND_RESTRICTED = 5;
+        LEVEL_HIBERNATION = 6;
+    }
+    // indicates the app background restriction level.
+    optional RestrictionLevel restriction_level = 2;
+
+    enum Threshold {
+        THRESHOLD_UNKNOWN = 0;
+        THRESHOLD_RESTRICTED = 1; // app was background restricted by the system.
+        THRESHOLD_USER = 2; // app was background restricted by user action.
+    }
+    // indicates which threshold caused the app to be put into bg restriction.
+    optional Threshold threshold = 3;
+
+    enum StateTracker {
+        UNKNOWN_TRACKER = 0;
+        BATTERY_TRACKER = 1;
+        BATTERY_EXEMPTION_TRACKER = 2;
+        FGS_TRACKER = 3;
+        MEDIA_SESSION_TRACKER = 4;
+        PERMISSION_TRACKER = 5;
+        BROADCAST_EVENTS_TRACKER = 6;
+        BIND_SERVICE_EVENTS_TRACKER = 7;
+    }
+    // indicates the reason/tracker which caused the app to hit the threshold.
+    optional StateTracker tracker = 4;
+
+    message FgsTrackerInfo {
+        // indicates whether an fgs notification was visible for this app or not.
+        optional bool fgs_notification_visible = 1;
+        // total FGS duration for this app.
+        optional int64 fgs_duration = 2;
+    }
+    optional FgsTrackerInfo fgs_tracker_info = 5;
+
+    message BatteryTrackerInfo {
+        // total battery usage within last 24h (percentage)
+        optional int32 battery_24h = 1;
+        // background battery usage (percentage)
+        optional int32 battery_usage_background = 2;
+        // FGS battery usage (percentage)
+        optional int32 battery_usage_fgs = 3;
+    }
+    optional BatteryTrackerInfo battery_tracker_info = 6;
+
+    message BroadcastEventsTrackerInfo {
+        // the number of broadcasts sent by this app.
+        optional int32 broadcasts_sent = 1;
+    }
+    optional BroadcastEventsTrackerInfo broadcast_events_tracker_info = 7;
+
+    message BindServiceEventsTrackerInfo {
+        // the number of bind service requests by this app.
+        optional int32 bind_service_requests = 1;
+    }
+    optional BindServiceEventsTrackerInfo bind_service_events_tracker_info =
+        8;
+
+    // The reasons listed below are defined in PowerExemptionManager.java
+    enum ExemptionReason {
+        // range 0-9 is reserved for default reasons
+        REASON_UNKNOWN = 0;
+        REASON_DENIED = 1;
+        REASON_OTHER = 2;
+        // range 10-49 is reserved for BG-FGS-launch allowed proc states
+        REASON_PROC_STATE_PERSISTENT = 10;
+        REASON_PROC_STATE_PERSISTENT_UI = 11;
+        REASON_PROC_STATE_TOP = 12;
+        REASON_PROC_STATE_BTOP = 13;
+        REASON_PROC_STATE_FGS = 14;
+        REASON_PROC_STATE_BFGS = 15;
+        // range 50-99 is reserved for BG-FGS-launch allowed reasons
+        REASON_UID_VISIBLE = 50;
+        REASON_SYSTEM_UID = 51;
+        REASON_ACTIVITY_STARTER = 52;
+        REASON_START_ACTIVITY_FLAG = 53;
+        REASON_FGS_BINDING = 54;
+        REASON_DEVICE_OWNER = 55;
+        REASON_PROFILE_OWNER = 56;
+        REASON_COMPANION_DEVICE_MANAGER = 57;
+        REASON_BACKGROUND_ACTIVITY_PERMISSION = 58;
+        REASON_BACKGROUND_FGS_PERMISSION = 59;
+        REASON_INSTR_BACKGROUND_ACTIVITY_PERMISSION = 60;
+        REASON_INSTR_BACKGROUND_FGS_PERMISSION = 61;
+        REASON_SYSTEM_ALERT_WINDOW_PERMISSION = 62;
+        REASON_DEVICE_DEMO_MODE = 63;
+        REASON_ALLOWLISTED_PACKAGE = 65;
+        REASON_APPOP = 66;
+        REASON_ACTIVITY_VISIBILITY_GRACE_PERIOD = 67;
+        REASON_OP_ACTIVATE_VPN = 68;
+        REASON_OP_ACTIVATE_PLATFORM_VPN = 69;
+        REASON_TEMP_ALLOWED_WHILE_IN_USE = 70;
+        REASON_CURRENT_INPUT_METHOD = 71;
+        // range 100-199 is reserved for public reasons
+        REASON_GEOFENCING = 100;
+        REASON_PUSH_MESSAGING = 101;
+        REASON_PUSH_MESSAGING_OVER_QUOTA = 102;
+        REASON_ACTIVITY_RECOGNITION = 103;
+        REASON_ACCOUNT_TRANSFER = 104;
+        // range 200-299 is reserved for broadcast actions
+        REASON_BOOT_COMPLETED = 200;
+        REASON_PRE_BOOT_COMPLETED = 201;
+        REASON_LOCKED_BOOT_COMPLETED = 202;
+        REASON_BLUETOOTH_BROADCAST = 203;
+        REASON_TIMEZONE_CHANGED = 204;
+        REASON_TIME_CHANGED = 205;
+        REASON_LOCALE_CHANGED = 206;
+        REASON_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED = 207;
+        REASON_REFRESH_SAFETY_SOURCES = 208;
+        // range 300-399 is reserved for other internal reasons
+        REASON_SYSTEM_ALLOW_LISTED = 300;
+        REASON_ALARM_MANAGER_ALARM_CLOCK = 301;
+        REASON_ALARM_MANAGER_WHILE_IDLE = 302;
+        REASON_SERVICE_LAUNCH = 303;
+        REASON_KEY_CHAIN = 304;
+        REASON_PACKAGE_VERIFIER = 305;
+        REASON_SYNC_MANAGER = 306;
+        REASON_DOMAIN_VERIFICATION_V1 = 307;
+        REASON_DOMAIN_VERIFICATION_V2 = 308;
+        REASON_VPN = 309;
+        REASON_NOTIFICATION_SERVICE = 310;
+        REASON_PACKAGE_REPLACED = 311;
+        REASON_LOCATION_PROVIDER = 312;
+        REASON_MEDIA_BUTTON = 313;
+        REASON_EVENT_SMS = 314;
+        REASON_EVENT_MMS = 315;
+        REASON_SHELL = 316;
+        REASON_MEDIA_SESSION_CALLBACK = 317;
+        REASON_ROLE_DIALER = 318;
+        REASON_ROLE_EMERGENCY = 319;
+        REASON_SYSTEM_MODULE = 320;
+        REASON_CARRIER_PRIVILEGED_APP = 321;
+        // app requested to be exempt
+        REASON_OPT_OUT_REQUESTED = 1000;
+    }
+    // indicates if the app is exempt from background restrictions and the reason if applicable.
+    optional ExemptionReason exemption_reason = 9;
+
+    enum OptimizationLevel {
+        UNKNOWN = 0;
+        OPTIMIZED = 1;
+        BACKGROUND_RESTRICTED = 2;
+        NOT_OPTIMIZED = 3;
+    }
+    // the user choice for the optimization level of the app.
+    optional OptimizationLevel opt_level = 10;
+
+    enum TargetSdk {
+        SDK_UNKNOWN = 0;
+        SDK_PRE_S = 1;
+        SDK_S = 2;
+        SDK_T = 3;
+    }
+    // indicates the target sdk level for this app.
+    optional TargetSdk target_sdk = 11;
+
+    // indicates if the current device is a low ram device.
+    optional bool low_mem_device = 12;
+}
+
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 7069e716..dd69fa0 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2303,6 +2303,7 @@
   <java-symbol type="drawable" name="scrubber_control_disabled_holo" />
   <java-symbol type="drawable" name="scrubber_control_selector_holo" />
   <java-symbol type="drawable" name="scrubber_progress_horizontal_holo_dark" />
+  <java-symbol type="drawable" name="progress_small_material" />
   <java-symbol type="string" name="chooseUsbActivity" />
   <java-symbol type="string" name="ext_media_badremoval_notification_message" />
   <java-symbol type="string" name="ext_media_badremoval_notification_title" />
diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
index 3e261a7..50639be 100644
--- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java
@@ -157,7 +157,7 @@
                 .setPendingResults(resultInfoList()).setPendingNewIntents(referrerIntentList())
                 .setIsForward(true).setAssistToken(assistToken)
                 .setShareableActivityToken(shareableActivityToken)
-                .build();
+                .setTaskFragmentToken(new Binder()).build();
 
         LaunchActivityItem emptyItem = new LaunchActivityItemBuilder().build();
         LaunchActivityItem item = itemSupplier.get();
diff --git a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
index 1467fed..26d9628 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java
@@ -110,6 +110,7 @@
         private IBinder mAssistToken;
         private IBinder mShareableActivityToken;
         private boolean mLaunchedFromBubble;
+        private IBinder mTaskFragmentToken;
 
         LaunchActivityItemBuilder setIntent(Intent intent) {
             mIntent = intent;
@@ -206,13 +207,18 @@
             return this;
         }
 
+        LaunchActivityItemBuilder setTaskFragmentToken(IBinder taskFragmentToken) {
+            mTaskFragmentToken = taskFragmentToken;
+            return this;
+        }
+
         LaunchActivityItem build() {
             return LaunchActivityItem.obtain(mIntent, mIdent, mInfo,
                     mCurConfig, mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor,
                     mProcState, mState, mPersistentState, mPendingResults, mPendingNewIntents,
                     mActivityOptions, mIsForward, mProfilerInfo, mAssistToken,
                     null /* activityClientController */, mShareableActivityToken,
-                    mLaunchedFromBubble);
+                    mLaunchedFromBubble, mTaskFragmentToken);
         }
     }
 }
diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
index beadc446..8276d10 100644
--- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
+++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java
@@ -203,6 +203,7 @@
                 .setPendingResults(resultInfoList()).setActivityOptions(ActivityOptions.makeBasic())
                 .setPendingNewIntents(referrerIntentList()).setIsForward(true)
                 .setAssistToken(new Binder()).setShareableActivityToken(new Binder())
+                .setTaskFragmentToken(new Binder())
                 .build();
 
         writeAndPrepareForReading(item);
diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
index b006a16..8d3751e 100644
--- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
+++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java
@@ -345,7 +345,7 @@
                     null /* pendingResults */, null /* pendingNewIntents */,
                     null /* activityOptions */, true /* isForward */, null /* profilerInfo */,
                     mThread /* client */, null /* asssitToken */, null /* shareableActivityToken */,
-                    false /* launchedFromBubble */);
+                    false /* launchedFromBubble */, null /* taskfragmentToken */);
         }
 
         @Override
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 449fb02..58a2073 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -273,6 +273,7 @@
         <permission name="android.permission.LOCAL_MAC_ADDRESS"/>
         <permission name="android.permission.MANAGE_ACCESSIBILITY"/>
         <permission name="android.permission.MANAGE_DEVICE_ADMINS"/>
+        <permission name="android.permission.ACCESS_FPS_COUNTER"/>
         <permission name="android.permission.MANAGE_GAME_MODE"/>
         <permission name="android.permission.MANAGE_GAME_ACTIVITY" />
         <permission name="android.permission.MANAGE_LOW_POWER_STANDBY" />
diff --git a/framework-jarjar-rules.txt b/framework-jarjar-rules.txt
index be21f4e..03b268d 100644
--- a/framework-jarjar-rules.txt
+++ b/framework-jarjar-rules.txt
@@ -5,3 +5,6 @@
 # Framework-specific renames.
 rule android.net.wifi.WifiAnnotations* android.internal.wifi.WifiAnnotations@1
 rule com.android.server.vcn.util.** com.android.server.vcn.repackaged.util.@1
+
+# for modules-utils-build dependency
+rule com.android.modules.utils.build.** android.internal.modules.utils.build.@1
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index c76fa96..01f5feb 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -615,14 +615,22 @@
         return null;
     }
 
+    private void updateCallbackIfNecessary() {
+        updateCallbackIfNecessary(true /* deferCallbackUntilAllActivitiesCreated */);
+    }
+
     /**
      * Notifies listeners about changes to split states if necessary.
+     *
+     * @param deferCallbackUntilAllActivitiesCreated boolean to indicate whether the split info
+     *                                               callback should be deferred until all the
+     *                                               organized activities have been created.
      */
-    private void updateCallbackIfNecessary() {
+    private void updateCallbackIfNecessary(boolean deferCallbackUntilAllActivitiesCreated) {
         if (mEmbeddingCallback == null) {
             return;
         }
-        if (!allActivitiesCreated()) {
+        if (deferCallbackUntilAllActivitiesCreated && !allActivitiesCreated()) {
             return;
         }
         List<SplitInfo> currentSplitStates = getActiveSplitStates();
@@ -838,6 +846,36 @@
     private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {
 
         @Override
+        public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
+            final IBinder activityToken = activity.getActivityToken();
+            final IBinder initialTaskFragmentToken = ActivityThread.currentActivityThread()
+                    .getActivityClient(activityToken).mInitialTaskFragmentToken;
+            // If the activity is not embedded, then it will not have an initial task fragment token
+            // so no further action is needed.
+            if (initialTaskFragmentToken == null) {
+                return;
+            }
+            for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
+                final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
+                        .mContainers;
+                for (int j = containers.size() - 1; j >= 0; j--) {
+                    final TaskFragmentContainer container = containers.get(j);
+                    if (!container.hasActivity(activityToken)
+                            && container.getTaskFragmentToken().equals(initialTaskFragmentToken)) {
+                        // The onTaskFragmentInfoChanged callback containing this activity has not
+                        // reached the client yet, so add the activity to the pending appeared
+                        // activities and send a split info callback to the client before
+                        // {@link Activity#onCreate} is called.
+                        container.addPendingAppearedActivity(activity);
+                        updateCallbackIfNecessary(
+                                false /* deferCallbackUntilAllActivitiesCreated */);
+                        return;
+                    }
+                }
+            }
+        }
+
+        @Override
         public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) {
             // Calling after Activity#onCreate is complete to allow the app launch something
             // first. In case of a configured placeholder activity we want to make sure
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index e49af41..9a12669 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -120,7 +120,7 @@
     }
 
     ActivityStack toActivityStack() {
-        return new ActivityStack(collectActivities(), mInfo.getRunningActivityCount() == 0);
+        return new ActivityStack(collectActivities(), isEmpty());
     }
 
     void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) {
diff --git a/libs/WindowManager/Jetpack/tests/OWNERS b/libs/WindowManager/Jetpack/tests/OWNERS
index f2c3388..ac522b2 100644
--- a/libs/WindowManager/Jetpack/tests/OWNERS
+++ b/libs/WindowManager/Jetpack/tests/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 909476
+# Bug component: 1157642
 # includes OWNERS from parent directories
 charlesccchen@google.com
 diegovela@google.com
diff --git a/libs/WindowManager/Shell/res/drawable/home_icon.xml b/libs/WindowManager/Shell/res/drawable/home_icon.xml
new file mode 100644
index 0000000..1669d01
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/home_icon.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+    android:gravity="center">
+    <item android:gravity="center">
+        <shape
+            android:shape="oval">
+            <stroke
+                android:color="@color/tv_pip_edu_text_home_icon"
+                android:width="1sp" />
+            <solid android:color="@android:color/transparent" />
+            <size
+                android:width="@dimen/pip_menu_edu_text_home_icon_outline"
+                android:height="@dimen/pip_menu_edu_text_home_icon_outline"/>
+        </shape>
+    </item>
+    <item
+        android:width="@dimen/pip_menu_edu_text_home_icon"
+        android:height="@dimen/pip_menu_edu_text_home_icon"
+        android:gravity="center">
+        <vector
+            android:width="24sp"
+            android:height="24sp"
+            android:viewportWidth="24"
+            android:viewportHeight="24">
+            <path
+                android:fillColor="@color/tv_pip_edu_text_home_icon"
+                android:pathData="M12,3L4,9v12h5v-7h6v7h5V9z"/>
+        </vector>
+    </item>
+</layer-list>
diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_background.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_background.xml
new file mode 100644
index 0000000..0c62792
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_background.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/pip_menu_background_corner_radius" />
+    <solid android:color="@color/tv_pip_menu_background"/>
+    <stroke android:width="@dimen/pip_menu_border_width"
+            android:color="@color/tv_pip_menu_background"/>
+</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml
index 9bc0311..846fdb3 100644
--- a/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml
+++ b/libs/WindowManager/Shell/res/drawable/tv_pip_menu_border.xml
@@ -14,9 +14,20 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <corners android:radius="@dimen/pip_menu_border_radius" />
-    <stroke android:width="@dimen/pip_menu_border_width"
-            android:color="@color/tv_pip_menu_focus_border" />
-</shape>
\ No newline at end of file
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:exitFadeDuration="@integer/pip_menu_fade_animation_duration">
+    <item android:state_activated="true">
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/pip_menu_border_corner_radius" />
+            <stroke android:width="@dimen/pip_menu_border_width"
+                    android:color="@color/tv_pip_menu_focus_border" />
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/pip_menu_border_corner_radius" />
+            <stroke android:width="@dimen/pip_menu_border_width"
+                    android:color="@color/tv_pip_menu_background"/>
+        </shape>
+    </item>
+</selector>
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
index b826d03..dbd5a9b 100644
--- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml
@@ -16,26 +16,41 @@
 -->
 <!-- Layout for TvPipMenuView -->
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             android:id="@+id/tv_pip_menu"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent">
+            android:id="@+id/tv_pip_menu"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="center|top">
+
+    <View
+        android:id="@+id/tv_pip"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="@dimen/pip_menu_outer_space"
+        android:layout_marginStart="@dimen/pip_menu_outer_space"
+        android:layout_marginEnd="@dimen/pip_menu_outer_space"/>
 
     <ScrollView
         android:id="@+id/tv_pip_menu_scroll"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:gravity="center_horizontal"
+        android:layout_alignTop="@+id/tv_pip"
+        android:layout_alignStart="@+id/tv_pip"
+        android:layout_alignEnd="@+id/tv_pip"
+        android:layout_alignBottom="@+id/tv_pip"
         android:scrollbars="none"
-        android:layout_margin="@dimen/pip_menu_outer_space"
         android:visibility="gone"/>
 
     <HorizontalScrollView
         android:id="@+id/tv_pip_menu_horizontal_scroll"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:layout_alignTop="@+id/tv_pip"
+        android:layout_alignStart="@+id/tv_pip"
+        android:layout_alignEnd="@+id/tv_pip"
+        android:layout_alignBottom="@+id/tv_pip"
         android:gravity="center_vertical"
-        android:scrollbars="none"
-        android:layout_margin="@dimen/pip_menu_outer_space">
+        android:scrollbars="none">
 
         <LinearLayout
             android:id="@+id/tv_pip_menu_action_buttons"
@@ -89,10 +104,44 @@
     </HorizontalScrollView>
 
     <View
+        android:id="@+id/tv_pip_border"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="@dimen/pip_menu_outer_space_frame"
+        android:layout_marginStart="@dimen/pip_menu_outer_space_frame"
+        android:layout_marginEnd="@dimen/pip_menu_outer_space_frame"
+        android:background="@drawable/tv_pip_menu_border"/>
+
+    <FrameLayout
+        android:id="@+id/tv_pip_menu_edu_text_container"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_below="@+id/tv_pip"
+        android:layout_alignBottom="@+id/tv_pip_menu_frame"
+        android:layout_alignStart="@+id/tv_pip"
+        android:layout_alignEnd="@+id/tv_pip"
+        android:background="@color/tv_pip_menu_background"
+        android:clipChildren="true">
+
+        <TextView
+            android:id="@+id/tv_pip_menu_edu_text"
+            android:layout_width="wrap_content"
+            android:layout_height="@dimen/pip_menu_edu_text_view_height"
+            android:layout_gravity="bottom|center"
+            android:gravity="center"
+            android:paddingBottom="@dimen/pip_menu_border_width"
+            android:text="@string/pip_edu_text"
+            android:singleLine="true"
+            android:ellipsize="marquee"
+            android:marqueeRepeatLimit="1"
+            android:scrollHorizontally="true"
+            android:textAppearance="@style/TvPipEduText"/>
+    </FrameLayout>
+
+    <View
         android:id="@+id/tv_pip_menu_frame"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:alpha="0"
         android:layout_margin="@dimen/pip_menu_outer_space_frame"
         android:background="@drawable/tv_pip_menu_border"/>
 
diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml
new file mode 100644
index 0000000..5af4020
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+-->
+<!-- Layout for the back surface of the PiP menu -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="@dimen/pip_menu_outer_space_frame"
+        android:background="@drawable/tv_pip_menu_background"
+        android:elevation="@dimen/pip_menu_elevation"/>
+</FrameLayout>
+
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
index 558ec51..776b18e 100644
--- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
+++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
@@ -22,7 +22,12 @@
     <dimen name="pip_menu_button_margin">4dp</dimen>
     <dimen name="pip_menu_button_wrapper_margin">26dp</dimen>
     <dimen name="pip_menu_border_width">4dp</dimen>
-    <dimen name="pip_menu_border_radius">4dp</dimen>
+    <integer name="pip_menu_fade_animation_duration">500</integer>
+    <!-- The pip menu front border corner radius is 2dp smaller than
+        the background corner radius to hide the background from
+        showing through. -->
+    <dimen name="pip_menu_border_corner_radius">4dp</dimen>
+    <dimen name="pip_menu_background_corner_radius">6dp</dimen>
     <dimen name="pip_menu_outer_space">24dp</dimen>
 
     <!-- outer space minus border width -->
@@ -30,5 +35,14 @@
 
     <dimen name="pip_menu_arrow_size">24dp</dimen>
     <dimen name="pip_menu_arrow_elevation">5dp</dimen>
+
+    <dimen name="pip_menu_elevation">1dp</dimen>
+
+    <dimen name="pip_menu_edu_text_view_height">24dp</dimen>
+    <dimen name="pip_menu_edu_text_home_icon">9sp</dimen>
+    <dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen>
+    <integer name="pip_edu_text_show_duration_ms">10500</integer>
+    <integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer>
+    <integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer>
 </resources>
 
diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml
index 64b146e..fa90fe3 100644
--- a/libs/WindowManager/Shell/res/values/colors_tv.xml
+++ b/libs/WindowManager/Shell/res/values/colors_tv.xml
@@ -23,4 +23,8 @@
     <color name="tv_pip_menu_icon_bg_focused">#E8EAED</color>
     <color name="tv_pip_menu_icon_bg_unfocused">#990E0E0F</color>
     <color name="tv_pip_menu_focus_border">#E8EAED</color>
-</resources>
\ No newline at end of file
+    <color name="tv_pip_menu_background">#1E232C</color>
+
+    <color name="tv_pip_edu_text">#99D2E3FC</color>
+    <color name="tv_pip_edu_text_home_icon">#D2E3FC</color>
+</resources>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 7733201..19f7c3e 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -47,4 +47,13 @@
         <item name="android:layout_width">96dp</item>
         <item name="android:layout_height">48dp</item>
     </style>
+
+    <style name="TvPipEduText">
+        <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
+        <item name="android:textAllCaps">true</item>
+        <item name="android:textSize">10sp</item>
+        <item name="android:lineSpacingExtra">4sp</item>
+        <item name="android:lineHeight">16sp</item>
+        <item name="android:textColor">@color/tv_pip_edu_text</item>
+    </style>
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java
new file mode 100644
index 0000000..6efdd57
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/CenteredImageSpan.java
@@ -0,0 +1,76 @@
+/*
+ * 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.pip.tv;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.style.ImageSpan;
+
+/** An ImageSpan for a Drawable that is centered vertically in the line. */
+public class CenteredImageSpan extends ImageSpan {
+
+    private Drawable mDrawable;
+
+    public CenteredImageSpan(Drawable drawable) {
+        super(drawable);
+    }
+
+    @Override
+    public int getSize(
+            Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fontMetrics) {
+        final Drawable drawable = getCachedDrawable();
+        final Rect rect = drawable.getBounds();
+
+        if (fontMetrics != null) {
+            Paint.FontMetricsInt paintFontMetrics = paint.getFontMetricsInt();
+            fontMetrics.ascent = paintFontMetrics.ascent;
+            fontMetrics.descent = paintFontMetrics.descent;
+            fontMetrics.top = paintFontMetrics.top;
+            fontMetrics.bottom = paintFontMetrics.bottom;
+        }
+
+        return rect.right;
+    }
+
+    @Override
+    public void draw(
+            Canvas canvas,
+            CharSequence text,
+            int start,
+            int end,
+            float x,
+            int top,
+            int y,
+            int bottom,
+            Paint paint) {
+        final Drawable drawable = getCachedDrawable();
+        canvas.save();
+        final int transY = (bottom - drawable.getBounds().bottom) / 2;
+        canvas.translate(x, transY);
+        drawable.draw(canvas);
+        canvas.restore();
+    }
+
+    private Drawable getCachedDrawable() {
+        if (mDrawable == null) {
+            mDrawable = getDrawable();
+        }
+        return mDrawable;
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index 1aefd77..21d5d40 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -27,6 +27,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Insets;
 import android.graphics.Rect;
 import android.os.SystemClock;
 import android.util.ArraySet;
@@ -150,6 +151,10 @@
         mKeepClearAlgorithm.setScreenSize(screenSize);
         mKeepClearAlgorithm.setMovementBounds(insetBounds);
         mKeepClearAlgorithm.setStashOffset(mTvPipBoundsState.getStashOffset());
+        mKeepClearAlgorithm.setPipPermanentDecorInsets(
+                mTvPipBoundsState.getPipMenuPermanentDecorInsets());
+        mKeepClearAlgorithm.setPipTemporaryDecorInsets(
+                mTvPipBoundsState.getPipMenuTemporaryDecorInsets());
 
         final Placement placement = mKeepClearAlgorithm.calculatePipPosition(
                 pipSize,
@@ -340,6 +345,7 @@
         final DisplayLayout displayLayout = mTvPipBoundsState.getDisplayLayout();
         final float expandedRatio =
                 mTvPipBoundsState.getDesiredTvExpandedAspectRatio(); // width / height
+        final Insets pipDecorations = mTvPipBoundsState.getPipMenuPermanentDecorInsets();
 
         final Size expandedSize;
         if (expandedRatio == 0) {
@@ -352,7 +358,8 @@
             if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
                 expandedSize = mTvPipBoundsState.getTvExpandedSize();
             } else {
-                int maxHeight = displayLayout.height() - (2 * mScreenEdgeInsets.y);
+                int maxHeight = displayLayout.height() - (2 * mScreenEdgeInsets.y)
+                        - pipDecorations.top - pipDecorations.bottom;
                 float aspectRatioHeight = mFixedExpandedWidthInPx / expandedRatio;
 
                 if (maxHeight > aspectRatioHeight) {
@@ -374,7 +381,8 @@
             if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_VERTICAL) {
                 expandedSize = mTvPipBoundsState.getTvExpandedSize();
             } else {
-                int maxWidth = displayLayout.width() - (2 * mScreenEdgeInsets.x);
+                int maxWidth = displayLayout.width() - (2 * mScreenEdgeInsets.x)
+                        - pipDecorations.left - pipDecorations.right;
                 float aspectRatioWidth = mFixedExpandedHeightInPx * expandedRatio;
                 if (maxWidth > aspectRatioWidth) {
                     if (DEBUG) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
index 9865548..ea07499 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.graphics.Insets;
 import android.util.Size;
 import android.view.Gravity;
 
@@ -60,7 +61,8 @@
     private @Orientation int mTvFixedPipOrientation;
     private int mTvPipGravity;
     private @Nullable Size mTvExpandedSize;
-
+    private @NonNull Insets mPipMenuPermanentDecorInsets = Insets.NONE;
+    private @NonNull Insets mPipMenuTemporaryDecorInsets = Insets.NONE;
 
     public TvPipBoundsState(@NonNull Context context) {
         super(context);
@@ -159,4 +161,19 @@
         return mIsTvExpandedPipSupported;
     }
 
+    public void setPipMenuPermanentDecorInsets(@NonNull Insets permanentInsets) {
+        mPipMenuPermanentDecorInsets = permanentInsets;
+    }
+
+    public @NonNull Insets getPipMenuPermanentDecorInsets() {
+        return mPipMenuPermanentDecorInsets;
+    }
+
+    public void setPipMenuTemporaryDecorInsets(@NonNull Insets temporaryDecorInsets) {
+        mPipMenuTemporaryDecorInsets = temporaryDecorInsets;
+    }
+
+    public @NonNull Insets getPipMenuTemporaryDecorInsets() {
+        return mPipMenuTemporaryDecorInsets;
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 46b8e60..0e1f5a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -115,6 +115,7 @@
     private int mPipForceCloseDelay;
 
     private int mResizeAnimationDuration;
+    private int mEduTextWindowExitAnimationDurationMs;
 
     public static Pip create(
             Context context,
@@ -323,11 +324,15 @@
         }
     }
 
+    private void updatePinnedStackBounds() {
+        updatePinnedStackBounds(mResizeAnimationDuration);
+    }
+
     /**
      * Update the PiP bounds based on the state of the PiP and keep clear areas.
      * Animates to the current PiP bounds, and schedules unstashing the PiP if necessary.
      */
-    private void updatePinnedStackBounds() {
+    private void updatePinnedStackBounds(int animationDuration) {
         if (mState == STATE_NO_PIP) {
             return;
         }
@@ -353,23 +358,26 @@
             mUnstashRunnable = null;
         }
         if (!disallowStashing && placement.getUnstashDestinationBounds() != null) {
-            mUnstashRunnable = () -> movePinnedStackTo(placement.getUnstashDestinationBounds());
+            mUnstashRunnable = () -> {
+                movePinnedStackTo(placement.getUnstashDestinationBounds(), animationDuration);
+            };
             mMainHandler.postAtTime(mUnstashRunnable, placement.getUnstashTime());
         }
     }
 
     /** Animates the PiP to the given bounds. */
     private void movePinnedStackTo(Rect bounds) {
+        movePinnedStackTo(bounds, mResizeAnimationDuration);
+    }
+
+    /** Animates the PiP to the given bounds with the given animation duration. */
+    private void movePinnedStackTo(Rect bounds, int animationDuration) {
         if (DEBUG) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: movePinnedStack() - new pip bounds: %s", TAG, bounds.toShortString());
         }
         mPipTaskOrganizer.scheduleAnimateResizePip(bounds,
-                mResizeAnimationDuration, rect -> {
-                    if (DEBUG) {
-                        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                                "%s: movePinnedStack() animation done", TAG);
-                    }
+                animationDuration, rect -> {
                     mTvPipMenuController.updateExpansionState();
                 });
     }
@@ -408,6 +416,11 @@
         onPipDisappeared();
     }
 
+    @Override
+    public void closeEduText() {
+        updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs);
+    }
+
     private void registerSessionListenerForCurrentUser() {
         mPipMediaController.registerSessionListenerForCurrentUser();
     }
@@ -457,6 +470,7 @@
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState));
         }
+        mTvPipMenuController.notifyPipAnimating(true);
     }
 
     @Override
@@ -465,6 +479,7 @@
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
         }
+        mTvPipMenuController.notifyPipAnimating(false);
     }
 
     @Override
@@ -476,6 +491,7 @@
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState));
         }
+        mTvPipMenuController.notifyPipAnimating(false);
     }
 
     private void setState(@State int state) {
@@ -491,6 +507,8 @@
         final Resources res = mContext.getResources();
         mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
         mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay);
+        mEduTextWindowExitAnimationDurationMs =
+                res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
     }
 
     private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
index 5ac7a72..9ede443 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.pip.tv
 
+import android.graphics.Insets
 import android.graphics.Point
 import android.graphics.Rect
 import android.util.Size
@@ -94,6 +95,13 @@
     private var lastAreasOverlappingUnstashPosition: Set<Rect> = emptySet()
     private var lastStashTime: Long = Long.MIN_VALUE
 
+    /** Spaces around the PiP that we should leave space for when placing the PiP. Permanent PiP
+     * decorations are relevant for calculating intersecting keep clear areas */
+    private var pipPermanentDecorInsets = Insets.NONE
+    /** Spaces around the PiP that we should leave space for when placing the PiP. Temporary PiP
+     * decorations are not relevant for calculating intersecting keep clear areas */
+    private var pipTemporaryDecorInsets = Insets.NONE
+
     /**
      * Calculates the position the PiP should be placed at, taking into consideration the
      * given keep clear areas.
@@ -120,20 +128,29 @@
     ): Placement {
         val transformedRestrictedAreas = transformAndFilterAreas(restrictedAreas)
         val transformedUnrestrictedAreas = transformAndFilterAreas(unrestrictedAreas)
-        val pipAnchorBounds = getNormalPipAnchorBounds(pipSize, transformedMovementBounds)
 
+        val pipSizeWithAllDecors = addDecors(pipSize)
+        val pipAnchorBoundsWithAllDecors =
+                getNormalPipAnchorBounds(pipSizeWithAllDecors, transformedMovementBounds)
+
+        val pipAnchorBoundsWithPermanentDecors = removeTemporaryDecors(pipAnchorBoundsWithAllDecors)
         val result = calculatePipPositionTransformed(
-            pipAnchorBounds,
+            pipAnchorBoundsWithPermanentDecors,
             transformedRestrictedAreas,
             transformedUnrestrictedAreas
         )
 
-        val screenSpaceBounds = fromTransformedSpace(result.bounds)
+        val pipBounds = removePermanentDecors(fromTransformedSpace(result.bounds))
+        val anchorBounds = removePermanentDecors(fromTransformedSpace(result.anchorBounds))
+        val unstashedDestBounds = result.unstashDestinationBounds?.let {
+            removePermanentDecors(fromTransformedSpace(it))
+        }
+
         return Placement(
-            screenSpaceBounds,
-            fromTransformedSpace(result.anchorBounds),
-            getStashType(screenSpaceBounds, movementBounds),
-            result.unstashDestinationBounds?.let { fromTransformedSpace(it) },
+            pipBounds,
+            anchorBounds,
+            getStashType(pipBounds, movementBounds),
+            unstashedDestBounds,
             result.unstashTime
         )
     }
@@ -447,6 +464,16 @@
         transformedMovementBounds = toTransformedSpace(movementBounds)
     }
 
+    fun setPipPermanentDecorInsets(insets: Insets) {
+        if (pipPermanentDecorInsets == insets) return
+        pipPermanentDecorInsets = insets
+    }
+
+    fun setPipTemporaryDecorInsets(insets: Insets) {
+        if (pipTemporaryDecorInsets == insets) return
+        pipTemporaryDecorInsets = insets
+    }
+
     /**
      * @param open Whether this event marks the opening of an occupied segment
      * @param pos The coordinate of this event
@@ -735,6 +762,35 @@
         return horizontal && vertical
     }
 
+    /**
+     * Adds space around [size] to leave space for decorations that will be drawn around the pip
+     */
+    private fun addDecors(size: Size): Size {
+        val bounds = Rect(0, 0, size.width, size.height)
+        bounds.inset(pipPermanentDecorInsets)
+        bounds.inset(pipTemporaryDecorInsets)
+
+        return Size(bounds.width(), bounds.height())
+    }
+
+    /**
+     * Removes the space that was reserved for permanent decorations around the pip
+     */
+    private fun removePermanentDecors(bounds: Rect): Rect {
+        val pipDecorReverseInsets = Insets.subtract(Insets.NONE, pipPermanentDecorInsets)
+        bounds.inset(pipDecorReverseInsets)
+        return bounds
+    }
+
+    /**
+     * Removes the space that was reserved for temporary decorations around the pip
+     */
+    private fun removeTemporaryDecors(bounds: Rect): Rect {
+        val pipDecorReverseInsets = Insets.subtract(Insets.NONE, pipTemporaryDecorInsets)
+        bounds.inset(pipDecorReverseInsets)
+        return bounds
+    }
+
     private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) }
     private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom
     private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index 35c34ac..7b8dcf7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -25,13 +25,17 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.ParceledListSlice;
+import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Handler;
 import android.os.RemoteException;
+import android.view.LayoutInflater;
 import android.view.SurfaceControl;
 import android.view.SyncRtSurfaceTransactionApplier;
+import android.view.View;
+import android.view.ViewRootImpl;
 import android.view.WindowManagerGlobal;
 
 import androidx.annotation.Nullable;
@@ -53,15 +57,20 @@
 public class TvPipMenuController implements PipMenuController, TvPipMenuView.Listener {
     private static final String TAG = "TvPipMenuController";
     private static final boolean DEBUG = TvPipController.DEBUG;
+    private static final String BACKGROUND_WINDOW_TITLE = "PipBackgroundView";
 
     private final Context mContext;
     private final SystemWindows mSystemWindows;
     private final TvPipBoundsState mTvPipBoundsState;
     private final Handler mMainHandler;
+    private final int mPipMenuBorderWidth;
+    private final int mPipEduTextShowDurationMs;
+    private final int mPipEduTextHeight;
 
     private Delegate mDelegate;
     private SurfaceControl mLeash;
     private TvPipMenuView mPipMenuView;
+    private View mPipBackgroundView;
 
     // User can actively move the PiP via the DPAD.
     private boolean mInMoveMode;
@@ -74,6 +83,7 @@
     private RemoteAction mCloseAction;
 
     private SyncRtSurfaceTransactionApplier mApplier;
+    private SyncRtSurfaceTransactionApplier mBackgroundApplier;
     RectF mTmpSourceRectF = new RectF();
     RectF mTmpDestinationRectF = new RectF();
     Matrix mMoveTransform = new Matrix();
@@ -91,6 +101,7 @@
             if (DEBUG) e.printStackTrace();
         }
     };
+    private final Runnable mCloseEduTextRunnable = this::closeEduText;
 
     public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState,
             SystemWindows systemWindows, PipMediaController pipMediaController,
@@ -113,6 +124,13 @@
                 mainHandler, Context.RECEIVER_EXPORTED);
 
         pipMediaController.addActionListener(this::onMediaActionsChanged);
+
+        mPipEduTextShowDurationMs = context.getResources()
+                .getInteger(R.integer.pip_edu_text_show_duration_ms);
+        mPipEduTextHeight = context.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
+        mPipMenuBorderWidth = context.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_border_width);
     }
 
     void setDelegate(Delegate delegate) {
@@ -138,24 +156,63 @@
         }
 
         mLeash = leash;
-        attachPipMenuView();
+        attachPipMenu();
     }
 
-    private void attachPipMenuView() {
+    private void attachPipMenu() {
         if (DEBUG) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: attachPipMenuView()", TAG);
+                    "%s: attachPipMenu()", TAG);
         }
 
         if (mPipMenuView != null) {
-            detachPipMenuView();
+            detachPipMenu();
         }
 
+        attachPipBackgroundView();
+        attachPipMenuView();
+
+        mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth,
+                    -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth));
+        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight));
+        mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs);
+    }
+
+    private void attachPipMenuView() {
         mPipMenuView = new TvPipMenuView(mContext);
         mPipMenuView.setListener(this);
-        mSystemWindows.addView(mPipMenuView,
-                getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
-                0, SHELL_ROOT_LAYER_PIP);
+        setUpViewSurfaceZOrder(mPipMenuView, 1);
+        addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE);
+    }
+
+    private void attachPipBackgroundView() {
+        mPipBackgroundView = LayoutInflater.from(mContext)
+                .inflate(R.layout.tv_pip_menu_background, null);
+        setUpViewSurfaceZOrder(mPipBackgroundView, -1);
+        addPipMenuViewToSystemWindows(mPipBackgroundView, BACKGROUND_WINDOW_TITLE);
+    }
+
+    private void setUpViewSurfaceZOrder(View v, int zOrderRelativeToPip) {
+        v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {
+                    v.getViewRootImpl().addSurfaceChangedCallback(
+                            new PipMenuSurfaceChangedCallback(v, zOrderRelativeToPip));
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                }
+        });
+    }
+
+    private void addPipMenuViewToSystemWindows(View v, String title) {
+        mSystemWindows.addView(v, getPipMenuLayoutParams(title, 0 /* width */, 0 /* height */),
+                0 /* displayId */, SHELL_ROOT_LAYER_PIP);
+    }
+
+    void notifyPipAnimating(boolean animating) {
+        mPipMenuView.setEduTextActive(!animating);
     }
 
     void showMovementMenuOnly() {
@@ -171,8 +228,7 @@
     @Override
     public void showMenu() {
         if (DEBUG) {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: showMenu()", TAG);
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMenu()", TAG);
         }
         mInMoveMode = false;
         mCloseAfterExitMoveMenu = false;
@@ -183,27 +239,31 @@
         if (mPipMenuView == null) {
             return;
         }
-        Rect menuBounds = getMenuBounds(mTvPipBoundsState.getBounds());
-        mSystemWindows.updateViewLayout(mPipMenuView, getPipMenuLayoutParams(
-                MENU_WINDOW_TITLE, menuBounds.width(), menuBounds.height()));
+        maybeCloseEduText();
         maybeUpdateMenuViewActions();
         updateExpansionState();
 
-        SurfaceControl menuSurfaceControl = getSurfaceControl();
-        if (menuSurfaceControl != null) {
-            SurfaceControl.Transaction t = new SurfaceControl.Transaction();
-            t.setRelativeLayer(mPipMenuView.getWindowSurfaceControl(), mLeash, 1);
-            t.setPosition(menuSurfaceControl, menuBounds.left, menuBounds.top);
-            t.apply();
-        }
         grantPipMenuFocus(true);
         if (mInMoveMode) {
             mPipMenuView.showMoveMenu(mDelegate.getPipGravity());
         } else {
-            mPipMenuView.showButtonMenu();
+            mPipMenuView.showButtonsMenu();
         }
     }
 
+    private void maybeCloseEduText() {
+        if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) {
+            mMainHandler.removeCallbacks(mCloseEduTextRunnable);
+            mCloseEduTextRunnable.run();
+        }
+    }
+
+    private void closeEduText() {
+        mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE);
+        mPipMenuView.hideEduText();
+        mDelegate.closeEduText();
+    }
+
     void updateGravity(int gravity) {
         mPipMenuView.showMovementHints(gravity);
     }
@@ -214,12 +274,8 @@
         mPipMenuView.setIsExpanded(mTvPipBoundsState.isTvPipExpanded());
     }
 
-    private Rect getMenuBounds(Rect pipBounds) {
-        int extraSpaceInPx = mContext.getResources()
-                .getDimensionPixelSize(R.dimen.pip_menu_outer_space);
-        Rect menuBounds = new Rect(pipBounds);
-        menuBounds.inset(-extraSpaceInPx, -extraSpaceInPx);
-        return menuBounds;
+    private Rect calculateMenuSurfaceBounds(Rect pipBounds) {
+        return mPipMenuView.getPipMenuContainerBounds(pipBounds);
     }
 
     void closeMenu() {
@@ -227,11 +283,12 @@
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: closeMenu()", TAG);
         }
+
         if (mPipMenuView == null) {
             return;
         }
 
-        mPipMenuView.hideAll();
+        mPipMenuView.hideAllUserControls();
         grantPipMenuFocus(false);
         mDelegate.onMenuClosed();
     }
@@ -266,7 +323,7 @@
         }
         if (mInMoveMode) {
             mInMoveMode = false;
-            mPipMenuView.showButtonMenu();
+            mPipMenuView.showButtonsMenu();
             return true;
         }
         return false;
@@ -287,7 +344,8 @@
     @Override
     public void detach() {
         closeMenu();
-        detachPipMenuView();
+        mMainHandler.removeCallbacks(mCloseEduTextRunnable);
+        detachPipMenu();
         mLeash = null;
     }
 
@@ -346,20 +404,15 @@
 
     @Override
     public boolean isMenuVisible() {
-        boolean isVisible = mPipMenuView != null && mPipMenuView.isVisible();
-        if (DEBUG) {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: isMenuVisible: %b", TAG, isVisible);
-        }
-        return isVisible;
+        return true;
     }
 
     /**
      * Does an immediate window crop of the PiP menu.
      */
     @Override
-    public void resizePipMenu(@android.annotation.Nullable SurfaceControl pipLeash,
-            @android.annotation.Nullable SurfaceControl.Transaction t,
+    public void resizePipMenu(@Nullable SurfaceControl pipLeash,
+            @Nullable SurfaceControl.Transaction t,
             Rect destinationBounds) {
         if (DEBUG) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -373,24 +426,36 @@
             return;
         }
 
-        SurfaceControl surfaceControl = getSurfaceControl();
-        SyncRtSurfaceTransactionApplier.SurfaceParams
-                params = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(surfaceControl)
-                .withWindowCrop(getMenuBounds(destinationBounds))
+        final Rect menuBounds = calculateMenuSurfaceBounds(destinationBounds);
+
+        final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
+        final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams =
+                new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface)
+                .withWindowCrop(menuBounds)
                 .build();
+
+        final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
+        final SyncRtSurfaceTransactionApplier.SurfaceParams backParams =
+                new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface)
+                .withWindowCrop(menuBounds)
+                .build();
+
+        // TODO(b/226580399): switch to using SurfaceSyncer (see b/200284684) to synchronize the
+        // animations of the pip surface with the content of the front and back menu surfaces
+        mBackgroundApplier.scheduleApply(backParams);
         if (pipLeash != null && t != null) {
-            SyncRtSurfaceTransactionApplier.SurfaceParams
+            final SyncRtSurfaceTransactionApplier.SurfaceParams
                     pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
                     .withMergeTransaction(t)
                     .build();
-            mApplier.scheduleApply(params, pipParams);
+            mApplier.scheduleApply(frontParams, pipParams);
         } else {
-            mApplier.scheduleApply(params);
+            mApplier.scheduleApply(frontParams);
         }
     }
 
-    private SurfaceControl getSurfaceControl() {
-        return mSystemWindows.getViewSurface(mPipMenuView);
+    private SurfaceControl getSurfaceControl(View v) {
+        return mSystemWindows.getViewSurface(v);
     }
 
     @Override
@@ -412,44 +477,52 @@
             return;
         }
 
-        Rect menuDestBounds = getMenuBounds(pipDestBounds);
-        Rect mTmpSourceBounds = new Rect();
+        final Rect menuDestBounds = calculateMenuSurfaceBounds(pipDestBounds);
+        final Rect tmpSourceBounds = new Rect();
         // If there is no pip leash supplied, that means the PiP leash is already finalized
         // resizing and the PiP menu is also resized. We then want to do a scale from the current
         // new menu bounds.
         if (pipLeash != null && transaction != null) {
             if (DEBUG) {
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                        "%s: mTmpSourceBounds based on mPipMenuView.getBoundsOnScreen()", TAG);
+                        "%s: tmpSourceBounds based on mPipMenuView.getBoundsOnScreen()", TAG);
             }
-            mPipMenuView.getBoundsOnScreen(mTmpSourceBounds);
+            mPipMenuView.getBoundsOnScreen(tmpSourceBounds);
         } else {
             if (DEBUG) {
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                        "%s: mTmpSourceBounds based on menu width and height", TAG);
+                        "%s: tmpSourceBounds based on menu width and height", TAG);
             }
-            mTmpSourceBounds.set(0, 0, menuDestBounds.width(), menuDestBounds.height());
+            tmpSourceBounds.set(0, 0, menuDestBounds.width(), menuDestBounds.height());
         }
 
-        mTmpSourceRectF.set(mTmpSourceBounds);
+        mTmpSourceRectF.set(tmpSourceBounds);
         mTmpDestinationRectF.set(menuDestBounds);
-        mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
+        mMoveTransform.setTranslate(mTmpDestinationRectF.left, mTmpDestinationRectF.top);
 
-        SurfaceControl surfaceControl = getSurfaceControl();
-        SyncRtSurfaceTransactionApplier.SurfaceParams params =
-                new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(
-                        surfaceControl)
+        final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
+        final SyncRtSurfaceTransactionApplier.SurfaceParams frontParams =
+                new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(frontSurface)
                         .withMatrix(mMoveTransform)
                         .build();
 
+        final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
+        final SyncRtSurfaceTransactionApplier.SurfaceParams backParams =
+                new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(backSurface)
+                        .withMatrix(mMoveTransform)
+                        .build();
+
+        // TODO(b/226580399): switch to using SurfaceSyncer (see b/200284684) to synchronize the
+        // animations of the pip surface with the content of the front and back menu surfaces
+        mBackgroundApplier.scheduleApply(backParams);
         if (pipLeash != null && transaction != null) {
-            SyncRtSurfaceTransactionApplier.SurfaceParams
-                    pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
+            final SyncRtSurfaceTransactionApplier.SurfaceParams pipParams =
+                    new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash)
                     .withMergeTransaction(transaction)
                     .build();
-            mApplier.scheduleApply(params, pipParams);
+            mApplier.scheduleApply(frontParams, pipParams);
         } else {
-            mApplier.scheduleApply(params);
+            mApplier.scheduleApply(frontParams);
         }
 
         if (mPipMenuView.getViewRootImpl() != null) {
@@ -470,29 +543,40 @@
         if (mApplier == null) {
             mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView);
         }
+        if (mBackgroundApplier == null) {
+            mBackgroundApplier = new SyncRtSurfaceTransactionApplier(mPipBackgroundView);
+        }
         return true;
     }
 
-    private void detachPipMenuView() {
-        if (mPipMenuView == null) {
-            return;
+    private void detachPipMenu() {
+        if (mPipMenuView != null) {
+            mApplier = null;
+            mSystemWindows.removeView(mPipMenuView);
+            mPipMenuView = null;
         }
 
-        mApplier = null;
-        mSystemWindows.removeView(mPipMenuView);
-        mPipMenuView = null;
+        if (mPipBackgroundView != null) {
+            mBackgroundApplier = null;
+            mSystemWindows.removeView(mPipBackgroundView);
+            mPipBackgroundView = null;
+        }
     }
 
     @Override
     public void updateMenuBounds(Rect destinationBounds) {
-        Rect menuBounds = getMenuBounds(destinationBounds);
+        final Rect menuBounds = calculateMenuSurfaceBounds(destinationBounds);
         if (DEBUG) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                     "%s: updateMenuBounds: %s", TAG, menuBounds.toShortString());
         }
+        mSystemWindows.updateViewLayout(mPipBackgroundView,
+                getPipMenuLayoutParams(BACKGROUND_WINDOW_TITLE, menuBounds.width(),
+                        menuBounds.height()));
         mSystemWindows.updateViewLayout(mPipMenuView,
                 getPipMenuLayoutParams(MENU_WINDOW_TITLE, menuBounds.width(),
                         menuBounds.height()));
+
         if (mPipMenuView != null) {
             mPipMenuView.updateLayout(destinationBounds);
         }
@@ -538,6 +622,8 @@
 
         void onMenuClosed();
 
+        void closeEduText();
+
         void closePip();
     }
 
@@ -555,4 +641,30 @@
                     "%s: Unable to update focus, %s", TAG, e);
         }
     }
+
+    private class PipMenuSurfaceChangedCallback implements ViewRootImpl.SurfaceChangedCallback {
+        private final View mView;
+        private final int mZOrder;
+
+        PipMenuSurfaceChangedCallback(View v, int zOrder) {
+            mView = v;
+            mZOrder = zOrder;
+        }
+
+        @Override
+        public void surfaceCreated(SurfaceControl.Transaction t) {
+            final SurfaceControl sc = getSurfaceControl(mView);
+            if (sc != null) {
+                t.setRelativeLayer(sc, mLeash, mZOrder);
+            }
+        }
+
+        @Override
+        public void surfaceReplaced(SurfaceControl.Transaction t) {
+        }
+
+        @Override
+        public void surfaceDestroyed() {
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index 9529d04..5b0db8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -25,11 +25,17 @@
 import static android.view.KeyEvent.KEYCODE_DPAD_UP;
 import static android.view.KeyEvent.KEYCODE_ENTER;
 
+import android.animation.ValueAnimator;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
 import android.content.Context;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Handler;
+import android.text.Annotation;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannedString;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.KeyEvent;
@@ -40,6 +46,7 @@
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
+import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -49,6 +56,7 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
@@ -67,6 +75,15 @@
     private final LinearLayout mActionButtonsContainer;
     private final View mMenuFrameView;
     private final List<TvPipMenuActionButton> mAdditionalButtons = new ArrayList<>();
+    private final View mPipFrameView;
+    private final View mPipView;
+    private final TextView mEduTextView;
+    private final View mEduTextContainerView;
+    private final int mPipMenuOuterSpace;
+    private final int mPipMenuBorderWidth;
+    private final int mEduTextFadeExitAnimationDurationMs;
+    private final int mEduTextSlideExitAnimationDurationMs;
+    private int mEduTextHeight;
 
     private final ImageView mArrowUp;
     private final ImageView mArrowRight;
@@ -76,11 +93,13 @@
     private final ViewGroup mScrollView;
     private final ViewGroup mHorizontalScrollView;
 
-    private Rect mCurrentBounds;
+    private Rect mCurrentPipBounds;
 
     private final TvPipMenuActionButton mExpandButton;
     private final TvPipMenuActionButton mCloseButton;
 
+    private final int mPipMenuFadeAnimationDuration;
+
     public TvPipMenuView(@NonNull Context context) {
         this(context, null);
     }
@@ -116,21 +135,86 @@
         mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll);
 
         mMenuFrameView = findViewById(R.id.tv_pip_menu_frame);
+        mPipFrameView = findViewById(R.id.tv_pip_border);
+        mPipView = findViewById(R.id.tv_pip);
 
         mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up);
         mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right);
         mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down);
         mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left);
+
+        mEduTextView = findViewById(R.id.tv_pip_menu_edu_text);
+        mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container);
+
+        mPipMenuFadeAnimationDuration = context.getResources()
+                .getInteger(R.integer.pip_menu_fade_animation_duration);
+        mPipMenuOuterSpace = context.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_outer_space);
+        mPipMenuBorderWidth = context.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_border_width);
+        mEduTextHeight = context.getResources()
+                .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height);
+        mEduTextFadeExitAnimationDurationMs = context.getResources()
+                .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms);
+        mEduTextSlideExitAnimationDurationMs = context.getResources()
+                .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms);
+
+        initEduText();
     }
 
-    void updateLayout(Rect updatedBounds) {
-        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                "%s: update menu layout: %s", TAG, updatedBounds.toShortString());
-        boolean previouslyVertical =
-                mCurrentBounds != null && mCurrentBounds.height() > mCurrentBounds.width();
-        boolean vertical = updatedBounds.height() > updatedBounds.width();
+    void initEduText() {
+        final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text);
+        final SpannableString spannableString = new SpannableString(eduText);
+        Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst()
+                .ifPresent(annotation -> {
+                    final Drawable icon =
+                            getResources().getDrawable(R.drawable.home_icon, mContext.getTheme());
+                    if (icon != null) {
+                        icon.mutate();
+                        icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
+                        spannableString.setSpan(new CenteredImageSpan(icon),
+                                eduText.getSpanStart(annotation),
+                                eduText.getSpanEnd(annotation),
+                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+                    }
+                });
 
-        mCurrentBounds = updatedBounds;
+        mEduTextView.setText(spannableString);
+    }
+
+    void setEduTextActive(boolean active) {
+        mEduTextView.setSelected(active);
+    }
+
+    void hideEduText() {
+        final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0);
+        heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs);
+        heightAnimation.setInterpolator(TvPipInterpolators.BROWSE);
+        heightAnimation.addUpdateListener(animator -> {
+            mEduTextHeight = (int) animator.getAnimatedValue();
+        });
+        mEduTextView.animate()
+                .alpha(0f)
+                .setInterpolator(TvPipInterpolators.EXIT)
+                .setDuration(mEduTextFadeExitAnimationDurationMs)
+                .withEndAction(() -> {
+                    mEduTextContainerView.setVisibility(GONE);
+                }).start();
+        heightAnimation.start();
+    }
+
+    void updateLayout(Rect updatedPipBounds) {
+        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                "%s: update menu layout: %s", TAG, updatedPipBounds.toShortString());
+
+        boolean previouslyVertical =
+                mCurrentPipBounds != null && mCurrentPipBounds.height() > mCurrentPipBounds.width();
+        boolean vertical = updatedPipBounds.height() > updatedPipBounds.width();
+
+        mCurrentPipBounds = updatedPipBounds;
+
+        updatePipFrameBounds();
+
         if (previouslyVertical == vertical) {
             if (DEBUG) {
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
@@ -158,6 +242,38 @@
         mHorizontalScrollView.setVisibility(vertical ? GONE : VISIBLE);
     }
 
+    Rect getPipMenuContainerBounds(Rect pipBounds) {
+        final Rect menuUiBounds = new Rect(pipBounds);
+        menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace);
+        menuUiBounds.bottom += mEduTextHeight;
+        return menuUiBounds;
+    }
+
+    /**
+     * Update mPipFrameView's bounds according to the new pip window bounds. We can't
+     * make mPipFrameView match_parent, because the pip menu might contain other content around
+     * the pip window (e.g. edu text).
+     * TvPipMenuView needs to account for this so that it can draw a white border around the whole
+     * pip menu when it gains focus.
+     */
+    private void updatePipFrameBounds() {
+        final ViewGroup.LayoutParams pipFrameParams = mPipFrameView.getLayoutParams();
+        if (pipFrameParams != null) {
+            pipFrameParams.width = mCurrentPipBounds.width() + 2 * mPipMenuBorderWidth;
+            pipFrameParams.height = mCurrentPipBounds.height() + 2 * mPipMenuBorderWidth;
+            mPipFrameView.setLayoutParams(pipFrameParams);
+        }
+
+        final ViewGroup.LayoutParams pipViewParams = mPipView.getLayoutParams();
+        if (pipViewParams != null) {
+            pipViewParams.width = mCurrentPipBounds.width();
+            pipViewParams.height = mCurrentPipBounds.height();
+            mPipView.setLayoutParams(pipViewParams);
+        }
+
+
+    }
+
     void setListener(@Nullable Listener listener) {
         mListener = listener;
     }
@@ -184,30 +300,32 @@
         if (DEBUG) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG);
         }
-        showMenuButtons(false);
+        showButtonsMenu(false);
         showMovementHints(gravity);
-        showMenuFrame(true);
+        setFrameHighlighted(true);
     }
 
-    void showButtonMenu() {
+    void showButtonsMenu() {
         if (DEBUG) {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showButtonMenu()", TAG);
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: showButtonsMenu()", TAG);
         }
-        showMenuButtons(true);
+        showButtonsMenu(true);
         hideMovementHints();
-        showMenuFrame(true);
+        setFrameHighlighted(true);
     }
 
     /**
      * Hides all menu views, including the menu frame.
      */
-    void hideAll() {
+    void hideAllUserControls() {
         if (DEBUG) {
-            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAll()", TAG);
+            ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+                    "%s: hideAllUserControls()", TAG);
         }
-        showMenuButtons(false);
+        showButtonsMenu(false);
         hideMovementHints();
-        showMenuFrame(false);
+        setFrameHighlighted(false);
     }
 
     private void animateAlphaTo(float alpha, View view) {
@@ -217,7 +335,7 @@
         view.animate()
                 .alpha(alpha)
                 .setInterpolator(alpha == 0f ? TvPipInterpolators.EXIT : TvPipInterpolators.ENTER)
-                .setDuration(500)
+                .setDuration(mPipMenuFadeAnimationDuration)
                 .withStartAction(() -> {
                     if (alpha != 0) {
                         view.setVisibility(VISIBLE);
@@ -230,15 +348,6 @@
                 });
     }
 
-    boolean isVisible() {
-        return mMenuFrameView.getAlpha() != 0f
-                || mActionButtonsContainer.getAlpha() != 0f
-                || mArrowUp.getAlpha() != 0f
-                || mArrowRight.getAlpha() != 0f
-                || mArrowDown.getAlpha() != 0f
-                || mArrowLeft.getAlpha() != 0f;
-    }
-
     void setAdditionalActions(List<RemoteAction> actions, RemoteAction closeAction,
             Handler mainHandler) {
         if (DEBUG) {
@@ -423,18 +532,18 @@
     }
 
     /**
-     * Show or hide the pip user actions.
+     * Show or hide the pip buttons menu.
      */
-    public void showMenuButtons(boolean show) {
+    public void showButtonsMenu(boolean show) {
         if (DEBUG) {
             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                    "%s: showMenuButtons: %b", TAG, show);
+                    "%s: showUserActions: %b", TAG, show);
         }
         animateAlphaTo(show ? 1 : 0, mActionButtonsContainer);
     }
 
-    private void showMenuFrame(boolean show) {
-        animateAlphaTo(show ? 1 : 0, mMenuFrameView);
+    private void setFrameHighlighted(boolean highlighted) {
+        mMenuFrameView.setActivated(highlighted);
     }
 
     interface Listener {
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index a244d14..f4efc37 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 909476
+# Bug component: 1157642
 # includes OWNERS from parent directories
 natanieljr@google.com
 pablogamito@google.com
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
index fb404b9..684e5cad 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt
@@ -17,6 +17,8 @@
 package com.android.wm.shell.flicker.bubble
 
 import android.platform.test.annotations.Presubmit
+import android.view.WindowInsets
+import android.view.WindowManager
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import androidx.test.uiautomator.By
@@ -59,6 +61,16 @@
                 }
             }
             transitions {
+                // Swipe & wait for the notification shade to expand so all can be seen
+                val wm = context.getSystemService(WindowManager::class.java)
+                val metricInsets = wm.getCurrentWindowMetrics().windowInsets
+                val insets = metricInsets.getInsetsIgnoringVisibility(
+                        WindowInsets.Type.statusBars()
+                        or WindowInsets.Type.displayCutout())
+                device.swipe(100, insets.top + 100, 100, device.getDisplayHeight() / 2, 4)
+                device.waitForIdle(2000)
+                instrumentation.uiAutomation.syncInputTransactions()
+
                 val notification = device.wait(Until.findObject(
                     By.text("BubbleChat")), FIND_OBJECT_TIMEOUT)
                 notification?.click() ?: error("Notification not found")
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
index 3fe6f02..9a8c894 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.flicker.pip
 
+import android.platform.test.annotations.Presubmit
 import android.view.Surface
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -84,7 +85,7 @@
     override fun statusBarLayerRotatesScales() = super.statusBarLayerRotatesScales()
 
     /** {@inheritDoc}  */
-    @FlakyTest(bugId = 197726610)
+    @Presubmit
     @Test
     override fun pipLayerExpands() = super.pipLayerExpands()
 
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
index 5fc80db..9f3fcea 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
@@ -111,7 +111,7 @@
     /**
      * Checks that the visible region of [pipApp] always expands during the animation
      */
-    @Presubmit
+    @FlakyTest(bugId = 228012337)
     @Test
     fun pipLayerExpands() {
         val layerName = pipApp.component.toLayerName()
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
index 6af01e2..c1ee1a7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt
@@ -33,7 +33,6 @@
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
 import com.android.wm.shell.flicker.helpers.FixedAppHelper
 import org.junit.Assume
-import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -69,11 +68,6 @@
     private val screenBoundsStart = WindowUtils.getDisplayBounds(testSpec.startRotation)
     private val screenBoundsEnd = WindowUtils.getDisplayBounds(testSpec.endRotation)
 
-    @Before
-    open fun before() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-    }
-
     override val transition: FlickerBuilder.() -> Unit
         get() = buildTransition(eachRun = false) {
             setup {
@@ -135,15 +129,30 @@
     /**
      * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition
      */
-    @Presubmit
-    @Test
-    open fun pipLayerRotates_StartingBounds() {
+    private fun pipLayerRotates_StartingBounds_internal() {
         testSpec.assertLayersStart {
             visibleRegion(pipApp.component).coversAtMost(screenBoundsStart)
         }
     }
 
     /**
+     * Checks that [pipApp] layer is within [screenBoundsStart] at the start of the transition
+     */
+    @Presubmit
+    @Test
+    fun pipLayerRotates_StartingBounds() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
+        pipLayerRotates_StartingBounds_internal()
+    }
+
+    @FlakyTest(bugId = 228024285)
+    @Test
+    fun pipLayerRotates_StartingBounds_ShellTransit() {
+        Assume.assumeTrue(isShellTransitionsEnabled)
+        pipLayerRotates_StartingBounds_internal()
+    }
+
+    /**
      * Checks that [pipApp] layer is within [screenBoundsEnd] at the end of the transition
      */
     @Presubmit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
index 81403d0..e40f2bc 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinnedTest.kt
@@ -47,7 +47,6 @@
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group4
-@FlakyTest(bugId = 218604389)
 open class SetRequestedOrientationWhilePinnedTest(
     testSpec: FlickerTestParameter
 ) : PipTransition(testSpec) {
diff --git a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
index d743dff..6cd93ef 100644
--- a/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
+++ b/libs/WindowManager/Shell/tests/flicker/test-apps/flickerapp/src/com/android/wm/shell/flicker/testapp/BubbleHelper.java
@@ -22,7 +22,6 @@
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.Person;
-import android.app.RemoteInput;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Point;
@@ -116,24 +115,20 @@
     private Notification.Builder getNotificationBuilder(int id) {
         Person chatBot = new Person.Builder()
                 .setBot(true)
-                .setName("BubbleBot")
+                .setName("BubbleChat")
                 .setImportant(true)
                 .build();
-
-        RemoteInput remoteInput = new RemoteInput.Builder("key")
-                .setLabel("Reply")
-                .build();
-
         String shortcutId = "BubbleChat";
         return new Notification.Builder(mContext, CHANNEL_ID)
                 .setChannelId(CHANNEL_ID)
                 .setShortcutId(shortcutId)
+                .setContentTitle("BubbleChat")
                 .setContentIntent(PendingIntent.getActivity(mContext, 0,
                         new Intent(mContext, LaunchBubbleActivity.class),
                         PendingIntent.FLAG_UPDATE_CURRENT))
                 .setStyle(new Notification.MessagingStyle(chatBot)
-                        .setConversationTitle("Bubble Chat")
-                        .addMessage("Hello? This is bubble: " + id,
+                        .setConversationTitle("BubbleChat")
+                        .addMessage("BubbleChat",
                                 SystemClock.currentThreadTimeMillis() - 300000, chatBot)
                         .addMessage("Is it me, " + id + ", you're looking for?",
                                 SystemClock.currentThreadTimeMillis(), chatBot)
diff --git a/location/java/android/location/LocationDeviceConfig.java b/location/java/android/location/LocationDeviceConfig.java
index c55eed9..7d22681 100644
--- a/location/java/android/location/LocationDeviceConfig.java
+++ b/location/java/android/location/LocationDeviceConfig.java
@@ -24,6 +24,30 @@
 public final class LocationDeviceConfig {
 
     /**
+     * Package/tag combinations that are allowlisted for ignoring location settings (may retrieve
+     * location even when user location settings are off), for advanced driver-assistance systems
+     * only.
+     *
+     * <p>Package/tag combinations are separated by commas (","), and with in each combination is a
+     * package name followed by 0 or more attribution tags, separated by semicolons (";"). If a
+     * package is followed by 0 attribution tags, this is interpreted the same as the wildcard
+     * value. There are two special interpreted values for attribution tags, the wildcard value
+     * ("*") which represents all attribution tags, and the null value ("null"), which is converted
+     * to the null string (since attribution tags may be null). This format implies that attribution
+     * tags which should be on this list may not contain semicolons.
+     *
+     * <p>Examples of valid entries:
+     *
+     * <ul>
+     *   <li>android
+     *   <li>android;*
+     *   <li>android;*,com.example.app;null;my_attr
+     *   <li>android;*,com.example.app;null;my_attr,com.example.otherapp;my_attr
+     * </ul>
+     */
+    public static final String ADAS_SETTINGS_ALLOWLIST = "adas_settings_allowlist";
+
+    /**
      * Package/tag combinations that are allowedlisted for ignoring location settings (may retrieve
      * location even when user location settings are off, and may ignore throttling, etc), for
      * emergency purposes only.
@@ -39,10 +63,10 @@
      * <p>Examples of valid entries:
      *
      * <ul>
-     *     <li>android</li>
-     *     <li>android;*</li>
-     *     <li>android;*,com.example.app;null;my_attr</li>
-     *     <li>android;*,com.example.app;null;my_attr,com.example.otherapp;my_attr</li>
+     *   <li>android
+     *   <li>android;*
+     *   <li>android;*,com.example.app;null;my_attr
+     *   <li>android;*,com.example.app;null;my_attr,com.example.otherapp;my_attr
      * </ul>
      */
     public static final String IGNORE_SETTINGS_ALLOWLIST = "ignore_settings_allowlist";
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 491a5cd..e7eda3e 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -5898,14 +5898,8 @@
      */
     @UnsupportedAppUsage
     @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
-    public void setWiredDeviceConnectionState(int device, int state, String address,
-            String name) {
-        final IAudioService service = getService();
-        int role = isOutputDevice(device)
-                ? AudioDeviceAttributes.ROLE_OUTPUT : AudioDeviceAttributes.ROLE_INPUT;
-        AudioDeviceAttributes attributes = new AudioDeviceAttributes(
-                role, AudioDeviceInfo.convertInternalDeviceToDeviceType(device), address,
-                name, new ArrayList<>()/*mAudioProfiles*/, new ArrayList<>()/*mAudioDescriptors*/);
+    public void setWiredDeviceConnectionState(int device, int state, String address, String name) {
+        AudioDeviceAttributes attributes = new AudioDeviceAttributes(device, address, name);
         setWiredDeviceConnectionState(attributes, state);
     }
 
diff --git a/media/tests/aidltests/Android.bp b/media/tests/aidltests/Android.bp
index c3d5fa2..7a25b6d 100644
--- a/media/tests/aidltests/Android.bp
+++ b/media/tests/aidltests/Android.bp
@@ -33,7 +33,6 @@
     libs: [
         "framework",
     ],
-    sdk_version: "current",
     platform_apis: true,
     test_suites: ["device-tests"],
     certificate: "platform",
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index c2e36b7..e1a2e8d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -28,6 +28,7 @@
 import android.util.Log;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
@@ -239,13 +240,24 @@
 
     /**
      * Dispatch a change in the about-to-connect device. See
-     * {@link DeviceCallback#onAboutToConnectDeviceChanged} for more information.
+     * {@link DeviceCallback#onAboutToConnectDeviceAdded} for more information.
      */
-    public void dispatchAboutToConnectDeviceChanged(
-            @Nullable String deviceName,
+    public void dispatchAboutToConnectDeviceAdded(
+            @NonNull String deviceAddress,
+            @NonNull String deviceName,
             @Nullable Drawable deviceIcon) {
         for (DeviceCallback callback : getCallbacks()) {
-            callback.onAboutToConnectDeviceChanged(deviceName, deviceIcon);
+            callback.onAboutToConnectDeviceAdded(deviceAddress, deviceName, deviceIcon);
+        }
+    }
+
+    /**
+     * Dispatch a change in the about-to-connect device. See
+     * {@link DeviceCallback#onAboutToConnectDeviceRemoved} for more information.
+     */
+    public void dispatchAboutToConnectDeviceRemoved() {
+        for (DeviceCallback callback : getCallbacks()) {
+            callback.onAboutToConnectDeviceRemoved();
         }
     }
 
@@ -705,13 +717,27 @@
          * connect imminently and should be displayed as the current device in the media player.
          * See [AudioManager.muteAwaitConnection] for more details.
          *
-         * @param deviceName the name of the device (displayed to the user).
-         * @param deviceIcon the icon that should be used with the device.
+         * The information in the most recent callback should override information from any previous
+         * callbacks.
+         *
+         * @param deviceAddress the address of the device. {@see AudioDeviceAttributes.address}.
+         *                      If present, we'll use this address to fetch the full information
+         *                      about the device (if we can find that information).
+         * @param deviceName the name of the device (displayed to the user). Used as a backup in
+         *                   case using deviceAddress doesn't work.
+         * @param deviceIcon the icon that should be used with the device. Used as a backup in case
+         *                   using deviceAddress doesn't work.
          */
-        default void onAboutToConnectDeviceChanged(
-                @Nullable String deviceName,
+        default void onAboutToConnectDeviceAdded(
+                @NonNull String deviceAddress,
+                @NonNull String deviceName,
                 @Nullable Drawable deviceIcon
         ) {}
+
+        /**
+         * Callback for notifying that we no longer have an about-to-connect device.
+         */
+        default void onAboutToConnectDeviceRemoved() {}
     }
 
     /**
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index a10ca9e..a28c4cf 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -575,6 +575,9 @@
     <uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" />
     <uses-permission android:name="android.permission.TRUST_LISTENER" />
 
+    <!-- Permission required for CTS test - CtsTaskFpsCallbackTestCases -->
+    <uses-permission android:name="android.permission.ACCESS_FPS_COUNTER" />
+
     <!-- Permission required for CTS test - CtsGameManagerTestCases -->
     <uses-permission android:name="android.permission.MANAGE_GAME_MODE" />
 
diff --git a/packages/SystemUI/res-keyguard/layout/fgs_footer.xml b/packages/SystemUI/res-keyguard/layout/fgs_footer.xml
index 9d801d2..9ffafbc 100644
--- a/packages/SystemUI/res-keyguard/layout/fgs_footer.xml
+++ b/packages/SystemUI/res-keyguard/layout/fgs_footer.xml
@@ -16,8 +16,9 @@
 -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
+    android:layout_width="0dp"
     android:layout_height="@dimen/qs_security_footer_single_line_height"
+    android:layout_weight="1"
     android:gravity="center"
     android:clickable="true"
     android:visibility="gone">
@@ -26,7 +27,7 @@
         android:id="@+id/fgs_text_container"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginEnd="@dimen/new_qs_footer_action_inset"
+        android:layout_marginEnd="@dimen/qs_footer_action_inset"
         android:background="@drawable/qs_security_footer_background"
         android:layout_gravity="end"
         android:gravity="center"
@@ -86,7 +87,7 @@
             android:layout_height="12dp"
             android:scaleType="fitCenter"
             android:layout_gravity="bottom|end"
-            android:src="@drawable/new_fgs_dot"
+            android:src="@drawable/fgs_dot"
             android:contentDescription="@string/fgs_dot_content_description"
             />
     </FrameLayout>
diff --git a/packages/SystemUI/res-keyguard/layout/footer_actions.xml b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
index fb401ee..6a1d62d 100644
--- a/packages/SystemUI/res-keyguard/layout/footer_actions.xml
+++ b/packages/SystemUI/res-keyguard/layout/footer_actions.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-** Copyright 2021, The Android Open Source Project
+** Copyright 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.
@@ -18,65 +18,80 @@
 <!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc -->
 <com.android.systemui.qs.FooterActionsView
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:layout_width="match_parent"
-    android:layout_height="@dimen/qs_footer_height"
+    android:layout_height="@dimen/footer_actions_height"
+    android:elevation="@dimen/qs_panel_elevation"
+    android:paddingTop="8dp"
+    android:paddingBottom="4dp"
+    android:background="@drawable/qs_footer_actions_background"
     android:gravity="center_vertical"
     android:layout_gravity="bottom"
 >
 
-    <com.android.systemui.statusbar.phone.MultiUserSwitch
-        android:id="@+id/multi_user_switch"
-        android:layout_width="0dp"
+    <LinearLayout
+        android:id="@+id/security_footers_container"
+        android:orientation="horizontal"
         android:layout_height="@dimen/qs_footer_action_button_size"
-        android:layout_marginEnd="@dimen/qs_tile_margin_horizontal"
-        android:layout_weight="1"
-        android:background="@drawable/qs_footer_action_chip_background"
-        android:focusable="true">
-
-        <ImageView
-            android:id="@+id/multi_user_avatar"
-            android:layout_width="@dimen/multi_user_avatar_expanded_size"
-            android:layout_height="@dimen/multi_user_avatar_expanded_size"
-            android:layout_gravity="center"
-            android:scaleType="centerInside" />
-    </com.android.systemui.statusbar.phone.MultiUserSwitch>
-
-    <com.android.systemui.statusbar.AlphaOptimizedImageView
-        android:id="@+id/pm_lite"
         android:layout_width="0dp"
-        android:layout_height="@dimen/qs_footer_action_button_size"
-        android:layout_marginEnd="@dimen/qs_tile_margin_horizontal"
         android:layout_weight="1"
-        android:background="@drawable/qs_footer_action_chip_background"
-        android:clickable="true"
-        android:clipToPadding="false"
-        android:focusable="true"
-        android:padding="@dimen/qs_footer_icon_padding"
-        android:src="@*android:drawable/ic_lock_power_off"
-        android:contentDescription="@string/accessibility_quick_settings_power_menu"
-        android:tint="?android:attr/textColorPrimary" />
+    />
 
-    <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
-        android:id="@+id/settings_button_container"
-        android:layout_width="0dp"
-        android:layout_height="@dimen/qs_footer_action_button_size"
-        android:background="@drawable/qs_footer_action_chip_background"
-        android:layout_weight="1"
-        android:clipChildren="false"
-        android:clipToPadding="false">
+    <!-- Negative margin equal to -->
+    <LinearLayout
+        android:layout_height="match_parent"
+        android:layout_width="wrap_content"
+        android:layout_marginEnd="@dimen/qs_footer_action_inset_negative"
+        >
 
-        <com.android.systemui.statusbar.phone.SettingsButton
-            android:id="@+id/settings_button"
-            android:layout_width="match_parent"
+        <com.android.systemui.statusbar.phone.MultiUserSwitch
+            android:id="@+id/multi_user_switch"
+            android:layout_width="@dimen/qs_footer_action_button_size"
             android:layout_height="@dimen/qs_footer_action_button_size"
-            android:layout_gravity="center"
-            android:contentDescription="@string/accessibility_quick_settings_settings"
-            android:background="@drawable/qs_footer_action_chip_background_borderless"
+            android:background="@drawable/qs_footer_action_circle"
+            android:focusable="true">
+
+            <ImageView
+                android:id="@+id/multi_user_avatar"
+                android:layout_width="@dimen/qs_footer_icon_size"
+                android:layout_height="@dimen/qs_footer_icon_size"
+                android:layout_gravity="center"
+                android:scaleType="centerInside" />
+        </com.android.systemui.statusbar.phone.MultiUserSwitch>
+
+        <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
+            android:id="@+id/settings_button_container"
+            android:layout_width="@dimen/qs_footer_action_button_size"
+            android:layout_height="@dimen/qs_footer_action_button_size"
+            android:background="@drawable/qs_footer_action_circle"
+            android:clipChildren="false"
+            android:clipToPadding="false">
+
+            <com.android.systemui.statusbar.phone.SettingsButton
+                android:id="@+id/settings_button"
+                android:layout_width="@dimen/qs_footer_icon_size"
+                android:layout_height="@dimen/qs_footer_icon_size"
+                android:layout_gravity="center"
+                android:background="@android:color/transparent"
+                android:contentDescription="@string/accessibility_quick_settings_settings"
+                android:scaleType="centerInside"
+                android:src="@drawable/ic_settings"
+                android:tint="?android:attr/textColorPrimary" />
+
+        </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
+
+        <com.android.systemui.statusbar.AlphaOptimizedImageView
+            android:id="@+id/pm_lite"
+            android:layout_width="@dimen/qs_footer_action_button_size"
+            android:layout_height="@dimen/qs_footer_action_button_size"
+            android:background="@drawable/qs_footer_action_circle_color"
+            android:clickable="true"
+            android:clipToPadding="false"
+            android:focusable="true"
             android:padding="@dimen/qs_footer_icon_padding"
-            android:scaleType="centerInside"
-            android:src="@drawable/ic_settings"
-            android:tint="?android:attr/textColorPrimary" />
+            android:src="@*android:drawable/ic_lock_power_off"
+            android:contentDescription="@string/accessibility_quick_settings_power_menu"
+            android:tint="?androidprv:attr/textColorOnAccent" />
 
-    </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
-
+    </LinearLayout>
 </com.android.systemui.qs.FooterActionsView>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/new_footer_actions.xml b/packages/SystemUI/res-keyguard/layout/new_footer_actions.xml
deleted file mode 100644
index 59712c0..0000000
--- a/packages/SystemUI/res-keyguard/layout/new_footer_actions.xml
+++ /dev/null
@@ -1,97 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-** Copyright 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.
--->
-
-<!-- Action buttons for footer in QS/QQS, containing settings button, power off button etc -->
-<com.android.systemui.qs.FooterActionsView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/new_footer_height"
-    android:elevation="@dimen/qs_panel_elevation"
-    android:paddingTop="8dp"
-    android:paddingBottom="4dp"
-    android:background="@drawable/qs_footer_actions_background"
-    android:gravity="center_vertical"
-    android:layout_gravity="bottom"
->
-
-    <LinearLayout
-        android:id="@+id/security_footers_container"
-        android:orientation="horizontal"
-        android:layout_height="@dimen/qs_footer_action_button_size"
-        android:layout_width="0dp"
-        android:layout_weight="1"
-    />
-
-    <!-- Negative margin equal to -->
-    <LinearLayout
-        android:layout_height="match_parent"
-        android:layout_width="wrap_content"
-        android:layout_marginEnd="@dimen/new_qs_footer_action_inset_negative"
-        >
-
-        <com.android.systemui.statusbar.phone.MultiUserSwitch
-            android:id="@+id/multi_user_switch"
-            android:layout_width="@dimen/qs_footer_action_button_size"
-            android:layout_height="@dimen/qs_footer_action_button_size"
-            android:background="@drawable/qs_footer_action_circle"
-            android:focusable="true">
-
-            <ImageView
-                android:id="@+id/multi_user_avatar"
-                android:layout_width="@dimen/qs_footer_icon_size"
-                android:layout_height="@dimen/qs_footer_icon_size"
-                android:layout_gravity="center"
-                android:scaleType="centerInside" />
-        </com.android.systemui.statusbar.phone.MultiUserSwitch>
-
-        <com.android.systemui.statusbar.AlphaOptimizedFrameLayout
-            android:id="@+id/settings_button_container"
-            android:layout_width="@dimen/qs_footer_action_button_size"
-            android:layout_height="@dimen/qs_footer_action_button_size"
-            android:background="@drawable/qs_footer_action_circle"
-            android:clipChildren="false"
-            android:clipToPadding="false">
-
-            <com.android.systemui.statusbar.phone.SettingsButton
-                android:id="@+id/settings_button"
-                android:layout_width="@dimen/qs_footer_icon_size"
-                android:layout_height="@dimen/qs_footer_icon_size"
-                android:layout_gravity="center"
-                android:background="@android:color/transparent"
-                android:contentDescription="@string/accessibility_quick_settings_settings"
-                android:scaleType="centerInside"
-                android:src="@drawable/ic_settings"
-                android:tint="?android:attr/textColorPrimary" />
-
-        </com.android.systemui.statusbar.AlphaOptimizedFrameLayout>
-
-        <com.android.systemui.statusbar.AlphaOptimizedImageView
-            android:id="@+id/pm_lite"
-            android:layout_width="@dimen/qs_footer_action_button_size"
-            android:layout_height="@dimen/qs_footer_action_button_size"
-            android:background="@drawable/qs_footer_action_circle_color"
-            android:clickable="true"
-            android:clipToPadding="false"
-            android:focusable="true"
-            android:padding="@dimen/qs_footer_icon_padding"
-            android:src="@*android:drawable/ic_lock_power_off"
-            android:contentDescription="@string/accessibility_quick_settings_power_menu"
-            android:tint="?androidprv:attr/textColorOnAccent" />
-
-    </LinearLayout>
-</com.android.systemui.qs.FooterActionsView>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/new_fgs_dot.xml b/packages/SystemUI/res/drawable/fgs_dot.xml
similarity index 100%
rename from packages/SystemUI/res/drawable/new_fgs_dot.xml
rename to packages/SystemUI/res/drawable/fgs_dot.xml
diff --git a/packages/SystemUI/res/drawable/ic_media_connecting_container.xml b/packages/SystemUI/res/drawable/ic_media_connecting_container.xml
new file mode 100644
index 0000000..79d2a06
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_media_connecting_container.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>

+<!--

+  ~ 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

+-->

+<vector xmlns:android="http://schemas.android.com/apk/res/android"

+        android:height="48dp"

+        android:width="48dp"

+        android:viewportHeight="48"

+        android:viewportWidth="48">

+    <group android:name="_R_G">

+        <group android:name="_R_G_L_1_G"

+                android:translateX="24"

+                android:translateY="24"

+                android:scaleX="0.5"

+                android:scaleY="0.5"/>

+        <group android:name="_R_G_L_0_G"

+                android:translateX="24"

+                android:translateY="24"

+                android:scaleX="0.5"

+                android:scaleY="0.5">

+            <path android:name="_R_G_L_0_G_D_0_P_0"

+                    android:fillColor="#ffddb3"

+                    android:fillAlpha="1"

+                    android:fillType="nonZero"

+                    android:pathData=" M48 -16 C48,-16 48,16 48,16 C48,33.67 33.67,48 16,48 C16,48 -16,48 -16,48 C-33.67,48 -48,33.67 -48,16 C-48,16 -48,-16 -48,-16 C-48,-33.67 -33.67,-48 -16,-48 C-16,-48 16,-48 16,-48 C33.67,-48 48,-33.67 48,-16c "/>

+        </group>

+    </group>

+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_chip_background.xml b/packages/SystemUI/res/drawable/qs_footer_action_chip_background.xml
deleted file mode 100644
index 9076da7..0000000
--- a/packages/SystemUI/res/drawable/qs_footer_action_chip_background.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2021 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.
-  -->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:insetTop="@dimen/qs_footer_action_inset"
-    android:insetBottom="@dimen/qs_footer_action_inset">
-    <ripple
-        android:color="?android:attr/colorControlHighlight"
-        android:height="44dp">
-        <item android:id="@android:id/mask">
-            <shape android:shape="rectangle">
-                <solid android:color="@android:color/white"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-        <item>
-            <shape android:shape="rectangle">
-                <solid android:color="?attr/underSurfaceColor"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-        <item>
-            <shape android:shape="rectangle">
-                <stroke android:width="1dp" android:color="?android:attr/colorBackground"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-    </ripple>
-</inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_chip_background_borderless.xml b/packages/SystemUI/res/drawable/qs_footer_action_chip_background_borderless.xml
deleted file mode 100644
index bbcfb15..0000000
--- a/packages/SystemUI/res/drawable/qs_footer_action_chip_background_borderless.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2021 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.
-  -->
-<inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:insetTop="@dimen/qs_footer_action_inset"
-    android:insetBottom="@dimen/qs_footer_action_inset">
-    <ripple
-        android:color="?android:attr/colorControlHighlight">
-        <item android:id="@android:id/mask">
-            <shape android:shape="rectangle">
-                <solid android:color="@android:color/white"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-        <item>
-            <shape android:shape="rectangle">
-                <solid android:color="@android:color/transparent"/>
-                <corners android:radius="@dimen/qs_footer_action_corner_radius"/>
-            </shape>
-        </item>
-    </ripple>
-</inset>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
index 31a8c3b..c8c36b0 100644
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
+++ b/packages/SystemUI/res/drawable/qs_footer_action_circle.xml
@@ -15,7 +15,7 @@
   ~ limitations under the License.
   -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
-       android:inset="@dimen/new_qs_footer_action_inset">
+       android:inset="@dimen/qs_footer_action_inset">
     <ripple
         android:color="?android:attr/colorControlHighlight">
         <item android:id="@android:id/mask">
diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
index 021a85f..6a365000 100644
--- a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
+++ b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml
@@ -15,7 +15,7 @@
   ~ limitations under the License.
   -->
 <inset xmlns:android="http://schemas.android.com/apk/res/android"
-    android:inset="@dimen/new_qs_footer_action_inset">
+    android:inset="@dimen/qs_footer_action_inset">
     <ripple
         android:color="?android:attr/colorControlHighlight">
         <item android:id="@android:id/mask">
diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml
index b6e3499..b1d3ed05 100644
--- a/packages/SystemUI/res/layout/qs_footer_impl.xml
+++ b/packages/SystemUI/res/layout/qs_footer_impl.xml
@@ -29,11 +29,6 @@
     android:clipChildren="false"
     android:clipToPadding="false">
 
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
-
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="@dimen/qs_footer_height"
@@ -80,14 +75,4 @@
 
         </LinearLayout>
 
-        <ViewStub
-            android:id="@+id/footer_stub"
-            android:inflatedId="@+id/qs_footer_actions"
-            android:layout="@layout/footer_actions"
-            android:layout_height="@dimen/qs_footer_height"
-            android:layout_width="match_parent"
-        />
-
-    </LinearLayout>
-
 </com.android.systemui.qs.QSFooterView>
diff --git a/packages/SystemUI/res/layout/qs_panel.xml b/packages/SystemUI/res/layout/qs_panel.xml
index 2040051..1eb05bf 100644
--- a/packages/SystemUI/res/layout/qs_panel.xml
+++ b/packages/SystemUI/res/layout/qs_panel.xml
@@ -47,11 +47,10 @@
 
     <include layout="@layout/quick_status_bar_expanded_header" />
 
-    <ViewStub
-        android:id="@+id/container_stub"
-        android:inflatedId="@+id/qs_footer_actions"
-        android:layout="@layout/new_footer_actions"
-        android:layout_height="@dimen/new_footer_height"
+    <include
+        layout="@layout/footer_actions"
+        android:id="@+id/qs_footer_actions"
+        android:layout_height="@dimen/footer_actions_height"
         android:layout_width="match_parent"
         android:layout_gravity="bottom"
         />
diff --git a/packages/SystemUI/res/layout/quick_settings_security_footer.xml b/packages/SystemUI/res/layout/quick_settings_security_footer.xml
index 08bd71c..1b11816 100644
--- a/packages/SystemUI/res/layout/quick_settings_security_footer.xml
+++ b/packages/SystemUI/res/layout/quick_settings_security_footer.xml
@@ -14,19 +14,18 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.systemui.util.DualHeightHorizontalLinearLayout
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:systemui="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/qs_security_footer_height"
+    android:layout_width="0dp"
+    android:layout_height="@dimen/qs_security_footer_single_line_height"
+    android:layout_weight="1"
     android:clickable="true"
-    android:padding="@dimen/qs_footer_padding"
+    android:orientation="horizontal"
+    android:paddingHorizontal="@dimen/qs_footer_padding"
     android:gravity="center_vertical"
     android:layout_gravity="center_vertical|center_horizontal"
-    android:layout_marginBottom="@dimen/qs_footers_margin_bottom"
+    android:layout_marginEnd="@dimen/qs_footer_action_inset"
     android:background="@drawable/qs_security_footer_background"
-    systemui:singleLineHeight="@dimen/qs_security_footer_single_line_height"
-    systemui:textViewId="@id/footer_text"
     >
 
     <ImageView
@@ -43,7 +42,7 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_weight="1"
-        android:maxLines="@integer/qs_security_footer_maxLines"
+        android:singleLine="true"
         android:ellipsize="end"
         android:textAppearance="@style/TextAppearance.QS.SecurityFooter"
         android:textColor="?android:attr/textColorSecondary"/>
@@ -58,4 +57,4 @@
         android:autoMirrored="true"
         android:tint="?android:attr/textColorSecondary" />
 
-</com.android.systemui.util.DualHeightHorizontalLinearLayout>
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
index b1e8c38..60bc373 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
@@ -52,7 +52,6 @@
     <!-- We want this to be centered (to align with notches). In order to do that, the following
          has to hold (in portrait):
          * date_container and privacy_container must have the same width and weight
-         * header_text_container must be gone
          -->
     <android.widget.Space
         android:id="@+id/space"
@@ -61,17 +60,6 @@
         android:layout_gravity="center_vertical|center_horizontal"
         android:visibility="gone" />
 
-    <!-- Will hold security footer in landscape with media -->
-    <FrameLayout
-        android:id="@+id/header_text_container"
-        android:layout_height="match_parent"
-        android:layout_width="0dp"
-        android:layout_weight="1"
-        android:paddingStart="16dp"
-        android:paddingEnd="16dp"
-        android:gravity="center"
-    />
-
     <FrameLayout
         android:id="@+id/privacy_container"
         android:layout_width="0dp"
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index 8de8084..c60609b 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -98,6 +98,7 @@
         android:scaleType="fitEnd"
         android:background="@drawable/overlay_preview_background"
         android:adjustViewBounds="true"
+        android:clickable="true"
         app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
         app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
         app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index 62903d5..3228c3c 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -188,12 +188,6 @@
         <attr name="borderColor" format="color" />
     </declare-styleable>
 
-    <declare-styleable name="DualHeightHorizontalLinearLayout">
-        <attr name="singleLineHeight" format="dimension" />
-        <attr name="singleLineVerticalPadding" format="dimension" />
-        <attr name="textViewId" format="reference" />
-    </declare-styleable>
-
     <attr name="overlayButtonTextColor" format="color" />
 
     <declare-styleable name="DreamOverlayDotImageView">
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 50fdc7b..24118d2 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -137,9 +137,11 @@
     <!-- UDFPS colors -->
     <color name="udfps_enroll_icon">#7DA7F1</color>
     <color name="udfps_moving_target_fill">#475670</color>
+    <!-- 50% of udfps_moving_target_fill-->
+    <color name="udfps_moving_target_fill_error">#80475670</color>
     <color name="udfps_enroll_progress">#7DA7F1</color>
     <color name="udfps_enroll_progress_help">#607DA7F1</color>
-    <color name="udfps_enroll_progress_help_with_talkback">#ffEE675C</color>
+    <color name="udfps_enroll_progress_help_with_talkback">#FFEE675C</color>
 
     <!-- Floating overlay actions -->
     <color name="overlay_button_ripple">#1f000000</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 851c6dd..7672251 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -327,23 +327,20 @@
          car setting. -->
     <dimen name="car_qs_header_system_icons_area_height">54dp</dimen>
 
-    <!-- The height of the quick settings footer that holds the user switcher, settings icon,
-         etc. -->
+    <!-- The height of the quick settings footer that holds the pagination dots and edit button -->
     <dimen name="qs_footer_height">48dp</dimen>
 
     <!-- 40dp (circles) + 8dp (circle padding) + 8dp (top) + 4dp (bottom) -->
-    <dimen name="new_footer_height">60dp</dimen>
+    <dimen name="footer_actions_height">60dp</dimen>
 
     <!-- The size of each of the icon buttons in the QS footer -->
     <dimen name="qs_footer_action_button_size">48dp</dimen>
 
     <dimen name="qs_footer_action_corner_radius">20dp</dimen>
 
-    <!-- (48dp - 44dp) / 2 -->
-    <dimen name="qs_footer_action_inset">2dp</dimen>
     <!-- (48dp - 40dp) / 2 -->
-    <dimen name="new_qs_footer_action_inset">4dp</dimen>
-    <dimen name="new_qs_footer_action_inset_negative">-4dp</dimen>
+    <dimen name="qs_footer_action_inset">4dp</dimen>
+    <dimen name="qs_footer_action_inset_negative">-4dp</dimen>
 
     <!-- Margins on each side of QS Footer -->
     <dimen name="qs_footer_margin">2dp</dimen>
@@ -495,7 +492,7 @@
     <dimen name="qs_panel_padding">16dp</dimen>
     <dimen name="qs_dual_tile_padding_horizontal">6dp</dimen>
     <dimen name="qs_panel_elevation">4dp</dimen>
-    <dimen name="qs_panel_padding_bottom">@dimen/new_footer_height</dimen>
+    <dimen name="qs_panel_padding_bottom">@dimen/footer_actions_height</dimen>
     <dimen name="qs_panel_padding_top">48dp</dimen>
 
     <dimen name="qs_data_usage_text_size">14sp</dimen>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 926734c..ff71b4f 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -23,6 +23,7 @@
     <item type="id" name="scale_x_animator_tag"/>
     <item type="id" name="scale_y_animator_tag"/>
     <item type="id" name="top_inset_animator_tag"/>
+    <item type="id" name="bottom_inset_animator_tag"/>
     <item type="id" name="height_animator_tag"/>
     <item type="id" name="x_animator_tag"/>
     <item type="id" name="y_animator_tag"/>
@@ -33,6 +34,7 @@
     <item type="id" name="scale_y_animator_end_value_tag"/>
     <item type="id" name="alpha_animator_end_value_tag"/>
     <item type="id" name="top_inset_animator_end_value_tag"/>
+    <item type="id" name="bottom_inset_animator_end_value_tag"/>
     <item type="id" name="height_animator_end_value_tag"/>
     <item type="id" name="x_animator_tag_end_value"/>
     <item type="id" name="y_animator_tag_end_value"/>
@@ -43,6 +45,7 @@
     <item type="id" name="scale_y_animator_start_value_tag"/>
     <item type="id" name="alpha_animator_start_value_tag"/>
     <item type="id" name="top_inset_animator_start_value_tag"/>
+    <item type="id" name="bottom_inset_animator_start_value_tag"/>
     <item type="id" name="height_animator_start_value_tag"/>
     <item type="id" name="x_animator_tag_start_value"/>
     <item type="id" name="y_animator_tag_start_value"/>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 33ed7b8..b248efe 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2199,6 +2199,8 @@
     <string name="controls_media_button_prev">Previous track</string>
     <!-- Description for button in media controls. Pressing button goes to next track [CHAR_LIMIT=NONE] -->
     <string name="controls_media_button_next">Next track</string>
+    <!-- Description for button in media controls. Used when media is connecting to a remote device (via something like chromecast). Pressing button does nothing [CHAR_LIMIT=NONE] -->
+    <string name="controls_media_button_connecting">Connecting</string>
 
     <!-- Title for Smartspace recommendation card within media controls. The "Play" means the action to play a media [CHAR_LIMIT=10] -->
     <string name="controls_media_smartspace_rec_title">Play</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
index 6541206..5953611 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
@@ -54,6 +54,7 @@
     val bouncerFullyShown: Boolean,
     val faceAuthenticated: Boolean,
     val faceDisabled: Boolean,
+    val goingToSleep: Boolean,
     val keyguardAwake: Boolean,
     val keyguardGoingAway: Boolean,
     val listeningForFaceAssistant: Boolean,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index dd2664d..1383635 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -628,7 +628,8 @@
      */
     public void setKeyguardGoingAway(boolean goingAway) {
         mKeyguardGoingAway = goingAway;
-        updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
+        // This is set specifically to stop face authentication from running.
+        updateBiometricListeningState(BIOMETRIC_ACTION_STOP);
     }
 
     /**
@@ -1746,7 +1747,8 @@
                 cb.onFinishedGoingToSleep(arg1);
             }
         }
-        updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
+        // This is set specifically to stop face authentication from running.
+        updateBiometricListeningState(BIOMETRIC_ACTION_STOP);
     }
 
     private void handleScreenTurnedOff() {
@@ -1764,7 +1766,12 @@
                 cb.onDreamingStateChanged(mIsDreaming);
             }
         }
-        updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
+        if (mIsDreaming) {
+            updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+            updateFaceListeningState(BIOMETRIC_ACTION_STOP);
+        } else {
+            updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
+        }
     }
 
     private void handleUserInfoChanged(int userId) {
@@ -2477,9 +2484,11 @@
         boolean strongAuthAllowsScanning = (!isEncryptedOrTimedOut || canBypass
                 && !mBouncerFullyShown);
 
-        // If the device supports face detection (without authentication), allow it to happen
-        // if the device is in lockdown mode. Otherwise, prevent scanning.
+        // If the device supports face detection (without authentication) and bypass is enabled,
+        // allow face scanning to happen if the device is in lockdown mode.
+        // Otherwise, prevent scanning.
         final boolean supportsDetectOnly = !mFaceSensorProperties.isEmpty()
+                && canBypass
                 && mFaceSensorProperties.get(0).supportsFaceDetection;
         if (isLockDown && !supportsDetectOnly) {
             strongAuthAllowsScanning = false;
@@ -2494,8 +2503,11 @@
         // 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 || mAuthInterruptActive || mOccludingAppRequestingFace
-                        || awakeKeyguard || shouldListenForFaceAssistant
+                (mBouncerFullyShown && !mGoingToSleep
+                        || mAuthInterruptActive
+                        || mOccludingAppRequestingFace
+                        || awakeKeyguard
+                        || shouldListenForFaceAssistant
                         || mAuthController.isUdfpsFingerDown())
                 && !mSwitchingUser && !faceDisabledForUser && becauseCannotSkipBouncer
                 && !mKeyguardGoingAway && biometricEnabledForUser && !mLockIconPressed
@@ -2517,6 +2529,7 @@
                         mBouncerFullyShown,
                         faceAuthenticated,
                         faceDisabledForUser,
+                        mGoingToSleep,
                         awakeKeyguard,
                         mKeyguardGoingAway,
                         shouldListenForFaceAssistant,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
index f3a603f..59c658f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollProgressBarDrawable.java
@@ -23,6 +23,7 @@
 import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
 import android.view.accessibility.AccessibilityManager;
+import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.view.animation.OvershootInterpolator;
 
@@ -40,13 +41,15 @@
 
     private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L;
     private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L;
-    private static final long FILL_COLOR_ANIMATION_DURATION_MS = 200L;
+    private static final long FILL_COLOR_ANIMATION_DURATION_MS = 350L;
     private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
     private static final float STROKE_WIDTH_DP = 12f;
+    private static final Interpolator DEACCEL = new DecelerateInterpolator();
 
     private final float mStrokeWidthPx;
     @ColorInt private final int mProgressColor;
     @ColorInt private final int mHelpColor;
+    @ColorInt private final int mOnFirstBucketFailedColor;
     @NonNull private final Drawable mCheckmarkDrawable;
     @NonNull private final Interpolator mCheckmarkInterpolator;
     @NonNull private final Paint mBackgroundPaint;
@@ -64,6 +67,9 @@
     @Nullable private ValueAnimator mFillColorAnimator;
     @NonNull private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
 
+    @Nullable private ValueAnimator mBackgroundColorAnimator;
+    @NonNull private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdateListener;
+
     private boolean mComplete = false;
     private float mCheckmarkScale = 0f;
     @Nullable private ValueAnimator mCheckmarkAnimator;
@@ -76,8 +82,10 @@
         final boolean isAccessbilityEnabled = am.isTouchExplorationEnabled();
         if (!isAccessbilityEnabled) {
             mHelpColor = context.getColor(R.color.udfps_enroll_progress_help);
+            mOnFirstBucketFailedColor = context.getColor(R.color.udfps_moving_target_fill_error);
         } else {
             mHelpColor = context.getColor(R.color.udfps_enroll_progress_help_with_talkback);
+            mOnFirstBucketFailedColor = mHelpColor;
         }
         mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark);
         mCheckmarkDrawable.mutate();
@@ -112,6 +120,11 @@
             mCheckmarkScale = (float) animation.getAnimatedValue();
             invalidateSelf();
         };
+
+        mBackgroundColorUpdateListener = animation -> {
+            mBackgroundPaint.setColor((int) animation.getAnimatedValue());
+            invalidateSelf();
+        };
     }
 
     void onEnrollmentProgress(int remaining, int totalSteps) {
@@ -163,19 +176,38 @@
         }
     }
 
+    private void animateBackgroundColor() {
+        if (mBackgroundColorAnimator != null && mBackgroundColorAnimator.isRunning()) {
+            mBackgroundColorAnimator.end();
+        }
+        mBackgroundColorAnimator = ValueAnimator.ofArgb(mBackgroundPaint.getColor(),
+                mOnFirstBucketFailedColor);
+        mBackgroundColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
+        mBackgroundColorAnimator.setRepeatCount(1);
+        mBackgroundColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
+        mBackgroundColorAnimator.setInterpolator(DEACCEL);
+        mBackgroundColorAnimator.addUpdateListener(mBackgroundColorUpdateListener);
+        mBackgroundColorAnimator.start();
+    }
+
     private void updateFillColor(boolean showingHelp) {
-        if (mShowingHelp == showingHelp) {
+        if (!mAfterFirstTouch && showingHelp) {
+            // If we are on the first touch, animate the background color
+            // instead of the progress color.
+            animateBackgroundColor();
             return;
         }
-        mShowingHelp = showingHelp;
 
         if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
-            mFillColorAnimator.cancel();
+            mFillColorAnimator.end();
         }
 
         @ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor;
         mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor);
         mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
+        mFillColorAnimator.setRepeatCount(1);
+        mFillColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
+        mFillColorAnimator.setInterpolator(DEACCEL);
         mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
         mFillColorAnimator.start();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 9356b16..c9a61a8 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -112,6 +112,10 @@
     public static final ResourceBooleanFlag QS_USER_DETAIL_SHORTCUT =
             new ResourceBooleanFlag(503, R.bool.flag_lockscreen_qs_user_detail_shortcut);
 
+    /**
+     * @deprecated Not needed anymore
+     */
+    @Deprecated
     public static final BooleanFlag NEW_FOOTER = new BooleanFlag(504, true);
 
     public static final BooleanFlag NEW_HEADER = new BooleanFlag(505, false);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index b93239b..d674b2b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -690,6 +690,11 @@
         startDelay: Long = 0
     ) {
         desiredHostState?.let {
+            if (this.desiredLocation != desiredLocation) {
+                // Only log an event when location changes
+                logger.logCarouselPosition(desiredLocation)
+            }
+
             // This is a hosting view, let's remeasure our players
             this.desiredLocation = desiredLocation
             this.desiredHostState = it
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 5ead375..05b2c50 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -333,7 +333,7 @@
             mMediaViewHolder.getPlayer().setOnClickListener(v -> {
                 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
                 if (mMediaViewController.isGutsVisible()) return;
-
+                mLogger.logTapContentView(mUid, mPackageName, mInstanceId);
                 logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
                 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
                         buildLaunchAnimatorController(mMediaViewHolder.getPlayer()));
@@ -660,38 +660,43 @@
             final ImageButton button, MediaAction mediaAction, ConstraintSet collapsedSet,
             ConstraintSet expandedSet, boolean showInCompact) {
 
-        animHandler.unregisterAll();
         if (mediaAction != null) {
-            final Drawable icon = mediaAction.getIcon();
-            button.setImageDrawable(icon);
-            button.setContentDescription(mediaAction.getContentDescription());
-            final Drawable bgDrawable = mediaAction.getBackground();
-            button.setBackground(bgDrawable);
+            if (animHandler.updateRebindId(mediaAction.getRebindId())) {
+                animHandler.unregisterAll();
 
-            animHandler.tryRegister(icon);
-            animHandler.tryRegister(bgDrawable);
+                final Drawable icon = mediaAction.getIcon();
+                button.setImageDrawable(icon);
+                button.setContentDescription(mediaAction.getContentDescription());
+                final Drawable bgDrawable = mediaAction.getBackground();
+                button.setBackground(bgDrawable);
 
-            Runnable action = mediaAction.getAction();
-            if (action == null) {
-                button.setEnabled(false);
-            } else {
-                button.setEnabled(true);
-                button.setOnClickListener(v -> {
-                    if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
-                        mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
-                        logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
-                        action.run();
+                animHandler.tryRegister(icon);
+                animHandler.tryRegister(bgDrawable);
 
-                        if (icon instanceof Animatable) {
-                            ((Animatable) icon).start();
+                Runnable action = mediaAction.getAction();
+                if (action == null) {
+                    button.setEnabled(false);
+                } else {
+                    button.setEnabled(true);
+                    button.setOnClickListener(v -> {
+                        if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                            mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
+                            mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
+                            logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
+                            action.run();
+
+                            if (icon instanceof Animatable) {
+                                ((Animatable) icon).start();
+                            }
+                            if (bgDrawable instanceof Animatable) {
+                                ((Animatable) bgDrawable).start();
+                            }
                         }
-                        if (bgDrawable instanceof Animatable) {
-                            ((Animatable) bgDrawable).start();
-                        }
-                    }
-                });
+                    });
+                }
             }
         } else {
+            animHandler.unregisterAll();
             button.setImageDrawable(null);
             button.setContentDescription(null);
             button.setEnabled(false);
@@ -702,9 +707,29 @@
         setVisibleAndAlpha(expandedSet, button.getId(), mediaAction != null);
     }
 
+    // AnimationBindHandler is responsible for tracking the bound animation state and preventing
+    // jank and conflicts due to media notifications arriving at any time during an animation. It
+    // does this in two parts.
+    //  - Exit animations fired as a result of user input are tracked. When these are running, any
+    //      bind actions are delayed until the animation completes (and then fired in sequence).
+    //  - Continuous animations are tracked using their rebind id. Later calls using the same
+    //      rebind id will be totally ignored to prevent the continuous animation from restarting.
     private static class AnimationBindHandler extends Animatable2.AnimationCallback {
         private ArrayList<Runnable> mOnAnimationsComplete = new ArrayList<>();
         private ArrayList<Animatable2> mRegistrations = new ArrayList<>();
+        private Integer mRebindId = null;
+
+        // This check prevents rebinding to the action button if the identifier has not changed. A
+        // null value is always considered to be changed. This is used to prevent the connecting
+        // animation from rebinding (and restarting) if multiple buffer PlaybackStates are pushed by
+        // an application in a row.
+        public boolean updateRebindId(Integer rebindId) {
+            if (mRebindId == null || rebindId == null || !mRebindId.equals(rebindId)) {
+                mRebindId = rebindId;
+                return true;
+            }
+            return false;
+        }
 
         public void tryRegister(Drawable drawable) {
             if (drawable instanceof Animatable2) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
index a4d2f7b..bc8cca5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -184,7 +184,12 @@
     val icon: Drawable?,
     val action: Runnable?,
     val contentDescription: CharSequence?,
-    val background: Drawable?
+    val background: Drawable?,
+
+    // Rebind Id is used to detect identical rebinds and ignore them. It is intended
+    // to prevent continuously looping animations from restarting due to the arrival
+    // of repeated media notifications that are visually identical.
+    val rebindId: Int? = null
 )
 
 /** State of the media device. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index 908aef4..57c93ba 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -30,6 +30,7 @@
 import android.content.pm.PackageManager
 import android.graphics.Bitmap
 import android.graphics.ImageDecoder
+import android.graphics.drawable.Animatable
 import android.graphics.drawable.Icon
 import android.media.MediaDescription
 import android.media.MediaMetadata
@@ -57,6 +58,7 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState
+import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState
 import com.android.systemui.statusbar.notification.row.HybridGroupManager
 import com.android.systemui.tuner.TunerService
 import com.android.systemui.util.Assert
@@ -777,7 +779,20 @@
         val actions = MediaButton()
         controller.playbackState?.let { state ->
             // First, check for standard actions
-            actions.playOrPause = if (isPlayingState(state.state)) {
+            actions.playOrPause = if (isConnectingState(state.state)) {
+                // Spinner needs to be animating to render anything. Start it here.
+                val drawable = context.getDrawable(
+                        com.android.internal.R.drawable.progress_small_material)
+                (drawable as Animatable).start()
+                MediaAction(
+                    drawable,
+                    null, // no action to perform when clicked
+                    context.getString(R.string.controls_media_button_connecting),
+                    context.getDrawable(R.drawable.ic_media_connecting_container),
+                    // Specify a rebind id to prevent the spinner from restarting on later binds.
+                    com.android.internal.R.drawable.progress_small_material
+                )
+            } else if (isPlayingState(state.state)) {
                 getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE)
             } else {
                 getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index 824a6fd..d623191 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -164,7 +164,7 @@
             }
         // A device that is not yet connected but is expected to connect imminently. Because it's
         // expected to connect imminently, it should be displayed as the current device.
-        private var aboutToConnectDeviceOverride: MediaDeviceData? = null
+        private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
 
         @AnyThread
         fun start() = bgExecutor.execute {
@@ -222,22 +222,34 @@
             }
         }
 
-        override fun onAboutToConnectDeviceChanged(deviceName: String?, deviceIcon: Drawable?) {
-            aboutToConnectDeviceOverride = if (deviceName == null || deviceIcon == null) {
-                null
-            } else {
-                MediaDeviceData(enabled = true, deviceIcon, deviceName)
-            }
+        override fun onAboutToConnectDeviceAdded(
+            deviceAddress: String,
+            deviceName: String,
+            deviceIcon: Drawable?
+        ) {
+            aboutToConnectDeviceOverride = AboutToConnectDevice(
+                fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
+                backupMediaDeviceData = MediaDeviceData(enabled = true, deviceIcon, deviceName)
+            )
+            updateCurrent()
+        }
+
+        override fun onAboutToConnectDeviceRemoved() {
+            aboutToConnectDeviceOverride = null
             updateCurrent()
         }
 
         @WorkerThread
         private fun updateCurrent() {
-            if (aboutToConnectDeviceOverride != null) {
-                current = aboutToConnectDeviceOverride
-                return
+            val aboutToConnect = aboutToConnectDeviceOverride
+            if (aboutToConnect != null &&
+                aboutToConnect.fullMediaDevice == null &&
+                aboutToConnect.backupMediaDeviceData != null) {
+                    // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
+                    current = aboutToConnect.backupMediaDeviceData
+                    return
             }
-            val device = localMediaManager.currentConnectedDevice
+            val device = aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
             val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
 
             // If we have a controller but get a null route, then don't trust the device
@@ -247,3 +259,17 @@
         }
     }
 }
+
+/**
+ * A class storing information for the about-to-connect device. See
+ * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information.
+ *
+ * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If
+ *   non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData].
+ * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum
+ *   information required to display the device. Only use if [fullMediaDevice] is null.
+ */
+private data class AboutToConnectDevice(
+    val fullMediaDevice: MediaDevice? = null,
+    val backupMediaDeviceData: MediaDeviceData? = null
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index 3d9f933..30771c7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -1146,7 +1146,10 @@
 @Retention(AnnotationRetention.SOURCE)
 private annotation class TransformationType
 
-@IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
-    MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
+@IntDef(prefix = ["LOCATION_"], value = [
+    MediaHierarchyManager.LOCATION_QS,
+    MediaHierarchyManager.LOCATION_QQS,
+    MediaHierarchyManager.LOCATION_LOCKSCREEN,
+    MediaHierarchyManager.LOCATION_DREAM_OVERLAY])
 @Retention(AnnotationRetention.SOURCE)
 annotation class MediaLocation
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
index 862b279..3eba3b5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaUiEventLogger.kt
@@ -134,6 +134,23 @@
     fun logOpenOutputSwitcher(uid: Int, packageName: String, instanceId: InstanceId) {
         logger.logWithInstanceId(MediaUiEvent.OPEN_OUTPUT_SWITCHER, uid, packageName, instanceId)
     }
+
+    fun logTapContentView(uid: Int, packageName: String, instanceId: InstanceId) {
+        logger.logWithInstanceId(MediaUiEvent.MEDIA_TAP_CONTENT_VIEW, uid, packageName, instanceId)
+    }
+
+    fun logCarouselPosition(@MediaLocation location: Int) {
+        val event = when (location) {
+            MediaHierarchyManager.LOCATION_QQS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QQS
+            MediaHierarchyManager.LOCATION_QS -> MediaUiEvent.MEDIA_CAROUSEL_LOCATION_QS
+            MediaHierarchyManager.LOCATION_LOCKSCREEN ->
+                MediaUiEvent.MEDIA_CAROUSEL_LOCATION_LOCKSCREEN
+            MediaHierarchyManager.LOCATION_DREAM_OVERLAY ->
+                MediaUiEvent.MEDIA_CAROUSEL_LOCATION_DREAM
+            else -> throw IllegalArgumentException("Unknown media carousel location $location")
+        }
+        logger.log(event)
+    }
 }
 
 enum class MediaUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum {
@@ -161,7 +178,7 @@
     @UiEvent(doc = "An existing active media control was converted into resumable media")
     ACTIVE_TO_RESUME(1014),
 
-    @UiEvent(doc = "Media timed out")
+    @UiEvent(doc = "A media control timed out")
     MEDIA_TIMEOUT(1015),
 
     @UiEvent(doc = "A media control was removed from the carousel")
@@ -173,35 +190,50 @@
     @UiEvent(doc = "The user swiped away the media carousel")
     DISMISS_SWIPE(1018),
 
-    @UiEvent(doc = "The user opened the long press menu")
+    @UiEvent(doc = "The user long pressed on a media control")
     OPEN_LONG_PRESS(1019),
 
-    @UiEvent(doc = "The user dismissed via long press menu")
+    @UiEvent(doc = "The user dismissed a media control via its long press menu")
     DISMISS_LONG_PRESS(1020),
 
-    @UiEvent(doc = "The user opened settings from long press menu")
+    @UiEvent(doc = "The user opened media settings from a media control's long press menu")
     OPEN_SETTINGS_LONG_PRESS(1021),
 
-    @UiEvent(doc = "The user opened settings from the carousel")
+    @UiEvent(doc = "The user opened media settings from the media carousel")
     OPEN_SETTINGS_CAROUSEL(1022),
 
-    @UiEvent(doc = "The play/pause button was tapped")
+    @UiEvent(doc = "The play/pause button on a media control was tapped")
     TAP_ACTION_PLAY_PAUSE(1023),
 
-    @UiEvent(doc = "The previous button was tapped")
+    @UiEvent(doc = "The previous button on a media control was tapped")
     TAP_ACTION_PREV(1024),
 
-    @UiEvent(doc = "The next button was tapped")
+    @UiEvent(doc = "The next button on a media control was tapped")
     TAP_ACTION_NEXT(1025),
 
-    @UiEvent(doc = "A custom or generic action button was tapped")
+    @UiEvent(doc = "A custom or generic action button on a media control was tapped")
     TAP_ACTION_OTHER(1026),
 
-    @UiEvent(doc = "The user seeked using the seekbar")
+    @UiEvent(doc = "The user seeked on a media control using the seekbar")
     ACTION_SEEK(1027),
 
     @UiEvent(doc = "The user opened the output switcher from a media control")
-    OPEN_OUTPUT_SWITCHER(1028);
+    OPEN_OUTPUT_SWITCHER(1028),
+
+    @UiEvent(doc = "The user tapped on a media control view")
+    MEDIA_TAP_CONTENT_VIEW(1036),
+
+    @UiEvent(doc = "The media carousel moved to QQS")
+    MEDIA_CAROUSEL_LOCATION_QQS(1037),
+
+    @UiEvent(doc = "THe media carousel moved to QS")
+    MEDIA_CAROUSEL_LOCATION_QS(1038),
+
+    @UiEvent(doc = "The media carousel moved to the lockscreen")
+    MEDIA_CAROUSEL_LOCATION_LOCKSCREEN(1039),
+
+    @UiEvent(doc = "The media carousel moved to the dream state")
+    MEDIA_CAROUSEL_LOCATION_DREAM(1040);
 
     override fun getId() = metricId
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt
index 22bc557..2783532 100644
--- a/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManager.kt
@@ -52,7 +52,9 @@
                 // There should only be one device that's mutedUntilConnection at a time, so we can
                 // safely override any previous value.
                 currentMutedDevice = device
-                localMediaManager.dispatchAboutToConnectDeviceChanged(device.name, device.getIcon())
+                localMediaManager.dispatchAboutToConnectDeviceAdded(
+                    device.address, device.name, device.getIcon()
+                )
             }
         }
 
@@ -63,7 +65,7 @@
         ) {
             if (currentMutedDevice == device && USAGE_MEDIA in mutedUsages) {
                 currentMutedDevice = null
-                localMediaManager.dispatchAboutToConnectDeviceChanged(null, null)
+                localMediaManager.dispatchAboutToConnectDeviceRemoved()
             }
         }
     }
@@ -76,8 +78,8 @@
         val currentDevice = audioManager.mutingExpectedDevice
         if (currentDevice != null) {
             currentMutedDevice = currentDevice
-            localMediaManager.dispatchAboutToConnectDeviceChanged(
-                currentDevice.name, currentDevice.getIcon()
+            localMediaManager.dispatchAboutToConnectDeviceAdded(
+                currentDevice.address, currentDevice.name, currentDevice.getIcon()
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
index 0365241..e5dc0ec 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
@@ -360,9 +360,7 @@
                 .getDimension(R.dimen.navigation_edge_action_drag_threshold);
         mSwipeProgressThreshold = context.getResources()
                 .getDimension(R.dimen.navigation_edge_action_progress_threshold);
-        if (mBackAnimation != null) {
-            mBackAnimation.setSwipeThresholds(mSwipeTriggerThreshold, mSwipeProgressThreshold);
-        }
+        initializeBackAnimation();
 
         setVisibility(GONE);
         Executor backgroundExecutor = Dependency.get(Dependency.BACKGROUND_EXECUTOR);
@@ -391,6 +389,13 @@
 
     public void setBackAnimation(BackAnimation backAnimation) {
         mBackAnimation = backAnimation;
+        initializeBackAnimation();
+    }
+
+    private void initializeBackAnimation() {
+        if (mBackAnimation != null) {
+            mBackAnimation.setSwipeThresholds(mSwipeTriggerThreshold, mSwipeProgressThreshold);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java
index c01d6dc..c6c9aca 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java
@@ -466,7 +466,7 @@
                 }
             }
         } catch (SQLException e) {
-            Log.e(TAG, "Failed to query contact: " + e);
+            Log.e(TAG, "Failed to query contact", e);
         } finally {
             if (cursor != null) {
                 cursor.close();
@@ -527,7 +527,7 @@
                 lookupKeysWithBirthdaysToday.add(lookupKey);
             }
         } catch (SQLException e) {
-            Log.e(TAG, "Failed to query birthdays: " + e);
+            Log.e(TAG, "Failed to query birthdays", e);
         } finally {
             if (cursor != null) {
                 cursor.close();
diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
index f6e1cd4..1a7bd8c 100644
--- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
+++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
@@ -275,7 +275,7 @@
                 updateSingleConversationWidgets(widgetIds);
             }
         } catch (Exception e) {
-            Log.e(TAG, "Exception: " + e);
+            Log.e(TAG, "failed to update widgets", e);
         }
     }
 
@@ -348,7 +348,7 @@
         try {
             return getTileForExistingWidgetThrowing(appWidgetId);
         } catch (Exception e) {
-            Log.e(TAG, "Failed to retrieve conversation for tile: " + e);
+            Log.e(TAG, "failed to retrieve tile for widget ID " + appWidgetId, e);
             return null;
         }
     }
@@ -423,7 +423,7 @@
             // Add current state.
             return getTileWithCurrentState(storedTile.build(), ACTION_BOOT_COMPLETED);
         } catch (RemoteException e) {
-            Log.e(TAG, "Could not retrieve data: " + e);
+            Log.e(TAG, "getTileFromPersistentStorage failing", e);
             return null;
         }
     }
@@ -441,12 +441,16 @@
                 Log.d(TAG, "Notification removed, key: " + sbn.getKey());
             }
         }
+        if (DEBUG) Log.d(TAG, "Fetching notifications");
+        Collection<NotificationEntry> notifications = mNotifCollection.getAllNotifs();
         mBgExecutor.execute(
-                () -> updateWidgetsWithNotificationChangedInBackground(sbn, notificationAction));
+                () -> updateWidgetsWithNotificationChangedInBackground(
+                        sbn, notificationAction, notifications));
     }
 
     private void updateWidgetsWithNotificationChangedInBackground(StatusBarNotification sbn,
-            PeopleSpaceUtils.NotificationAction action) {
+            PeopleSpaceUtils.NotificationAction action,
+            Collection<NotificationEntry> notifications) {
         try {
             PeopleTileKey key = new PeopleTileKey(
                     sbn.getShortcutId(), sbn.getUser().getIdentifier(), sbn.getPackageName());
@@ -469,23 +473,23 @@
                     Log.d(TAG, "Widgets by URI to be updated:" + tilesUpdatedByUri.toString());
                 }
                 tilesUpdated.addAll(tilesUpdatedByUri);
-                updateWidgetIdsBasedOnNotifications(tilesUpdated);
+                updateWidgetIdsBasedOnNotifications(tilesUpdated, notifications);
             }
         } catch (Exception e) {
-            Log.e(TAG, "Throwing exception: " + e);
+            Log.e(TAG, "updateWidgetsWithNotificationChangedInBackground failing", e);
         }
     }
 
     /** Updates {@code widgetIdsToUpdate} with {@code action}. */
-    private void updateWidgetIdsBasedOnNotifications(Set<String> widgetIdsToUpdate) {
+    private void updateWidgetIdsBasedOnNotifications(Set<String> widgetIdsToUpdate,
+            Collection<NotificationEntry> ungroupedNotifications) {
         if (widgetIdsToUpdate.isEmpty()) {
             if (DEBUG) Log.d(TAG, "No widgets to update, returning.");
             return;
         }
         try {
-            if (DEBUG) Log.d(TAG, "Fetching grouped notifications");
             Map<PeopleTileKey, Set<NotificationEntry>> groupedNotifications =
-                    getGroupedConversationNotifications();
+                    groupConversationNotifications(ungroupedNotifications);
 
             widgetIdsToUpdate
                     .stream()
@@ -495,7 +499,7 @@
                             id -> getAugmentedTileForExistingWidget(id, groupedNotifications)))
                     .forEach((id, tile) -> updateAppWidgetOptionsAndViewOptional(id, tile));
         } catch (Exception e) {
-            Log.e(TAG, "Exception updating widgets: " + e);
+            Log.e(TAG, "updateWidgetIdsBasedOnNotifications failing", e);
         }
     }
 
@@ -510,7 +514,7 @@
                     "Augmenting tile from NotificationEntryManager widget: " + key.toString());
         }
         Map<PeopleTileKey, Set<NotificationEntry>> notifications =
-                getGroupedConversationNotifications();
+                groupConversationNotifications(mNotifCollection.getAllNotifs());
         String contactUri = null;
         if (tile.getContactUri() != null) {
             contactUri = tile.getContactUri().toString();
@@ -518,9 +522,10 @@
         return augmentTileFromNotifications(tile, key, contactUri, notifications, appWidgetId);
     }
 
-    /** Returns active and pending notifications grouped by {@link PeopleTileKey}. */
-    public Map<PeopleTileKey, Set<NotificationEntry>> getGroupedConversationNotifications() {
-        Collection<NotificationEntry> notifications = mNotifCollection.getAllNotifs();
+    /** Groups active and pending notifications grouped by {@link PeopleTileKey}. */
+    public Map<PeopleTileKey, Set<NotificationEntry>> groupConversationNotifications(
+            Collection<NotificationEntry> notifications
+    ) {
         if (DEBUG) Log.d(TAG, "Number of total notifications: " + notifications.size());
         Map<PeopleTileKey, Set<NotificationEntry>> groupedNotifications =
                 notifications
@@ -846,7 +851,7 @@
                     Collections.singletonList(tile.getId()),
                     tile.getUserHandle(), LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS);
         } catch (Exception e) {
-            Log.w(TAG, "Exception caching shortcut:" + e);
+            Log.w(TAG, "failed to cache shortcut", e);
         }
         PeopleSpaceTile finalTile = tile;
         mBgExecutor.execute(
@@ -954,7 +959,7 @@
                     UserHandle.of(key.getUserId()),
                     LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS);
         } catch (Exception e) {
-            Log.d(TAG, "Exception uncaching shortcut:" + e);
+            Log.d(TAG, "failed to uncache shortcut", e);
         }
     }
 
@@ -1037,7 +1042,7 @@
                     packageName, userHandle.getIdentifier(), shortcutId);
             tile = PeopleSpaceUtils.getTile(channel, mLauncherApps);
         } catch (Exception e) {
-            Log.w(TAG, "Exception getting tiles: " + e);
+            Log.w(TAG, "failed to get conversation or tile", e);
             return null;
         }
         if (tile == null) {
@@ -1086,7 +1091,7 @@
                 }
             } catch (PackageManager.NameNotFoundException e) {
                 // Delete data for uninstalled widgets.
-                Log.e(TAG, "Package no longer found for tile: " + e);
+                Log.e(TAG, "package no longer found for tile", e);
                 JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
                 if (jobScheduler != null
                         && jobScheduler.getPendingJob(PeopleBackupFollowUpJob.JOB_ID) != null) {
@@ -1296,7 +1301,7 @@
                     try {
                         editor.putString(newId, (String) entry.getValue());
                     } catch (Exception e) {
-                        Log.e(TAG, "Malformed entry value: " + entry.getValue());
+                        Log.e(TAG, "malformed entry value: " + entry.getValue(), e);
                     }
                     editor.remove(key);
                     break;
@@ -1306,7 +1311,7 @@
                     try {
                         oldWidgetIds = (Set<String>) entry.getValue();
                     } catch (Exception e) {
-                        Log.e(TAG, "Malformed entry value: " + entry.getValue());
+                        Log.e(TAG, "malformed entry value: " + entry.getValue(), e);
                         editor.remove(key);
                         break;
                     }
@@ -1337,7 +1342,7 @@
             try {
                 oldWidgetIds = (Set<String>) entry.getValue();
             } catch (Exception e) {
-                Log.e(TAG, "Malformed entry value: " + entry.getValue());
+                Log.e(TAG, "malformed entry value: " + entry.getValue(), e);
                 followUpEditor.remove(key);
                 continue;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
index 44ef2b6..3f394e7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
@@ -23,7 +23,6 @@
 import android.provider.Settings.Global.USER_SWITCHER_ENABLED
 import android.view.View
 import android.view.ViewGroup
-import android.widget.LinearLayout
 import androidx.annotation.VisibleForTesting
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.logging.MetricsLogger
@@ -32,8 +31,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.FalsingManager
@@ -45,7 +42,6 @@
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.UserInfoController
 import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener
-import com.android.systemui.util.DualHeightHorizontalLinearLayout
 import com.android.systemui.util.ViewController
 import com.android.systemui.util.settings.GlobalSettings
 import javax.inject.Inject
@@ -74,8 +70,7 @@
     private val uiEventLogger: UiEventLogger,
     @Named(PM_LITE_ENABLED) private val showPMLiteButton: Boolean,
     private val globalSetting: GlobalSettings,
-    private val handler: Handler,
-    private val featureFlags: FeatureFlags
+    private val handler: Handler
 ) : ViewController<FooterActionsView>(view) {
 
     private var globalActionsDialog: GlobalActionsDialogLite? = null
@@ -100,7 +95,9 @@
         view.findViewById(R.id.security_footers_container)
     private val powerMenuLite: View = view.findViewById(R.id.pm_lite)
     private val multiUserSwitchController = multiUserSwitchControllerFactory.create(view)
-    private val securityFootersSeparator = View(context).apply {
+
+    @VisibleForTesting
+    internal val securityFootersSeparator = View(context).apply {
         visibility = View.GONE
     }
 
@@ -171,48 +168,30 @@
         }
         settingsButton.setOnClickListener(onClickListener)
         multiUserSetting.isListening = true
-        if (featureFlags.isEnabled(Flags.NEW_FOOTER)) {
-            val securityFooter = securityFooterController.view as DualHeightHorizontalLinearLayout
-            securityFootersContainer?.addView(securityFooter)
-            val separatorWidth = resources.getDimensionPixelSize(R.dimen.new_qs_footer_action_inset)
-            securityFootersContainer?.addView(securityFootersSeparator, separatorWidth, 1)
-            reformatForNewFooter(securityFooter)
-            val fgsFooter = fgsManagerFooterController.view
-            securityFootersContainer?.addView(fgsFooter)
-            (fgsFooter.layoutParams as LinearLayout.LayoutParams).apply {
-                width = 0
-                weight = 1f
-            }
 
-            val visibilityListener =
-                VisibilityChangedDispatcher.OnVisibilityChangedListener { visibility ->
-                    if (visibility == View.GONE) {
-                        securityFootersSeparator.visibility = View.GONE
-                    } else if (securityFooter.visibility == View.VISIBLE &&
-                        fgsFooter.visibility == View.VISIBLE) {
-                        securityFootersSeparator.visibility = View.VISIBLE
-                    } else {
-                        securityFootersSeparator.visibility = View.GONE
-                    }
-                    fgsManagerFooterController
-                        .setCollapsed(securityFooter.visibility == View.VISIBLE)
+        val securityFooter = securityFooterController.view
+        securityFootersContainer?.addView(securityFooter)
+        val separatorWidth = resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset)
+        securityFootersContainer?.addView(securityFootersSeparator, separatorWidth, 1)
+
+        val fgsFooter = fgsManagerFooterController.view
+        securityFootersContainer?.addView(fgsFooter)
+
+        val visibilityListener =
+            VisibilityChangedDispatcher.OnVisibilityChangedListener { visibility ->
+                if (securityFooter.visibility == View.VISIBLE &&
+                    fgsFooter.visibility == View.VISIBLE) {
+                    securityFootersSeparator.visibility = View.VISIBLE
+                } else {
+                    securityFootersSeparator.visibility = View.GONE
                 }
-            securityFooterController.setOnVisibilityChangedListener(visibilityListener)
-            fgsManagerFooterController.setOnVisibilityChangedListener(visibilityListener)
-        }
-        updateView()
-    }
+                fgsManagerFooterController
+                    .setCollapsed(securityFooter.visibility == View.VISIBLE)
+            }
+        securityFooterController.setOnVisibilityChangedListener(visibilityListener)
+        fgsManagerFooterController.setOnVisibilityChangedListener(visibilityListener)
 
-    private fun reformatForNewFooter(view: DualHeightHorizontalLinearLayout) {
-        // This is only necessary while things are flagged as the view could be attached in two
-        // different locations.
-        (view.layoutParams as LinearLayout.LayoutParams).apply {
-            bottomMargin = 0
-            width = 0
-            weight = 1f
-            marginEnd = resources.getDimensionPixelSize(R.dimen.new_qs_footer_action_inset)
-        }
-        view.alwaysSingleLine = true
+        updateView()
     }
 
     private fun updateView() {
@@ -237,10 +216,9 @@
         } else {
             userInfoController.removeCallback(onUserInfoChangedListener)
         }
-        if (featureFlags.isEnabled(Flags.NEW_FOOTER)) {
-            fgsManagerFooterController.setListening(listening)
-            securityFooterController.setListening(listening)
-        }
+
+        fgsManagerFooterController.setListening(listening)
+        securityFooterController.setListening(listening)
     }
 
     fun disable(state2: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index 4640205..56298fa 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -81,8 +81,6 @@
     private final QSPanelController mQsPanelController;
     private final QuickQSPanelController mQuickQSPanelController;
     private final QuickStatusBarHeader mQuickStatusBarHeader;
-    private final QSFgsManagerFooter mFgsManagerFooter;
-    private final QSSecurityFooter mSecurityFooter;
     private final QS mQs;
 
     @Nullable
@@ -105,7 +103,7 @@
     private TouchAnimator mNonfirstPageAlphaAnimator;
     // TranslatesY the QS Tile layout using QS.getHeightDiff()
     private TouchAnimator mQSTileLayoutTranslatorAnimator;
-    // This animates fading of SecurityFooter and media divider
+    // This animates fading of media player
     private TouchAnimator mAllPagesDelayedAnimator;
     // Animator for brightness slider(s)
     @Nullable
@@ -146,7 +144,6 @@
     public QSAnimator(QS qs, QuickQSPanel quickPanel, QuickStatusBarHeader quickStatusBarHeader,
             QSPanelController qsPanelController,
             QuickQSPanelController quickQSPanelController, QSTileHost qsTileHost,
-            QSFgsManagerFooter fgsManagerFooter, QSSecurityFooter securityFooter,
             @Main Executor executor, TunerService tunerService,
             QSExpansionPathInterpolator qsExpansionPathInterpolator) {
         mQs = qs;
@@ -154,8 +151,6 @@
         mQsPanelController = qsPanelController;
         mQuickQSPanelController = quickQSPanelController;
         mQuickStatusBarHeader = quickStatusBarHeader;
-        mFgsManagerFooter = fgsManagerFooter;
-        mSecurityFooter = securityFooter;
         mHost = qsTileHost;
         mExecutor = executor;
         mTunerService = tunerService;
@@ -472,10 +467,8 @@
                     .setListener(this)
                     .build();
 
-            // Fade in the security footer and the divider as we reach the final position
+            // Fade in the media player as we reach the final position
             Builder builder = new Builder().setStartDelay(EXPANDED_TILE_DELAY);
-            builder.addFloat(mFgsManagerFooter.getView(), "alpha", 0, 1);
-            builder.addFloat(mSecurityFooter.getView(), "alpha", 0, 1);
             if (mQsPanelController.shouldUseHorizontalLayout()
                     && mQsPanelController.mMediaHost.hostView != null) {
                 builder.addFloat(mQsPanelController.mMediaHost.hostView, "alpha", 0, 1);
@@ -484,8 +477,6 @@
                 mQsPanelController.mMediaHost.hostView.setAlpha(1.0f);
             }
             mAllPagesDelayedAnimator = builder.build();
-            mAllViews.add(mFgsManagerFooter.getView());
-            mAllViews.add(mSecurityFooter.getView());
             translationYBuilder.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator());
             qqsTranslationYBuilder.setInterpolator(mQSExpansionPathInterpolator.getYInterpolator());
             translationXBuilder.setInterpolator(mQSExpansionPathInterpolator.getXInterpolator());
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 519ed5c..6658441 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -138,12 +138,8 @@
     }
 
     void updateResources(QSPanelController qsPanelController,
-            QuickStatusBarHeaderController quickStatusBarHeaderController,
-            boolean newFooter) {
-        int bottomPadding = 0;
-        if (newFooter) {
-            bottomPadding = getResources().getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
-        }
+            QuickStatusBarHeaderController quickStatusBarHeaderController) {
+        int bottomPadding = getResources().getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
         mQSPanelContainer.setPaddingRelative(
                 mQSPanelContainer.getPaddingStart(),
                 QSUtils.getQsHeaderSystemIconsAreaHeight(mContext),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index 61da182..7d61991 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -18,8 +18,6 @@
 
 import android.content.res.Configuration;
 
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.ViewController;
@@ -32,26 +30,23 @@
     private final QSPanelController mQsPanelController;
     private final QuickStatusBarHeaderController mQuickStatusBarHeaderController;
     private final ConfigurationController mConfigurationController;
-    private final boolean mNewFooter;
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
         @Override
         public void onConfigChanged(Configuration newConfig) {
-            mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController, mNewFooter);
+            mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController);
         }
     };
 
     @Inject
     QSContainerImplController(QSContainerImpl view, QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController,
-            ConfigurationController configurationController,
-            FeatureFlags featureFlags) {
+            ConfigurationController configurationController) {
         super(view);
         mQsPanelController = qsPanelController;
         mQuickStatusBarHeaderController = quickStatusBarHeaderController;
         mConfigurationController = configurationController;
-        mNewFooter = featureFlags.isEnabled(Flags.NEW_FOOTER);
     }
 
     @Override
@@ -65,7 +60,7 @@
 
     @Override
     protected void onViewAttached() {
-        mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController, mNewFooter);
+        mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController);
         mConfigurationController.addCallback(mConfigurationListener);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 795a606..4fa05c8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -211,7 +211,6 @@
                     }
         });
         mHeader = view.findViewById(R.id.header);
-        mQSPanelController.setHeaderContainer(view.findViewById(R.id.header_text_container));
         mFooter = qsFragmentComponent.getQSFooter();
 
         mQSContainerImplController = qsFragmentComponent.getQSContainerImplController();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 8c4dedc..9fbdd3c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -75,7 +75,6 @@
     @Nullable
     protected BrightnessSliderController mToggleSliderController;
 
-    private final H mHandler = new H();
     /** Whether or not the QS media player feature is enabled. */
     protected boolean mUsingMediaPlayer;
 
@@ -87,16 +86,9 @@
             new ArrayList<>();
 
     @Nullable
-    protected View mFgsManagerFooter;
-    @Nullable
-    protected View mSecurityFooter;
-
-    @Nullable
     protected View mFooter;
 
     @Nullable
-    private ViewGroup mHeaderContainer;
-    @Nullable
     private PageIndicator mFooterPageIndicator;
     private int mContentMarginStart;
     private int mContentMarginEnd;
@@ -112,7 +104,6 @@
     private float mSquishinessFraction = 1f;
     private final ArrayMap<View, Integer> mChildrenLayoutTop = new ArrayMap<>();
     private final Rect mClippingRect = new Rect();
-    private boolean mUseNewFooter = false;
     private ViewGroup mMediaHostView;
     private boolean mShouldMoveMediaOnExpansion = true;
 
@@ -156,10 +147,6 @@
         }
     }
 
-    void setUseNewFooter(boolean useNewFooter) {
-        mUseNewFooter = useNewFooter;
-    }
-
     protected void setHorizontalContentContainerClipping() {
         mHorizontalContentContainer.setClipChildren(true);
         mHorizontalContentContainer.setClipToPadding(false);
@@ -444,27 +431,6 @@
         }
     }
 
-    /** Switch the security footer between top and bottom of QS depending on orientation. */
-    public void switchSecurityFooter(boolean shouldUseSplitNotificationShade) {
-        if (mSecurityFooter == null) return;
-
-        if (!shouldUseSplitNotificationShade
-                && mContext.getResources().getConfiguration().orientation
-                == Configuration.ORIENTATION_LANDSCAPE && mHeaderContainer != null) {
-            // Adding the security view to the header, that enables us to avoid scrolling
-            switchToParent(mSecurityFooter, mHeaderContainer, 0);
-        } else {
-            // Add after the footer
-            int index;
-            if (mFgsManagerFooter != null) {
-                index = indexOfChild(mFgsManagerFooter);
-            } else {
-                index = indexOfChild(mFooter);
-            }
-            switchToParent(mSecurityFooter, this, index + 1);
-        }
-    }
-
     private void switchToParent(View child, ViewGroup parent, int index) {
         switchToParent(child, parent, index, getDumpableTag());
     }
@@ -609,38 +575,10 @@
         }
     }
 
-    /**
-     * Set the header container of quick settings.
-     */
-    public void setHeaderContainer(@NonNull ViewGroup headerContainer) {
-        mHeaderContainer = headerContainer;
-    }
-
     public boolean isListening() {
         return mListening;
     }
 
-    /**
-     * Set the security footer view and switch it into the right place
-     * @param view the view in question
-     * @param shouldUseSplitNotificationShade if QS is in split shade mode
-     */
-    public void setSecurityFooter(View view, boolean shouldUseSplitNotificationShade) {
-        mSecurityFooter = view;
-        switchSecurityFooter(shouldUseSplitNotificationShade);
-    }
-
-    /**
-     * Set the fgs manager footer view and switch it into the right place
-     * @param view the view in question
-     */
-    public void setFgsManagerFooter(View view) {
-        mFgsManagerFooter = view;
-        // Add after the footer
-        int index = indexOfChild(mFooter);
-        switchToParent(mFgsManagerFooter, this, index + 1);
-    }
-
     protected void setPageMargin(int pageMargin) {
         if (mTileLayout instanceof PagedTileLayout) {
             ((PagedTileLayout) mTileLayout).setPageMargin(pageMargin);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index b46f9c3..5670836 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -21,18 +21,14 @@
 import static com.android.systemui.qs.QSPanel.QS_SHOW_BRIGHTNESS;
 import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
 
-import android.annotation.NonNull;
 import android.content.res.Configuration;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewGroup;
 
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.media.MediaHierarchyManager;
 import com.android.systemui.media.MediaHost;
 import com.android.systemui.media.MediaHostState;
@@ -56,8 +52,6 @@
 @QSScope
 public class QSPanelController extends QSPanelControllerBase<QSPanel> {
 
-    private final QSFgsManagerFooter mQSFgsManagerFooter;
-    private final QSSecurityFooter mQsSecurityFooter;
     private final TunerService mTunerService;
     private final QSCustomizerController mQsCustomizerController;
     private final QSTileRevealController.Factory mQsTileRevealControllerFactory;
@@ -65,7 +59,6 @@
     private final BrightnessController mBrightnessController;
     private final BrightnessSliderController mBrightnessSliderController;
     private final BrightnessMirrorHandler mBrightnessMirrorHandler;
-    private final FeatureFlags mFeatureFlags;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
 
     private boolean mGridContentVisible = true;
@@ -75,11 +68,9 @@
         @Override
         public void onConfigurationChange(Configuration newConfig) {
             mView.updateResources();
-            mQsSecurityFooter.onConfigurationChanged();
             if (mView.isListening()) {
                 refreshAllTiles();
             }
-            mView.switchSecurityFooter(mShouldUseSplitNotificationShade);
         }
     };
 
@@ -94,8 +85,7 @@
     };
 
     @Inject
-    QSPanelController(QSPanel view, QSFgsManagerFooter qsFgsManagerFooter,
-            QSSecurityFooter qsSecurityFooter, TunerService tunerService,
+    QSPanelController(QSPanel view, TunerService tunerService,
             QSTileHost qstileHost, QSCustomizerController qsCustomizerController,
             @Named(QS_USING_MEDIA_PLAYER) boolean usingMediaPlayer,
             @Named(QS_PANEL) MediaHost mediaHost,
@@ -103,12 +93,10 @@
             DumpManager dumpManager, MetricsLogger metricsLogger, UiEventLogger uiEventLogger,
             QSLogger qsLogger, BrightnessController.Factory brightnessControllerFactory,
             BrightnessSliderController.Factory brightnessSliderFactory,
-            FalsingManager falsingManager, FeatureFlags featureFlags,
+            FalsingManager falsingManager,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
         super(view, qstileHost, qsCustomizerController, usingMediaPlayer, mediaHost,
                 metricsLogger, uiEventLogger, qsLogger, dumpManager);
-        mQSFgsManagerFooter = qsFgsManagerFooter;
-        mQsSecurityFooter = qsSecurityFooter;
         mTunerService = tunerService;
         mQsCustomizerController = qsCustomizerController;
         mQsTileRevealControllerFactory = qsTileRevealControllerFactory;
@@ -119,9 +107,7 @@
 
         mBrightnessController = brightnessControllerFactory.create(mBrightnessSliderController);
         mBrightnessMirrorHandler = new BrightnessMirrorHandler(mBrightnessController);
-        mFeatureFlags = featureFlags;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
-        view.setUseNewFooter(featureFlags.isEnabled(Flags.NEW_FOOTER));
     }
 
     @Override
@@ -132,7 +118,6 @@
         mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
         mQsCustomizerController.init();
         mBrightnessSliderController.init();
-        mQSFgsManagerFooter.init();
     }
 
     @Override
@@ -147,10 +132,6 @@
             refreshAllTiles();
         }
         mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
-        if (!mFeatureFlags.isEnabled(Flags.NEW_FOOTER)) {
-            mView.setSecurityFooter(mQsSecurityFooter.getView(), mShouldUseSplitNotificationShade);
-            mView.setFgsManagerFooter(mQSFgsManagerFooter.getView());
-        }
         switchTileLayout(true);
         mBrightnessMirrorHandler.onQsPanelAttached();
 
@@ -172,13 +153,6 @@
         super.onViewDetached();
     }
 
-    /**
-     * Set the header container of quick settings.
-     */
-    public void setHeaderContainer(@NonNull ViewGroup headerContainer) {
-        mView.setHeaderContainer(headerContainer);
-    }
-
     /** */
     public void setVisibility(int visibility) {
         mView.setVisibility(visibility);
@@ -191,11 +165,6 @@
             refreshAllTiles();
         }
 
-        if (!mFeatureFlags.isEnabled(Flags.NEW_FOOTER)) {
-            mQSFgsManagerFooter.setListening(listening);
-            mQsSecurityFooter.setListening(listening);
-        }
-
         // Set the listening as soon as the QS fragment starts listening regardless of the
         //expansion, so it will update the current brightness before the slider is visible.
         if (listening) {
@@ -224,8 +193,6 @@
     public void refreshAllTiles() {
         mBrightnessController.checkRestrictionAndSetEnabled();
         super.refreshAllTiles();
-        mQSFgsManagerFooter.refreshState();
-        mQsSecurityFooter.refreshState();
     }
 
     /** Start customizing the Quick Settings. */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
index c5ca285..264edb1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -70,7 +70,6 @@
     private View mDateView;
     // DateView next to clock. Visible on QQS
     private VariableDateView mClockDateView;
-    private View mSecurityHeaderView;
     private View mStatusIconsView;
     private View mContainer;
 
@@ -137,7 +136,6 @@
         mPrivacyChip = findViewById(R.id.privacy_chip);
         mDateView = findViewById(R.id.date);
         mClockDateView = findViewById(R.id.date_clock);
-        mSecurityHeaderView = findViewById(R.id.header_text_container);
         mClockIconsSeparator = findViewById(R.id.separator);
         mRightLayout = findViewById(R.id.rightLayout);
         mDateContainer = findViewById(R.id.date_container);
@@ -152,8 +150,6 @@
         updateResources();
         Configuration config = mContext.getResources().getConfiguration();
         setDatePrivacyContainersWidth(config.orientation == Configuration.ORIENTATION_LANDSCAPE);
-        setSecurityHeaderContainerVisibility(
-                config.orientation == Configuration.ORIENTATION_LANDSCAPE);
 
         // QS will always show the estimate, and BatteryMeterView handles the case where
         // it's unavailable or charging
@@ -207,8 +203,6 @@
         super.onConfigurationChanged(newConfig);
         updateResources();
         setDatePrivacyContainersWidth(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE);
-        setSecurityHeaderContainerVisibility(
-                newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE);
     }
 
     @Override
@@ -229,10 +223,6 @@
         mPrivacyContainer.setLayoutParams(lp);
     }
 
-    private void setSecurityHeaderContainerVisibility(boolean landscape) {
-        mSecurityHeaderView.setVisibility(landscape ? VISIBLE : GONE);
-    }
-
     private void updateBatteryMode() {
         if (mConfigShowBatteryEstimate && !mHasCenterCutout) {
             mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
@@ -337,7 +327,6 @@
             return;
         }
         TouchAnimator.Builder builder = new TouchAnimator.Builder()
-                .addFloat(mSecurityHeaderView, "alpha", 0, 1)
                 // These views appear on expanding down
                 .addFloat(mDateView, "alpha", 0, 0, 1)
                 .addFloat(mClockDateView, "alpha", 1, 0, 0)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
index 2780b16..aa505fb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSFragmentModule.java
@@ -22,13 +22,10 @@
 import android.content.Context;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewStub;
 
 import com.android.systemui.R;
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.dagger.qualifiers.RootView;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.privacy.OngoingPrivacyChip;
 import com.android.systemui.qs.FooterActionsView;
@@ -128,15 +125,7 @@
      * This will replace a ViewStub either in {@link QSFooterView} or in {@link QSContainerImpl}.
      */
     @Provides
-    static FooterActionsView providesQSFooterActionsView(@RootView View view,
-            FeatureFlags featureFlags) {
-        ViewStub stub;
-        if (featureFlags.isEnabled(Flags.NEW_FOOTER)) {
-            stub = view.requireViewById(R.id.container_stub);
-        } else {
-            stub = view.requireViewById(R.id.footer_stub);
-        }
-        stub.inflate();
+    static FooterActionsView providesQSFooterActionsView(@RootView View view) {
         return view.findViewById(R.id.qs_footer_actions);
     }
 
@@ -161,9 +150,10 @@
     @Named(QS_SECURITY_FOOTER_VIEW)
     static View providesQSSecurityFooterView(
             @QSThemedContext LayoutInflater layoutInflater,
-            QSPanel qsPanel
+            FooterActionsView footerActionsView
     ) {
-        return layoutInflater.inflate(R.layout.quick_settings_security_footer, qsPanel, false);
+        return layoutInflater.inflate(R.layout.quick_settings_security_footer, footerActionsView,
+                false);
     }
 
     /** */
@@ -200,8 +190,8 @@
     @Named(QS_FGS_MANAGER_FOOTER_VIEW)
     static View providesQSFgsManagerFooterView(
             @QSThemedContext LayoutInflater layoutInflater,
-            QSPanel qsPanel
+            FooterActionsView footerActionsView
     ) {
-        return layoutInflater.inflate(R.layout.fgs_footer, qsPanel, false);
+        return layoutInflater.inflate(R.layout.fgs_footer, footerActionsView, false);
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index 41e468d..50ee1f7 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -246,7 +246,9 @@
                     new ClipData.Item(uri));
             sharingIntent.setClipData(clipdata);
             sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
-            sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                    .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+
 
             // Make sure pending intents for the system user are still unique across users
             // by setting the (otherwise unused) request code to the current user id.
@@ -256,6 +258,7 @@
                     .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
                     .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 
+
             // cancel current pending intent (if any) since clipData isn't used for matching
             PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
                     context, 0, sharingChooserIntent,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 7239d0c..1df4091 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -102,12 +102,14 @@
             KeyguardStateController.class);
     private final KeyguardBypassController mKeyguardBypassController;
     private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>();
+    private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>();
     static {
         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_NONE);
         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_STOPPED);
         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED);
         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
-        PAUSED_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING);
+        CONNECTING_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING);
+        CONNECTING_MEDIA_STATES.add(PlaybackState.STATE_BUFFERING);
     }
 
     private final NotificationVisibilityProvider mVisibilityProvider;
@@ -363,7 +365,17 @@
      * @return true if playing
      */
     public static boolean isPlayingState(int state) {
-        return !PAUSED_MEDIA_STATES.contains(state);
+        return !PAUSED_MEDIA_STATES.contains(state)
+            && !CONNECTING_MEDIA_STATES.contains(state);
+    }
+
+    /**
+     * Check if a state should be considered as connecting
+     * @param state a PlaybackState
+     * @return true if connecting or buffering
+     */
+    public static boolean isConnectingState(int state) {
+        return CONNECTING_MEDIA_STATES.contains(state);
     }
 
     public void setUpWithPresenter(NotificationPresenter presenter) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index eaa66bb..633786f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -594,16 +594,13 @@
         } else {
             shouldClipOwnTop = view.showingPulsing();
         }
-        if (viewEnd > notificationClipEnd && !shouldClipOwnTop
-                && (mAmbientState.isShadeExpanded() || !isPinned)) {
-            int clipBottomAmount = (int) (viewEnd - notificationClipEnd);
-            if (isPinned) {
-                clipBottomAmount = Math.min(view.getIntrinsicHeight() - view.getCollapsedHeight(),
-                        clipBottomAmount);
+        if (!isPinned) {
+            if (viewEnd > notificationClipEnd && !shouldClipOwnTop) {
+                int clipBottomAmount = (int) (viewEnd - notificationClipEnd);
+                view.setClipBottomAmount(clipBottomAmount);
+            } else {
+                view.setClipBottomAmount(0);
             }
-            view.setClipBottomAmount(clipBottomAmount);
-        } else {
-            view.setClipBottomAmount(0);
         }
         if (shouldClipOwnTop) {
             return (int) (viewEnd - getTranslationY());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
index b95d153..7f3381c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java
@@ -34,10 +34,13 @@
 
     private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
     private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
+    private static final int TAG_ANIMATOR_BOTTOM_INSET = R.id.bottom_inset_animator_tag;
     private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
     private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
+    private static final int TAG_END_BOTTOM_INSET = R.id.bottom_inset_animator_end_value_tag;
     private static final int TAG_START_HEIGHT = R.id.height_animator_start_value_tag;
     private static final int TAG_START_TOP_INSET = R.id.top_inset_animator_start_value_tag;
+    private static final int TAG_START_BOTTOM_INSET = R.id.bottom_inset_animator_start_value_tag;
 
     // These are flags such that we can create masks for filtering.
 
@@ -96,12 +99,17 @@
     public boolean headsUpIsVisible;
 
     /**
-     * How much the child overlaps with the previous child on top. This is used to
-     * show the background properly when the child on top is translating away.
+     * How much the child overlaps on top with the child above.
      */
     public int clipTopAmount;
 
     /**
+     * How much the child overlaps on bottom with the child above. This is used to
+     * show the background properly when the child on top is translating away.
+     */
+    public int clipBottomAmount;
+
+    /**
      * The index of the view, only accounting for views not equal to GONE
      */
     public int notGoneIndex;
@@ -138,8 +146,8 @@
         if (view instanceof ExpandableView) {
             ExpandableView expandableView = (ExpandableView) view;
 
-            int height = expandableView.getActualHeight();
-            int newHeight = this.height;
+            final int height = expandableView.getActualHeight();
+            final int newHeight = this.height;
 
             // apply height
             if (height != newHeight) {
@@ -157,10 +165,14 @@
             expandableView.setBelowSpeedBump(this.belowSpeedBump);
 
             // apply clipping
-            float oldClipTopAmount = expandableView.getClipTopAmount();
+            final float oldClipTopAmount = expandableView.getClipTopAmount();
             if (oldClipTopAmount != this.clipTopAmount) {
                 expandableView.setClipTopAmount(this.clipTopAmount);
             }
+            final float oldClipBottomAmount = expandableView.getClipBottomAmount();
+            if (oldClipBottomAmount != this.clipBottomAmount) {
+                expandableView.setClipBottomAmount(this.clipBottomAmount);
+            }
 
             expandableView.setTransformingInShelf(false);
             expandableView.setInShelf(inShelf);
@@ -187,13 +199,20 @@
             abortAnimation(child, TAG_ANIMATOR_HEIGHT);
         }
 
-        // start top inset animation
+        // start clip top animation
         if (this.clipTopAmount != expandableView.getClipTopAmount()) {
-            startInsetAnimation(expandableView, properties);
+            startClipAnimation(expandableView, properties, /* clipTop */true);
         } else {
             abortAnimation(child, TAG_ANIMATOR_TOP_INSET);
         }
 
+        // start clip bottom animation
+        if (this.clipBottomAmount != expandableView.getClipBottomAmount()) {
+            startClipAnimation(expandableView, properties, /* clipTop */ false);
+        } else {
+            abortAnimation(child, TAG_ANIMATOR_BOTTOM_INSET);
+        }
+
         // start dimmed animation
         expandableView.setDimmed(this.dimmed, animationFilter.animateDimmed);
 
@@ -301,16 +320,20 @@
         child.setActualHeightAnimating(true);
     }
 
-    private void startInsetAnimation(final ExpandableView child, AnimationProperties properties) {
-        Integer previousStartValue = getChildTag(child, TAG_START_TOP_INSET);
-        Integer previousEndValue = getChildTag(child, TAG_END_TOP_INSET);
-        int newEndValue = this.clipTopAmount;
+    private void startClipAnimation(final ExpandableView child, AnimationProperties properties,
+            boolean clipTop) {
+        Integer previousStartValue = getChildTag(child,
+                clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET);
+        Integer previousEndValue = getChildTag(child,
+                clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET);
+        int newEndValue = clipTop ? this.clipTopAmount : this.clipBottomAmount;
         if (previousEndValue != null && previousEndValue == newEndValue) {
             return;
         }
-        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TOP_INSET);
+        ValueAnimator previousAnimator = getChildTag(child,
+                clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET);
         AnimationFilter filter = properties.getAnimationFilter();
-        if (!filter.animateTopInset) {
+        if (clipTop && !filter.animateTopInset || !clipTop) {
             // just a local update was performed
             if (previousAnimator != null) {
                 // we need to increase all animation keyframes of the previous animator by the
@@ -319,22 +342,28 @@
                 int relativeDiff = newEndValue - previousEndValue;
                 int newStartValue = previousStartValue + relativeDiff;
                 values[0].setIntValues(newStartValue, newEndValue);
-                child.setTag(TAG_START_TOP_INSET, newStartValue);
-                child.setTag(TAG_END_TOP_INSET, newEndValue);
+                child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, newStartValue);
+                child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, newEndValue);
                 previousAnimator.setCurrentPlayTime(previousAnimator.getCurrentPlayTime());
                 return;
             } else {
                 // no new animation needed, let's just apply the value
-                child.setClipTopAmount(newEndValue);
+                if (clipTop) {
+                    child.setClipTopAmount(newEndValue);
+                } else {
+                    child.setClipBottomAmount(newEndValue);
+                }
                 return;
             }
         }
 
-        ValueAnimator animator = ValueAnimator.ofInt(child.getClipTopAmount(), newEndValue);
-        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-            @Override
-            public void onAnimationUpdate(ValueAnimator animation) {
+        ValueAnimator animator = ValueAnimator.ofInt(
+                clipTop ? child.getClipTopAmount() : child.getClipBottomAmount(), newEndValue);
+        animator.addUpdateListener(animation -> {
+            if (clipTop) {
                 child.setClipTopAmount((int) animation.getAnimatedValue());
+            } else {
+                child.setClipBottomAmount((int) animation.getAnimatedValue());
             }
         });
         animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
@@ -353,15 +382,16 @@
         animator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                child.setTag(TAG_ANIMATOR_TOP_INSET, null);
-                child.setTag(TAG_START_TOP_INSET, null);
-                child.setTag(TAG_END_TOP_INSET, null);
+                child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET, null);
+                child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, null);
+                child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, null);
             }
         });
         startAnimator(animator, listener);
-        child.setTag(TAG_ANIMATOR_TOP_INSET, animator);
-        child.setTag(TAG_START_TOP_INSET, child.getClipTopAmount());
-        child.setTag(TAG_END_TOP_INSET, newEndValue);
+        child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET:TAG_ANIMATOR_BOTTOM_INSET, animator);
+        child.setTag(clipTop ? TAG_START_TOP_INSET: TAG_START_BOTTOM_INSET,
+                clipTop ? child.getClipTopAmount() : child.getClipBottomAmount());
+        child.setTag(clipTop ? TAG_END_TOP_INSET: TAG_END_BOTTOM_INSET, newEndValue);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index e1f8c35..c097133 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -192,6 +192,7 @@
         float clipStart = 0;
         int childCount = algorithmState.visibleChildren.size();
         boolean firstHeadsUp = true;
+        float firstHeadsUpEnd = 0;
         for (int i = 0; i < childCount; i++) {
             ExpandableView child = algorithmState.visibleChildren.get(i);
             ExpandableViewState state = child.getViewState();
@@ -203,14 +204,18 @@
             float newNotificationEnd = newYTranslation + newHeight;
             boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && child.isPinned();
             if (mClipNotificationScrollToTop
-                    && (!state.inShelf || (isHeadsUp && !firstHeadsUp))
-                    && newYTranslation < clipStart
+                    && ((isHeadsUp && !firstHeadsUp) || child.isHeadsUpAnimatingAway())
+                    && newNotificationEnd > firstHeadsUpEnd
                     && !ambientState.isShadeExpanded()) {
-                // The previous view is overlapping on top, clip!
-                float overlapAmount = clipStart - newYTranslation;
-                state.clipTopAmount = (int) overlapAmount;
+                // The bottom of this view is peeking out from under the previous view.
+                // Clip the part that is peeking out.
+                float overlapAmount = newNotificationEnd - firstHeadsUpEnd;
+                state.clipBottomAmount = (int) overlapAmount;
             } else {
-                state.clipTopAmount = 0;
+                state.clipBottomAmount = 0;
+            }
+            if (firstHeadsUp) {
+                firstHeadsUpEnd = newNotificationEnd;
             }
             if (isHeadsUp) {
                 firstHeadsUp = false;
@@ -635,8 +640,6 @@
                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
                     // the top most z-position
                     childState.height = row.getIntrinsicHeight();
-                    childState.yTranslation = Math.min(topState.yTranslation + topState.height
-                            - childState.height, childState.yTranslation);
                 }
 
                 // heads up notification show and this row is the top entry of heads up
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt
index 745228e..491e9fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQSContainerController.kt
@@ -168,14 +168,7 @@
     private fun updateBottomSpacing() {
         val (containerPadding, notificationsMargin) = calculateBottomSpacing()
         var qsScrollPaddingBottom = 0
-        val newFooter = featureFlags.isEnabled(Flags.NEW_FOOTER)
-        if (!newFooter && !(splitShadeEnabled || isQSCustomizing || isQSDetailShowing ||
-                        isGestureNavigation || taskbarVisible)) {
-            // no taskbar, portrait, navigation buttons enabled:
-            // padding is needed so QS can scroll up over bottom insets - to reach the point when
-            // the whole QS is above bottom insets
-            qsScrollPaddingBottom = bottomStableInsets
-        } else if (newFooter && !(isQSCustomizing || isQSDetailShowing)) {
+        if (!(isQSCustomizing || isQSDetailShowing)) {
             // With the new footer, we also want this padding in the bottom in these cases
             qsScrollPaddingBottom = if (splitShadeEnabled) {
                 notificationsMargin - scrimShadeBottomMargin
@@ -185,11 +178,7 @@
         }
         mView.setPadding(0, 0, 0, containerPadding)
         mView.setNotificationsMarginBottom(notificationsMargin)
-        if (newFooter) {
-            mView.setQSContainerPaddingBottom(qsScrollPaddingBottom)
-        } else {
-            mView.setQSScrollPaddingBottom(qsScrollPaddingBottom)
-        }
+        mView.setQSContainerPaddingBottom(qsScrollPaddingBottom)
     }
 
     private fun calculateBottomSpacing(): Pair<Int, Int> {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
index 7caea06..2446cf7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationsQuickSettingsContainer.java
@@ -103,16 +103,6 @@
         mStackScroller.setLayoutParams(params);
     }
 
-    public void setQSScrollPaddingBottom(int paddingBottom) {
-        if (mQSScrollView != null) {
-            mQSScrollView.setPaddingRelative(
-                    mQSScrollView.getPaddingLeft(),
-                    mQSScrollView.getPaddingTop(),
-                    mQSScrollView.getPaddingRight(),
-                    paddingBottom);
-        }
-    }
-
     public void setQSContainerPaddingBottom(int paddingBottom) {
         if (mQSContainer != null) {
             mQSContainer.setPadding(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index 6fe92fa..87ca942 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -489,7 +489,7 @@
             ExpandableNotificationRow row,
             boolean animate,
             boolean isActivityIntent) {
-        mLogger.logStartNotificationIntent(entry.getKey(), intent);
+        mLogger.logStartNotificationIntent(entry.getKey());
         try {
             Runnable onFinishAnimationCallback = animate
                     ? () -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry)
@@ -513,8 +513,10 @@
                                 mKeyguardStateController.isShowing(),
                                 eventTime)
                                 : getActivityOptions(mCentralSurfaces.getDisplayId(), adapter);
-                        return intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
+                        int result = intent.sendAndReturnResult(mContext, 0, fillInIntent, null,
                                 null, null, options);
+                        mLogger.logSendPendingIntent(entry.getKey(), intent, result);
+                        return result;
                     });
         } catch (PendingIntent.CanceledException e) {
             // the stack trace isn't very helpful here.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
index d118747..2fbe520 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt
@@ -32,7 +32,7 @@
         buffer.log(TAG, DEBUG, {
             str1 = key
         }, {
-            "(1/4) onNotificationClicked: $str1"
+            "(1/5) onNotificationClicked: $str1"
         })
     }
 
@@ -40,7 +40,7 @@
         buffer.log(TAG, DEBUG, {
             str1 = key
         }, {
-            "(2/4) handleNotificationClickAfterKeyguardDismissed: $str1"
+            "(2/5) handleNotificationClickAfterKeyguardDismissed: $str1"
         })
     }
 
@@ -48,16 +48,25 @@
         buffer.log(TAG, DEBUG, {
             str1 = key
         }, {
-            "(3/4) handleNotificationClickAfterPanelCollapsed: $str1"
+            "(3/5) handleNotificationClickAfterPanelCollapsed: $str1"
         })
     }
 
-    fun logStartNotificationIntent(key: String, pendingIntent: PendingIntent) {
+    fun logStartNotificationIntent(key: String) {
+        buffer.log(TAG, INFO, {
+            str1 = key
+        }, {
+            "(4/5) startNotificationIntent: $str1"
+        })
+    }
+
+    fun logSendPendingIntent(key: String, pendingIntent: PendingIntent, result: Int) {
         buffer.log(TAG, INFO, {
             str1 = key
             str2 = pendingIntent.intent.toString()
+            int1 = result
         }, {
-            "(4/4) Starting $str2 for notification $str1"
+            "(5/5) Started intent $str2 for notification $str1 with result code $int1"
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index fe08fb0..217a613 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -1215,7 +1215,8 @@
                 }
                 // Use broadcast instead of ShadeController, as this dialog may have started in
                 // another process and normal dagger bindings are not available
-                mBroadcastSender.closeSystemDialogs();
+                mBroadcastSender.sendBroadcastAsUser(
+                        new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT);
                 getContext().startActivityAsUser(
                         CreateUserActivity.createIntentForStart(getContext()),
                         mUserTracker.getUserHandle());
diff --git a/packages/SystemUI/src/com/android/systemui/util/DualHeightHorizontalLinearLayout.kt b/packages/SystemUI/src/com/android/systemui/util/DualHeightHorizontalLinearLayout.kt
deleted file mode 100644
index cfceefa..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/DualHeightHorizontalLinearLayout.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2021 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.systemui.util
-
-import android.content.Context
-import android.content.res.Configuration
-import android.util.AttributeSet
-import android.util.DisplayMetrics
-import android.util.TypedValue
-import android.widget.LinearLayout
-import android.widget.TextView
-import com.android.systemui.R
-
-/**
- * Horizontal [LinearLayout] to contain some text.
- *
- * The height of this container can alternate between two different heights, depending on whether
- * the text takes one line or more.
- *
- * When the text takes multiple lines, it will use the values in the regular attributes (`padding`,
- * `layout_height`). The single line behavior must be set in XML.
- *
- * XML attributes for single line behavior:
- * * `systemui:textViewId`: set the id for the [TextView] that determines the height of the
- *   container
- * * `systemui:singleLineHeight`: sets the height of the view when the text takes up only one line.
- *   By default, it will use [getMinimumHeight].
- * * `systemui:singleLineVerticalPadding`: sets the padding (top and bottom) when then text takes up
- * only one line. By default, it is 0.
- *
- * All dimensions are updated when configuration changes.
- */
-class DualHeightHorizontalLinearLayout @JvmOverloads constructor(
-    context: Context,
-    attrs: AttributeSet? = null,
-    defStyleAttrs: Int = 0,
-    defStyleRes: Int = 0
-) : LinearLayout(context, attrs, defStyleAttrs, defStyleRes) {
-
-    private val singleLineHeightValue: TypedValue?
-    private var singleLineHeightPx = 0
-
-    private val singleLineVerticalPaddingValue: TypedValue?
-    private var singleLineVerticalPaddingPx = 0
-
-    private val textViewId: Int
-    private var textView: TextView? = null
-
-    private val displayMetrics: DisplayMetrics
-        get() = context.resources.displayMetrics
-
-    private var initialPadding = mPaddingTop // All vertical padding is the same
-
-    private var originalMaxLines = 1
-    var alwaysSingleLine: Boolean = false
-        set(value) {
-            field = value
-            if (field) {
-                textView?.setSingleLine()
-            } else {
-                textView?.maxLines = originalMaxLines
-            }
-        }
-
-    init {
-        if (orientation != HORIZONTAL) {
-            throw IllegalStateException("This view should always have horizontal orientation")
-        }
-
-        val ta = context.obtainStyledAttributes(
-                attrs,
-                R.styleable.DualHeightHorizontalLinearLayout, defStyleAttrs, defStyleRes
-        )
-
-        val tempHeight = TypedValue()
-        singleLineHeightValue = if (
-                ta.hasValue(R.styleable.DualHeightHorizontalLinearLayout_singleLineHeight)
-        ) {
-            ta.getValue(R.styleable.DualHeightHorizontalLinearLayout_singleLineHeight, tempHeight)
-            tempHeight
-        } else {
-            null
-        }
-
-        val tempPadding = TypedValue()
-        singleLineVerticalPaddingValue = if (
-                ta.hasValue(R.styleable.DualHeightHorizontalLinearLayout_singleLineVerticalPadding)
-        ) {
-            ta.getValue(
-                    R.styleable.DualHeightHorizontalLinearLayout_singleLineVerticalPadding,
-                    tempPadding
-            )
-            tempPadding
-        } else {
-            null
-        }
-
-        textViewId = ta.getResourceId(R.styleable.DualHeightHorizontalLinearLayout_textViewId, 0)
-
-        ta.recycle()
-    }
-
-    init {
-        updateResources()
-    }
-
-    override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
-        super.setPadding(left, top, right, bottom)
-        initialPadding = top
-    }
-
-    override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
-        super.setPaddingRelative(start, top, end, bottom)
-        initialPadding = top
-    }
-
-    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
-        textView?.let { tv ->
-            if (tv.lineCount < 2 || alwaysSingleLine) {
-                setMeasuredDimension(measuredWidth, singleLineHeightPx)
-                mPaddingBottom = 0
-                mPaddingTop = 0
-            } else {
-                mPaddingBottom = initialPadding
-                mPaddingTop = initialPadding
-            }
-        }
-    }
-
-    override fun onFinishInflate() {
-        super.onFinishInflate()
-        textView = findViewById<TextView>(textViewId)?.also {
-            originalMaxLines = it.maxLines
-        }
-    }
-
-    override fun onConfigurationChanged(newConfig: Configuration?) {
-        super.onConfigurationChanged(newConfig)
-        updateResources()
-    }
-
-    override fun setOrientation(orientation: Int) {
-        if (orientation == VERTICAL) {
-            throw IllegalStateException("This view should always have horizontal orientation")
-        }
-        super.setOrientation(orientation)
-    }
-
-    private fun updateResources() {
-        updateDimensionValue(singleLineHeightValue, minimumHeight, ::singleLineHeightPx::set)
-        updateDimensionValue(singleLineVerticalPaddingValue, 0, ::singleLineVerticalPaddingPx::set)
-    }
-
-    private inline fun updateDimensionValue(
-        tv: TypedValue?,
-        defaultValue: Int,
-        propertySetter: (Int) -> Unit
-    ) {
-        val value = tv?.let {
-            if (it.resourceId != 0) {
-                context.resources.getDimensionPixelSize(it.resourceId)
-            } else {
-                it.getDimension(displayMetrics).toInt()
-            }
-        } ?: defaultValue
-        propertySetter(value)
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 4d34aa3..b70220d 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -576,7 +576,9 @@
             @Override
             public void onEntryRemoved(NotificationEntry entry,
                     @NotifCollection.CancellationReason int reason) {
-                BubblesManager.this.onEntryRemoved(entry);
+                if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL) {
+                    BubblesManager.this.onEntryRemoved(entry);
+                }
             }
 
             @Override
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
index 4cca38a..cc606de 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
@@ -88,6 +88,7 @@
     bouncerFullyShown = false,
     faceAuthenticated = false,
     faceDisabled = false,
+    goingToSleep = false,
     keyguardAwake = false,
     keyguardGoingAway = false,
     listeningForFaceAssistant = false,
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 26296d6..67fe044 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -52,6 +52,7 @@
     @Mock lateinit var panel: MediaControlPanel
     @Mock lateinit var visualStabilityProvider: VisualStabilityProvider
     @Mock lateinit var mediaHostStatesManager: MediaHostStatesManager
+    @Mock lateinit var mediaHostState: MediaHostState
     @Mock lateinit var activityStarter: ActivityStarter
     @Mock @Main private lateinit var executor: DelayableExecutor
     @Mock lateinit var mediaDataManager: MediaDataManager
@@ -188,4 +189,40 @@
 
         verify(logger).logCarouselSettings()
     }
+
+    @Test
+    fun testLocationChangeQs_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_QS,
+            mediaHostState,
+            animate = false)
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QS)
+    }
+
+    @Test
+    fun testLocationChangeQqs_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_QQS,
+            mediaHostState,
+            animate = false)
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_QQS)
+    }
+
+    @Test
+    fun testLocationChangeLockscreen_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_LOCKSCREEN,
+            mediaHostState,
+            animate = false)
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_LOCKSCREEN)
+    }
+
+    @Test
+    fun testLocationChangeDream_logged() {
+        mediaCarouselController.onDesiredLocationChanged(
+            MediaHierarchyManager.LOCATION_DREAM_OVERLAY,
+            mediaHostState,
+            animate = false)
+        verify(logger).logCarouselPosition(MediaHierarchyManager.LOCATION_DREAM_OVERLAY)
+    }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index f2e3edb..9116716 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media
 
+import android.app.PendingIntent
 import org.mockito.Mockito.`when` as whenever
 import android.content.Intent
 import android.graphics.Color
@@ -749,6 +750,20 @@
     }
 
     @Test
+    fun tapContentView_isLogged() {
+        val pendingIntent = mock(PendingIntent::class.java)
+        val captor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
+        val data = mediaData.copy(clickIntent = pendingIntent)
+        player.attachPlayer(viewHolder)
+        player.bindPlayer(data, KEY)
+        verify(viewHolder.player).setOnClickListener(captor.capture())
+
+        captor.value.onClick(viewHolder.player)
+
+        verify(logger).logTapContentView(anyInt(), eq(PACKAGE), eq(instanceId))
+    }
+
+    @Test
     fun logSeek() {
         player.attachPlayer(viewHolder)
         player.bindPlayer(mediaData, KEY)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index b9ff877..1921cb6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -726,6 +726,25 @@
     }
 
     @Test
+    fun testPlaybackActions_connecting() {
+        whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
+        val stateActions = PlaybackState.ACTION_PLAY
+        val stateBuilder = PlaybackState.Builder()
+                .setState(PlaybackState.STATE_BUFFERING, 0, 10f)
+                .setActions(stateActions)
+        whenever(controller.playbackState).thenReturn(stateBuilder.build())
+
+        addNotificationAndLoad()
+
+        assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull()
+        val actions = mediaDataCaptor.value!!.semanticActions!!
+
+        assertThat(actions.playOrPause).isNotNull()
+        assertThat(actions.playOrPause!!.contentDescription).isEqualTo(
+                context.getString(R.string.controls_media_button_connecting))
+    }
+
+    @Test
     fun testPlaybackActions_reservedSpace() {
         val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4")
         whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
index e6f48ec..10eeb11 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -265,20 +265,58 @@
     }
 
     @Test
-    fun onAboutToConnectDeviceChangedWithNonNullParams() {
+    fun onAboutToConnectDeviceAdded_findsDeviceInfoFromAddress() {
         manager.onMediaDataLoaded(KEY, null, mediaData)
         // Run and reset the executors and listeners so we only focus on new events.
         fakeBgExecutor.runAllReady()
         fakeFgExecutor.runAllReady()
         reset(listener)
 
-        val deviceCallback = captureCallback()
+        // Ensure we'll get device info when using the address
+        val fullMediaDevice = mock(MediaDevice::class.java)
+        val address = "fakeAddress"
+        val nameFromDevice = "nameFromDevice"
+        val iconFromDevice = mock(Drawable::class.java)
+        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(fullMediaDevice)
+        whenever(fullMediaDevice.name).thenReturn(nameFromDevice)
+        whenever(fullMediaDevice.iconWithoutBackground).thenReturn(iconFromDevice)
+
         // WHEN the about-to-connect device changes to non-null
+        val deviceCallback = captureCallback()
+        val nameFromParam = "nameFromParam"
+        val iconFromParam = mock(Drawable::class.java)
+        deviceCallback.onAboutToConnectDeviceAdded(address, nameFromParam, iconFromParam)
+        assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
+
+        // THEN the about-to-connect device based on the address is returned
+        val data = captureDeviceData(KEY)
+        assertThat(data.enabled).isTrue()
+        assertThat(data.name).isEqualTo(nameFromDevice)
+        assertThat(data.name).isNotEqualTo(nameFromParam)
+        assertThat(data.icon).isEqualTo(iconFromDevice)
+        assertThat(data.icon).isNotEqualTo(iconFromParam)
+    }
+
+    @Test
+    fun onAboutToConnectDeviceAdded_cantFindDeviceInfoFromAddress() {
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        // Run and reset the executors and listeners so we only focus on new events.
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        reset(listener)
+
+        // Ensure we can't get device info based on the address
+        val address = "fakeAddress"
+        whenever(lmm.getMediaDeviceById(eq(address))).thenReturn(null)
+
+        // WHEN the about-to-connect device changes to non-null
+        val deviceCallback = captureCallback()
         val name = "AboutToConnectDeviceName"
         val mockIcon = mock(Drawable::class.java)
-        deviceCallback.onAboutToConnectDeviceChanged(name, mockIcon)
+        deviceCallback.onAboutToConnectDeviceAdded(address, name, mockIcon)
         assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
-        // THEN the about-to-connect device is returned
+
+        // THEN the about-to-connect device based on the parameters is returned
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isTrue()
         assertThat(data.name).isEqualTo(name)
@@ -286,21 +324,21 @@
     }
 
     @Test
-    fun onAboutToConnectDeviceChangedWithNullParams() {
+    fun onAboutToConnectDeviceAddedThenRemoved_usesNormalDevice() {
         manager.onMediaDataLoaded(KEY, null, mediaData)
         fakeBgExecutor.runAllReady()
         val deviceCallback = captureCallback()
         // First set a non-null about-to-connect device
-        deviceCallback.onAboutToConnectDeviceChanged(
-            "AboutToConnectDeviceName", mock(Drawable::class.java)
+        deviceCallback.onAboutToConnectDeviceAdded(
+            "fakeAddress", "AboutToConnectDeviceName", mock(Drawable::class.java)
         )
         // Run and reset the executors and listeners so we only focus on new events.
         fakeBgExecutor.runAllReady()
         fakeFgExecutor.runAllReady()
         reset(listener)
 
-        // WHEN the about-to-connect device changes to null
-        deviceCallback.onAboutToConnectDeviceChanged(null, null)
+        // WHEN hasDevice switches to false
+        deviceCallback.onAboutToConnectDeviceRemoved()
         assertThat(fakeFgExecutor.runAllReady()).isEqualTo(1)
         // THEN the normal device is returned
         val data = captureDeviceData(KEY)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt
index 88c4514..27c039d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerTest.kt
@@ -24,7 +24,7 @@
 import android.media.AudioDeviceInfo
 import android.media.AudioManager
 import android.media.AudioManager.MuteAwaitConnectionCallback.EVENT_CONNECTION
-import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.filters.SmallTest
 import com.android.settingslib.media.DeviceIconUtil
 import com.android.settingslib.media.LocalMediaManager
 import com.android.systemui.R
@@ -95,7 +95,7 @@
 
         muteAwaitConnectionManager.startListening()
 
-        verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any())
+        verify(localMediaManager, never()).dispatchAboutToConnectDeviceAdded(any(), any(), any())
     }
 
     @Test
@@ -104,7 +104,9 @@
 
         muteAwaitConnectionManager.startListening()
 
-        verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), eq(icon))
+        verify(localMediaManager).dispatchAboutToConnectDeviceAdded(
+            eq(DEVICE_ADDRESS), eq(DEVICE_NAME), eq(icon)
+        )
     }
 
     @Test
@@ -114,7 +116,7 @@
 
         muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_UNKNOWN))
 
-        verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any())
+        verify(localMediaManager, never()).dispatchAboutToConnectDeviceAdded(any(), any(), any())
     }
 
     @Test
@@ -125,7 +127,9 @@
 
         muteAwaitListener.onMutedUntilConnection(DEVICE, intArrayOf(USAGE_MEDIA))
 
-        verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(DEVICE_NAME), eq(icon))
+        verify(localMediaManager).dispatchAboutToConnectDeviceAdded(
+            eq(DEVICE_ADDRESS), eq(DEVICE_NAME), eq(icon)
+        )
     }
 
     @Test
@@ -135,7 +139,7 @@
 
         muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_MEDIA))
 
-        verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any())
+        verify(localMediaManager, never()).dispatchAboutToConnectDeviceAdded(any(), any(), any())
     }
 
     @Test
@@ -155,7 +159,7 @@
         )
         muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, otherDevice, intArrayOf(USAGE_MEDIA))
 
-        verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any())
+        verify(localMediaManager, never()).dispatchAboutToConnectDeviceAdded(any(), any(), any())
     }
 
     @Test
@@ -167,7 +171,7 @@
 
         muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_UNKNOWN))
 
-        verify(localMediaManager, never()).dispatchAboutToConnectDeviceChanged(any(), any())
+        verify(localMediaManager, never()).dispatchAboutToConnectDeviceAdded(any(), any(), any())
     }
 
     @Test
@@ -179,7 +183,7 @@
 
         muteAwaitListener.onUnmutedEvent(EVENT_CONNECTION, DEVICE, intArrayOf(USAGE_MEDIA))
 
-        verify(localMediaManager).dispatchAboutToConnectDeviceChanged(eq(null), eq(null))
+        verify(localMediaManager).dispatchAboutToConnectDeviceRemoved()
     }
 
     private fun getMuteAwaitListener(): AudioManager.MuteAwaitConnectionCallback {
@@ -191,11 +195,12 @@
     }
 }
 
+private const val DEVICE_ADDRESS = "DeviceAddress"
 private const val DEVICE_NAME = "DeviceName"
 private val DEVICE = AudioDeviceAttributes(
         AudioDeviceAttributes.ROLE_OUTPUT,
         AudioDeviceInfo.TYPE_USB_HEADSET,
-        "address",
+        DEVICE_ADDRESS,
         DEVICE_NAME,
         listOf(),
         listOf(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
index 7b17c36..35d0024 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
@@ -8,20 +8,20 @@
 import android.testing.ViewUtils
 import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewGroup
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.MetricsLogger
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.logging.testing.FakeMetricsLogger
 import com.android.systemui.R
 import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.globalactions.GlobalActionsDialogLite
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.MultiUserSwitchController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.UserInfoController
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.utils.leaks.LeakCheckedTest
 import com.google.common.truth.Truth.assertThat
@@ -29,10 +29,14 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Captor
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
@@ -65,11 +69,12 @@
     @Mock
     private lateinit var uiEventLogger: UiEventLogger
     @Mock
-    private lateinit var featureFlags: FeatureFlags
-    @Mock
     private lateinit var securityFooterController: QSSecurityFooter
     @Mock
     private lateinit var fgsManagerController: QSFgsManagerFooter
+    @Captor
+    private lateinit var visibilityChangedCaptor:
+        ArgumentCaptor<VisibilityChangedDispatcher.OnVisibilityChangedListener>
 
     private lateinit var controller: FooterActionsController
 
@@ -78,6 +83,8 @@
     private val falsingManager: FalsingManagerFake = FalsingManagerFake()
     private lateinit var testableLooper: TestableLooper
     private lateinit var fakeSettings: FakeSettings
+    private lateinit var securityFooter: View
+    private lateinit var fgsFooter: View
 
     @Before
     fun setUp() {
@@ -87,9 +94,14 @@
 
         whenever(multiUserSwitchControllerFactory.create(any()))
                 .thenReturn(multiUserSwitchController)
-        whenever(featureFlags.isEnabled(Flags.NEW_FOOTER)).thenReturn(false)
         whenever(globalActionsDialogProvider.get()).thenReturn(globalActionsDialog)
 
+        securityFooter = View(mContext)
+        fgsFooter = View(mContext)
+
+        whenever(securityFooterController.view).thenReturn(securityFooter)
+        whenever(fgsManagerController.view).thenReturn(fgsFooter)
+
         view = inflateView()
 
         controller = constructFooterActionsController(view)
@@ -107,6 +119,13 @@
     }
 
     @Test
+    fun testInitializesControllers() {
+        verify(multiUserSwitchController).init()
+        verify(fgsManagerController).init()
+        verify(securityFooterController).init()
+    }
+
+    @Test
     fun testLogPowerMenuClick() {
         controller.visible = true
         falsingManager.setFalseTap(false)
@@ -182,6 +201,10 @@
     @Test
     fun testCleanUpGAD() {
         reset(globalActionsDialogProvider)
+        // We are creating a new controller, so detach the views from it
+        (securityFooter.parent as ViewGroup).removeView(securityFooter)
+        (fgsFooter.parent as ViewGroup).removeView(fgsFooter)
+
         whenever(globalActionsDialogProvider.get()).thenReturn(globalActionsDialog)
         val view = inflateView()
         controller = constructFooterActionsController(view)
@@ -198,6 +221,80 @@
         verify(globalActionsDialog).destroy()
     }
 
+    @Test
+    fun testSeparatorVisibility_noneVisible_gone() {
+        verify(securityFooterController)
+            .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
+        val listener = visibilityChangedCaptor.value
+        val separator = controller.securityFootersSeparator
+
+        setVisibilities(securityFooterVisible = false, fgsFooterVisible = false, listener)
+        assertThat(separator.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun testSeparatorVisibility_onlySecurityFooterVisible_gone() {
+        verify(securityFooterController)
+            .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
+        val listener = visibilityChangedCaptor.value
+        val separator = controller.securityFootersSeparator
+
+        setVisibilities(securityFooterVisible = true, fgsFooterVisible = false, listener)
+        assertThat(separator.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun testSeparatorVisibility_onlyFgsFooterVisible_gone() {
+        verify(securityFooterController)
+            .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
+        val listener = visibilityChangedCaptor.value
+        val separator = controller.securityFootersSeparator
+
+        setVisibilities(securityFooterVisible = false, fgsFooterVisible = true, listener)
+        assertThat(separator.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun testSeparatorVisibility_bothVisible_visible() {
+        verify(securityFooterController)
+            .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
+        val listener = visibilityChangedCaptor.value
+        val separator = controller.securityFootersSeparator
+
+        setVisibilities(securityFooterVisible = true, fgsFooterVisible = true, listener)
+        assertThat(separator.visibility).isEqualTo(View.VISIBLE)
+    }
+
+    @Test
+    fun testFgsFooterCollapsed() {
+        verify(securityFooterController)
+            .setOnVisibilityChangedListener(capture(visibilityChangedCaptor))
+        val listener = visibilityChangedCaptor.value
+
+        val booleanCaptor = ArgumentCaptor.forClass(Boolean::class.java)
+
+        clearInvocations(fgsManagerController)
+        setVisibilities(securityFooterVisible = false, fgsFooterVisible = true, listener)
+        verify(fgsManagerController, atLeastOnce()).setCollapsed(capture(booleanCaptor))
+        assertThat(booleanCaptor.allValues.last()).isFalse()
+
+        clearInvocations(fgsManagerController)
+        setVisibilities(securityFooterVisible = true, fgsFooterVisible = true, listener)
+        verify(fgsManagerController, atLeastOnce()).setCollapsed(capture(booleanCaptor))
+        assertThat(booleanCaptor.allValues.last()).isTrue()
+    }
+
+    private fun setVisibilities(
+        securityFooterVisible: Boolean,
+        fgsFooterVisible: Boolean,
+        listener: VisibilityChangedDispatcher.OnVisibilityChangedListener
+    ) {
+        securityFooter.visibility = if (securityFooterVisible) View.VISIBLE else View.GONE
+        listener.onVisibilityChanged(securityFooter.visibility)
+        fgsFooter.visibility = if (fgsFooterVisible) View.VISIBLE else View.GONE
+        listener.onVisibilityChanged(fgsFooter.visibility)
+    }
+
     private fun inflateView(): FooterActionsView {
         return LayoutInflater.from(context)
                 .inflate(R.layout.footer_actions, null) as FooterActionsView
@@ -208,6 +305,6 @@
                 activityStarter, userManager, userTracker, userInfoController,
                 deviceProvisionedController, securityFooterController, fgsManagerController,
                 falsingManager, metricsLogger, globalActionsDialogProvider, uiEventLogger,
-                showPMLiteButton = true, fakeSettings, Handler(testableLooper.looper), featureFlags)
+                showPMLiteButton = true, fakeSettings, Handler(testableLooper.looper))
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
index bf82e90..489c8c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
@@ -59,22 +59,14 @@
     fun testContainerBottomPadding() {
         qsContainer.updateResources(
             qsPanelController,
-            quickStatusBarHeaderController,
-            /* newFooter */ false
-        )
-        verify(qsPanelContainer).setPaddingRelative(anyInt(), anyInt(), anyInt(), eq(0))
-
-        qsContainer.updateResources(
-            qsPanelController,
-            quickStatusBarHeaderController,
-            /* newFooter */ true
+            quickStatusBarHeaderController
         )
         verify(qsPanelContainer)
             .setPaddingRelative(
                 anyInt(),
                 anyInt(),
                 anyInt(),
-                eq(mContext.resources.getDimensionPixelSize(R.dimen.new_footer_height))
+                eq(mContext.resources.getDimensionPixelSize(R.dimen.footer_actions_height))
             )
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index 58a070d..689de50 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -6,7 +6,6 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.media.MediaHost
 import com.android.systemui.media.MediaHostState
 import com.android.systemui.plugins.FalsingManager
@@ -35,8 +34,6 @@
 class QSPanelControllerTest : SysuiTestCase() {
 
     @Mock private lateinit var qsPanel: QSPanel
-    @Mock private lateinit var qsFgsManagerFooter: QSFgsManagerFooter
-    @Mock private lateinit var qsSecurityFooter: QSSecurityFooter
     @Mock private lateinit var tunerService: TunerService
     @Mock private lateinit var qsTileHost: QSTileHost
     @Mock private lateinit var qsCustomizerController: QSCustomizerController
@@ -50,7 +47,6 @@
     @Mock private lateinit var brightnessSlider: BrightnessSliderController
     @Mock private lateinit var brightnessSliderFactory: BrightnessSliderController.Factory
     @Mock private lateinit var falsingManager: FalsingManager
-    @Mock private lateinit var featureFlags: FeatureFlags
     @Mock private lateinit var mediaHost: MediaHost
     @Mock private lateinit var tile: QSTile
     @Mock private lateinit var otherTile: QSTile
@@ -69,8 +65,6 @@
 
         controller = QSPanelController(
             qsPanel,
-            qsFgsManagerFooter,
-            qsSecurityFooter,
             tunerService,
             qsTileHost,
             qsCustomizerController,
@@ -84,7 +78,6 @@
             brightnessControllerFactory,
             brightnessSliderFactory,
             falsingManager,
-            featureFlags,
             statusBarKeyguardViewManager
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index ba02a82..e237a5c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -13,9 +13,6 @@
  */
 package com.android.systemui.qs
 
-import android.content.res.Configuration
-import android.content.res.Configuration.ORIENTATION_LANDSCAPE
-import android.content.res.Configuration.ORIENTATION_PORTRAIT
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
@@ -59,8 +56,6 @@
             mFooter = LinearLayout(mContext).apply { id = R.id.qs_footer }
             mQsPanel.addView(mFooter)
             mQsPanel.onFinishInflate()
-            mQsPanel.setSecurityFooter(View(mContext), false)
-            mQsPanel.setHeaderContainer(LinearLayout(mContext))
             // Provides a parent with non-zero size for QSPanel
             mParentView = FrameLayout(mContext).apply {
                 addView(mQsPanel)
@@ -69,49 +64,6 @@
     }
 
     @Test
-    fun testSecurityFooter_appearsOnBottomOnSplitShade() {
-        mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_LANDSCAPE))
-        mQsPanel.switchSecurityFooter(true)
-
-        mTestableLooper.runWithLooper {
-            mQsPanel.isExpanded = true
-        }
-
-        // After mFooter
-        assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(
-                mQsPanel.indexOfChild(mFooter) + 1
-        )
-    }
-
-    @Test
-    fun testSecurityFooter_appearsOnBottomIfPortrait() {
-        mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_PORTRAIT))
-        mQsPanel.switchSecurityFooter(false)
-
-        mTestableLooper.runWithLooper {
-            mQsPanel.isExpanded = true
-        }
-
-        // After mFooter
-        assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(
-                mQsPanel.indexOfChild(mFooter) + 1
-        )
-    }
-
-    @Test
-    fun testSecurityFooter_appearsOnTopIfSmallScreenAndLandscape() {
-        mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_LANDSCAPE))
-        mQsPanel.switchSecurityFooter(false)
-
-        mTestableLooper.runWithLooper {
-            mQsPanel.isExpanded = true
-        }
-
-        // -1 means that it is part of the mHeaderContainer
-        assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(-1)
-    }
-
-    @Test
     fun testHasCollapseAccessibilityAction() {
         val info = AccessibilityNodeInfo(mQsPanel)
         mQsPanel.onInitializeAccessibilityNodeInfo(info)
@@ -128,7 +80,4 @@
         mQsPanel.performAccessibilityAction(AccessibilityNodeInfo.ACTION_COLLAPSE, null)
         verify(mockRunnable).run()
     }
-
-    private fun getNewOrientationConfig(@Configuration.Orientation newOrientation: Int) =
-            context.resources.configuration.apply { orientation = newOrientation }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
index 60c2bde..a6a584d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
@@ -2,11 +2,9 @@
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.View
 import android.view.ViewGroup
 import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.FrameLayout
-import android.widget.LinearLayout
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.google.common.truth.Truth
@@ -37,8 +35,6 @@
             quickQSPanel.initialize()
 
             quickQSPanel.onFinishInflate()
-            quickQSPanel.setSecurityFooter(View(mContext), false)
-            quickQSPanel.setHeaderContainer(LinearLayout(mContext))
             // Provides a parent with non-zero size for QSPanel
             parentView = FrameLayout(mContext).apply {
                 addView(quickQSPanel)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt
index 05a21db..e2fabbd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationQSContainerControllerTest.kt
@@ -13,7 +13,6 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.navigationbar.NavigationModeController
 import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
 import com.android.systemui.recents.OverviewProxyService
@@ -114,25 +113,6 @@
     @Test
     fun testTaskbarVisibleInSplitShade() {
         enableSplitShade()
-        useNewFooter(false)
-
-        given(taskbarVisible = true,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
-                expectedNotificationsMargin = NOTIFICATIONS_MARGIN)
-
-        given(taskbarVisible = true,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = STABLE_INSET_BOTTOM,
-                expectedNotificationsMargin = NOTIFICATIONS_MARGIN)
-    }
-
-    @Test
-    fun testTaskbarVisibleInSplitShade_newFooter() {
-        enableSplitShade()
-        useNewFooter(true)
 
         given(taskbarVisible = true,
                 navigationMode = GESTURES_NAVIGATION,
@@ -153,25 +133,6 @@
     fun testTaskbarNotVisibleInSplitShade() {
         // when taskbar is not visible, it means we're on the home screen
         enableSplitShade()
-        useNewFooter(false)
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
-                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN)
-    }
-
-    @Test
-    fun testTaskbarNotVisibleInSplitShade_newFooter() {
-        // when taskbar is not visible, it means we're on the home screen
-        enableSplitShade()
-        useNewFooter(true)
 
         given(taskbarVisible = false,
                 navigationMode = GESTURES_NAVIGATION,
@@ -190,24 +151,6 @@
     @Test
     fun testTaskbarNotVisibleInSplitShadeWithCutout() {
         enableSplitShade()
-        useNewFooter(false)
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withCutout())
-        then(expectedContainerPadding = CUTOUT_HEIGHT)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withCutout().withStableBottom())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN)
-    }
-
-    @Test
-    fun testTaskbarNotVisibleInSplitShadeWithCutout_newFooter() {
-        enableSplitShade()
-        useNewFooter(true)
 
         given(taskbarVisible = false,
                 navigationMode = GESTURES_NAVIGATION,
@@ -226,23 +169,6 @@
     @Test
     fun testTaskbarVisibleInSinglePaneShade() {
         disableSplitShade()
-        useNewFooter(false)
-
-        given(taskbarVisible = true,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0)
-
-        given(taskbarVisible = true,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = STABLE_INSET_BOTTOM)
-    }
-
-    @Test
-    fun testTaskbarVisibleInSinglePaneShade_newFooter() {
-        disableSplitShade()
-        useNewFooter(true)
 
         given(taskbarVisible = true,
                 navigationMode = GESTURES_NAVIGATION,
@@ -260,28 +186,6 @@
     @Test
     fun testTaskbarNotVisibleInSinglePaneShade() {
         disableSplitShade()
-        useNewFooter(false)
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = emptyInsets())
-        then(expectedContainerPadding = 0)
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withCutout().withStableBottom())
-        then(expectedContainerPadding = CUTOUT_HEIGHT)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM)
-    }
-
-    @Test
-    fun testTaskbarNotVisibleInSinglePaneShade_newFooter() {
-        disableSplitShade()
-        useNewFooter(true)
 
         given(taskbarVisible = false,
                 navigationMode = GESTURES_NAVIGATION,
@@ -303,27 +207,6 @@
     fun testCustomizingInSinglePaneShade() {
         disableSplitShade()
         controller.setCustomizerShowing(true)
-        useNewFooter(false)
-
-        // always sets spacings to 0
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = 0)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = emptyInsets())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = 0)
-    }
-
-    @Test
-    fun testCustomizingInSinglePaneShade_newFooter() {
-        disableSplitShade()
-        controller.setCustomizerShowing(true)
-        useNewFooter(true)
 
         // always sets spacings to 0
         given(taskbarVisible = false,
@@ -343,27 +226,6 @@
     fun testDetailShowingInSinglePaneShade() {
         disableSplitShade()
         controller.setDetailShowing(true)
-        useNewFooter(false)
-
-        // always sets spacings to 0
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = 0)
-
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = emptyInsets())
-        then(expectedContainerPadding = 0,
-                expectedNotificationsMargin = 0)
-    }
-
-    @Test
-    fun testDetailShowingInSinglePaneShade_newFooter() {
-        disableSplitShade()
-        controller.setDetailShowing(true)
-        useNewFooter(true)
 
         // always sets spacings to 0
         given(taskbarVisible = false,
@@ -383,25 +245,6 @@
     fun testDetailShowingInSplitShade() {
         enableSplitShade()
         controller.setDetailShowing(true)
-        useNewFooter(false)
-
-        given(taskbarVisible = false,
-                navigationMode = GESTURES_NAVIGATION,
-                insets = windowInsets().withStableBottom())
-        then(expectedContainerPadding = 0)
-
-        // should not influence spacing
-        given(taskbarVisible = false,
-                navigationMode = BUTTONS_NAVIGATION,
-                insets = emptyInsets())
-        then(expectedContainerPadding = 0)
-    }
-
-    @Test
-    fun testDetailShowingInSplitShade_newFooter() {
-        enableSplitShade()
-        controller.setDetailShowing(true)
-        useNewFooter(true)
 
         given(taskbarVisible = false,
                 navigationMode = GESTURES_NAVIGATION,
@@ -531,7 +374,6 @@
     @Test
     fun testWindowInsetDebounce() {
         disableSplitShade()
-        useNewFooter(true)
 
         given(taskbarVisible = false,
             navigationMode = GESTURES_NAVIGATION,
@@ -596,13 +438,8 @@
         verify(notificationsQSContainer)
                 .setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
         verify(notificationsQSContainer).setNotificationsMarginBottom(expectedNotificationsMargin)
-        val newFooter = featureFlags.isEnabled(Flags.NEW_FOOTER)
-        if (newFooter) {
-            verify(notificationsQSContainer)
+        verify(notificationsQSContainer)
                     .setQSContainerPaddingBottom(expectedQsPadding)
-        } else {
-            verify(notificationsQSContainer).setQSScrollPaddingBottom(expectedQsPadding)
-        }
         Mockito.clearInvocations(notificationsQSContainer)
     }
 
@@ -620,10 +457,6 @@
         return this
     }
 
-    private fun useNewFooter(useNewFooter: Boolean) {
-        whenever(featureFlags.isEnabled(Flags.NEW_FOOTER)).thenReturn(useNewFooter)
-    }
-
     private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout {
         return constraintSetCaptor.value.getConstraint(id).layout
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
index 40657fb..ce7924a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/NewNotifPipelineBubblesTest.java
@@ -807,7 +807,7 @@
         assertTrue(mBubbleController.hasBubbles());
 
         // Removes the notification
-        mEntryListener.onEntryRemoved(mRow, 0);
+        mEntryListener.onEntryRemoved(mRow, REASON_APP_CANCEL);
         assertFalse(mBubbleController.hasBubbles());
     }
 
@@ -938,7 +938,7 @@
         mBubblesManager.handleDismissalInterception(groupSummary.getEntry());
 
         // WHEN the summary is cancelled by the app
-        mEntryListener.onEntryRemoved(groupSummary.getEntry(), 0);
+        mEntryListener.onEntryRemoved(groupSummary.getEntry(), REASON_APP_CANCEL);
 
         // THEN the summary and its children are removed from bubble data
         assertFalse(mBubbleData.hasBubbleInStackWithKey(groupedBubble.getEntry().getKey()));
diff --git a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
index 7e3ede1..d75d648 100644
--- a/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
+++ b/services/backup/backuplib/java/com/android/server/backup/transport/BackupTransportClient.java
@@ -34,6 +34,7 @@
 import java.util.List;
 import java.util.Queue;
 import java.util.Set;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
@@ -374,7 +375,8 @@
     private <T> T getFutureResult(AndroidFuture<T> future) {
         try {
             return future.get(600, TimeUnit.SECONDS);
-        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+        } catch (InterruptedException | ExecutionException | TimeoutException
+                | CancellationException e) {
             Slog.w(TAG, "Failed to get result from transport:", e);
             return null;
         } finally {
@@ -403,7 +405,11 @@
         void cancelActiveFutures() {
             synchronized (mActiveFuturesLock) {
                 for (AndroidFuture<?> future : mActiveFutures) {
-                    future.cancel(true);
+                    try {
+                        future.cancel(true);
+                    } catch (CancellationException ignored) {
+                        // This is expected, so ignore the exception.
+                    }
                 }
                 mActiveFutures.clear();
             }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 96761ca..07c2818 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -17215,17 +17215,17 @@
         }
 
         @Override
-        public boolean isUidCurrentlyInstrumented(int uid) {
+        public int getInstrumentationSourceUid(int uid) {
             synchronized (mProcLock) {
                 for (int i = mActiveInstrumentation.size() - 1; i >= 0; i--) {
                     ActiveInstrumentation activeInst = mActiveInstrumentation.get(i);
                     if (!activeInst.mFinished && activeInst.mTargetInfo != null
                             && activeInst.mTargetInfo.uid == uid) {
-                        return true;
+                        return activeInst.mSourceUid;
                     }
                 }
             }
-            return false;
+            return INVALID_UID;
         }
 
         @Override
diff --git a/services/core/java/com/android/server/am/AppBatteryTracker.java b/services/core/java/com/android/server/am/AppBatteryTracker.java
index 032b129..64ff532 100644
--- a/services/core/java/com/android/server/am/AppBatteryTracker.java
+++ b/services/core/java/com/android/server/am/AppBatteryTracker.java
@@ -49,6 +49,7 @@
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.os.AppBackgroundRestrictionsInfo;
 import android.os.AppBatteryStatsProto;
 import android.os.BatteryConsumer;
 import android.os.BatteryConsumer.Dimensions;
@@ -74,6 +75,7 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.am.AppBatteryTracker.AppBatteryPolicy;
 import com.android.server.am.AppRestrictionController.TrackerType;
 import com.android.server.am.AppRestrictionController.UidBatteryUsageProvider;
@@ -175,6 +177,12 @@
     @GuardedBy("mLock")
     private long mLastUidBatteryUsageStartTs;
 
+    /**
+     * elapseRealTime of last time the AppBatteryTracker is reported to statsd.
+     */
+    @GuardedBy("mLock")
+    private long mLastReportTime = 0;
+
     // For debug only.
     private final SparseArray<ImmutableBatteryUsage> mDebugUidPercentages = new SparseArray<>();
 
@@ -228,9 +236,92 @@
                     mBgHandler.postDelayed(mBgBatteryUsageStatsPolling, delay);
                 }
             }
+            logAppBatteryTrackerIfNeeded();
         }
     }
 
+    /**
+     * Log per-uid BatteryTrackerInfo to statsd every 24 hours (as the window specified in
+     * {@link AppBatteryPolicy#mBgCurrentDrainWindowMs})
+     */
+    private void logAppBatteryTrackerIfNeeded() {
+        final long now = SystemClock.elapsedRealtime();
+        synchronized (mLock) {
+            final AppBatteryPolicy bgPolicy = mInjector.getPolicy();
+            if (now - mLastReportTime < bgPolicy.mBgCurrentDrainWindowMs) {
+                return;
+            } else {
+                mLastReportTime = now;
+            }
+        }
+        updateBatteryUsageStatsIfNecessary(mInjector.currentTimeMillis(), true);
+        synchronized (mLock) {
+            for (int i = 0, size = mUidBatteryUsageInWindow.size(); i < size; i++) {
+                final int uid = mUidBatteryUsageInWindow.keyAt(i);
+                if (!UserHandle.isCore(uid) && !UserHandle.isApp(uid)) {
+                    continue;
+                }
+                if (BATTERY_USAGE_NONE.equals(mUidBatteryUsageInWindow.valueAt(i))) {
+                    continue;
+                }
+                FrameworkStatsLog.write(FrameworkStatsLog.APP_BACKGROUND_RESTRICTIONS_INFO,
+                        uid,
+                        FrameworkStatsLog
+                                .APP_BACKGROUND_RESTRICTIONS_INFO__RESTRICTION_LEVEL__LEVEL_UNKNOWN,
+                        FrameworkStatsLog
+                                .APP_BACKGROUND_RESTRICTIONS_INFO__THRESHOLD__THRESHOLD_UNKNOWN,
+                        FrameworkStatsLog
+                                .APP_BACKGROUND_RESTRICTIONS_INFO__TRACKER__UNKNOWN_TRACKER,
+                        null /*byte[] fgs_tracker_info*/,
+                        getBatteryTrackerInfoProtoLocked(uid) /*byte[] battery_tracker_info*/,
+                        null /*byte[] broadcast_events_tracker_info*/,
+                        null /*byte[] bind_service_events_tracker_info*/,
+                        FrameworkStatsLog
+                                .APP_BACKGROUND_RESTRICTIONS_INFO__EXEMPTION_REASON__REASON_UNKNOWN,
+                        FrameworkStatsLog
+                                .APP_BACKGROUND_RESTRICTIONS_INFO__OPT_LEVEL__UNKNOWN,
+                        FrameworkStatsLog
+                                .APP_BACKGROUND_RESTRICTIONS_INFO__TARGET_SDK__SDK_UNKNOWN,
+                        isLowRamDeviceStatic());
+            }
+        }
+    }
+
+    /**
+     * Get the BatteryTrackerInfo proto of a UID.
+     * @param uid
+     * @return byte array of the proto.
+     */
+     @NonNull byte[] getBatteryTrackerInfoProtoLocked(int uid) {
+        final ImmutableBatteryUsage temp = mUidBatteryUsageInWindow.get(uid);
+        if (temp == null) {
+            return new byte[0];
+        }
+        final BatteryUsage bgUsage = temp.calcPercentage(uid, mInjector.getPolicy());
+        final double allUsage = bgUsage.mPercentage[BatteryUsage.BATTERY_USAGE_INDEX_UNSPECIFIED]
+                + bgUsage.mPercentage[BatteryUsage.BATTERY_USAGE_INDEX_FOREGROUND]
+                + bgUsage.mPercentage[BatteryUsage.BATTERY_USAGE_INDEX_BACKGROUND]
+                + bgUsage.mPercentage[BatteryUsage.BATTERY_USAGE_INDEX_FOREGROUND_SERVICE]
+                + bgUsage.mPercentage[BatteryUsage.BATTERY_USAGE_INDEX_CACHED];
+        final double usageBackground =
+                bgUsage.mPercentage[BatteryUsage.BATTERY_USAGE_INDEX_BACKGROUND];
+        final double usageFgs =
+                bgUsage.mPercentage[BatteryUsage.BATTERY_USAGE_INDEX_FOREGROUND_SERVICE];
+        Slog.d(TAG, "getBatteryTrackerInfoProtoLocked uid:" + uid
+                + " allUsage:" + String.format("%4.2f%%", allUsage)
+                + " usageBackground:" + String.format("%4.2f%%", usageBackground)
+                + " usageFgs:" + String.format("%4.2f%%", usageFgs));
+        final ProtoOutputStream proto = new ProtoOutputStream();
+        proto.write(AppBackgroundRestrictionsInfo.BatteryTrackerInfo.BATTERY_24H,
+                allUsage * 10000);
+        proto.write(AppBackgroundRestrictionsInfo.BatteryTrackerInfo.BATTERY_USAGE_BACKGROUND,
+                usageBackground * 10000);
+        proto.write(AppBackgroundRestrictionsInfo.BatteryTrackerInfo.BATTERY_USAGE_FGS,
+                usageFgs * 10000);
+        proto.flush();
+        return proto.getBytes();
+    }
+
     @Override
     void onUserStarted(final @UserIdInt int userId) {
         synchronized (mLock) {
diff --git a/services/core/java/com/android/server/am/AppFGSTracker.java b/services/core/java/com/android/server/am/AppFGSTracker.java
index f8378c3..ddd2764 100644
--- a/services/core/java/com/android/server/am/AppFGSTracker.java
+++ b/services/core/java/com/android/server/am/AppFGSTracker.java
@@ -52,6 +52,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.SomeArgs;
 import com.android.server.am.AppFGSTracker.AppFGSPolicy;
 import com.android.server.am.AppFGSTracker.PackageDurations;
 import com.android.server.am.AppRestrictionController.TrackerType;
@@ -112,9 +113,14 @@
 
     @Override
     public void onForegroundServiceNotificationUpdated(String packageName, int uid,
-            int foregroundId) {
-        mHandler.obtainMessage(MyHandler.MSG_FOREGROUND_SERVICES_NOTIFICATION_UPDATED,
-                uid, foregroundId, packageName).sendToTarget();
+            int foregroundId, boolean canceling) {
+        final SomeArgs args = SomeArgs.obtain();
+        args.argi1 = uid;
+        args.argi2 = foregroundId;
+        args.arg1 = packageName;
+        args.arg2 = canceling ? Boolean.TRUE : Boolean.FALSE;
+        mHandler.obtainMessage(MyHandler.MSG_FOREGROUND_SERVICES_NOTIFICATION_UPDATED, args)
+                .sendToTarget();
     }
 
     private static class MyHandler extends Handler {
@@ -149,8 +155,10 @@
                             (String) msg.obj, msg.arg1, msg.arg2);
                     break;
                 case MSG_FOREGROUND_SERVICES_NOTIFICATION_UPDATED:
+                    final SomeArgs args = (SomeArgs) msg.obj;
                     mTracker.handleForegroundServiceNotificationUpdated(
-                            (String) msg.obj, msg.arg1, msg.arg2);
+                            (String) args.arg1, args.argi1, args.argi2, (Boolean) args.arg2);
+                    args.recycle();
                     break;
                 case MSG_CHECK_LONG_RUNNING_FGS:
                     mTracker.checkLongRunningFgs();
@@ -241,18 +249,18 @@
     }
 
     private void handleForegroundServiceNotificationUpdated(String packageName, int uid,
-            int notificationId) {
+            int notificationId, boolean canceling) {
         synchronized (mLock) {
             SparseBooleanArray notificationIDs = mFGSNotificationIDs.get(uid, packageName);
-            if (notificationId > 0) {
+            if (!canceling) {
                 if (notificationIDs == null) {
                     notificationIDs = new SparseBooleanArray();
                     mFGSNotificationIDs.put(uid, packageName, notificationIDs);
                 }
                 notificationIDs.put(notificationId, false);
-            } else if (notificationId < 0) {
+            } else {
                 if (notificationIDs != null) {
-                    final int indexOfKey = notificationIDs.indexOfKey(-notificationId);
+                    final int indexOfKey = notificationIDs.indexOfKey(notificationId);
                     if (indexOfKey >= 0) {
                         final boolean wasVisible = notificationIDs.valueAt(indexOfKey);
                         notificationIDs.removeAt(indexOfKey);
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index 708bd16..6f74359 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -1975,6 +1975,9 @@
         }
         try {
             final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */);
+            if (pkg == null || pkg.applicationInfo == null) {
+                return FrameworkStatsLog.APP_BACKGROUND_RESTRICTIONS_INFO__TARGET_SDK__SDK_UNKNOWN;
+            }
             final int targetSdk = pkg.applicationInfo.targetSdkVersion;
             if (targetSdk < Build.VERSION_CODES.S) {
                 return FrameworkStatsLog.APP_BACKGROUND_RESTRICTIONS_INFO__TARGET_SDK__SDK_PRE_S;
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index c70fe03..ade44ea 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -880,9 +880,9 @@
 
         // Ensure that broadcasts are only sent to other apps if they are explicitly marked as
         // exported, or are System level broadcasts
-        if (!skip && !filter.exported && mService.checkComponentPermission(null, r.callingPid,
-                r.callingUid, filter.receiverList.uid, filter.exported)
-                != PackageManager.PERMISSION_GRANTED) {
+        if (!skip && !filter.exported && !Process.isCoreUid(r.callingUid)
+                && filter.receiverList.uid != r.callingUid) {
+
             Slog.w(TAG, "Exported Denial: sending "
                     + r.intent.toString()
                     + ", action: " + r.intent.getAction()
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 639f56c..5a55b8b 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -1112,7 +1112,7 @@
                         foregroundNoti = localForegroundNoti; // save it for amending next time
 
                         signalForegroundServiceNotification(packageName, appInfo.uid,
-                                localForegroundId);
+                                localForegroundId, false /* canceling */);
 
                     } catch (RuntimeException e) {
                         Slog.w(TAG, "Error showing notification for service", e);
@@ -1147,17 +1147,18 @@
                 } catch (RuntimeException e) {
                     Slog.w(TAG, "Error canceling notification for service", e);
                 }
-                signalForegroundServiceNotification(packageName, appInfo.uid, -localForegroundId);
+                signalForegroundServiceNotification(packageName, appInfo.uid, localForegroundId,
+                        true /* canceling */);
             }
         });
     }
 
     private void signalForegroundServiceNotification(String packageName, int uid,
-            int foregroundId) {
+            int foregroundId, boolean canceling) {
         synchronized (ams) {
             for (int i = ams.mForegroundServiceStateListeners.size() - 1; i >= 0; i--) {
                 ams.mForegroundServiceStateListeners.get(i).onForegroundServiceNotificationUpdated(
-                        packageName, appInfo.uid, foregroundId);
+                        packageName, appInfo.uid, foregroundId, canceling);
             }
         }
     }
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 361629b..752e17e 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2367,7 +2367,8 @@
         ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class);
         boolean isSelfRequest = (filter & FILTER_BY_UID) != 0 && uid == Binder.getCallingUid();
         if (!isSelfRequest) {
-            boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(Binder.getCallingUid());
+            boolean isCallerInstrumented =
+                    ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID;
             boolean isCallerSystem = Binder.getCallingPid() == Process.myPid();
             boolean isCallerPermissionController;
             try {
@@ -6894,7 +6895,8 @@
     @Override
     public @Nullable RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage() {
         ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class);
-        boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(Binder.getCallingUid());
+        boolean isCallerInstrumented =
+                ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID;
         boolean isCallerSystem = Binder.getCallingPid() == Process.myPid();
         if (!isCallerSystem && !isCallerInstrumented) {
             return null;
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 5a105f5..5fcdc8a 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -70,6 +70,8 @@
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.DisplayGroupListener;
 import android.hardware.display.DisplayManagerInternal.DisplayTransactionListener;
+import android.hardware.display.DisplayManagerInternal.RefreshRateLimitation;
+import android.hardware.display.DisplayManagerInternal.RefreshRateRange;
 import android.hardware.display.DisplayViewport;
 import android.hardware.display.DisplayedContentSample;
 import android.hardware.display.DisplayedContentSamplingAttributes;
@@ -708,9 +710,6 @@
         synchronized (mSyncRoot) {
             final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId);
             if (display != null) {
-                // Do not let constrain be overwritten by override from WindowManager.
-                info.shouldConstrainMetricsForLauncher =
-                        display.getDisplayInfoLocked().shouldConstrainMetricsForLauncher;
                 if (display.setDisplayInfoOverrideFromWindowManagerLocked(info)) {
                     handleLogicalDisplayChangedLocked(display);
                 }
@@ -2212,21 +2211,6 @@
         }
     }
 
-    void setShouldConstrainMetricsForLauncher(boolean constrain) {
-        // Apply constrain for every display.
-        synchronized (mSyncRoot) {
-            int[] displayIds = mLogicalDisplayMapper.getDisplayIdsLocked(Process.myUid());
-            for (int i : displayIds) {
-                final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(i);
-                if (display == null) {
-                    return;
-                }
-                display.getDisplayInfoLocked().shouldConstrainMetricsForLauncher = constrain;
-                setDisplayInfoOverrideFromWindowManagerInternal(i, display.getDisplayInfoLocked());
-            }
-        }
-    }
-
     void setDockedAndIdleEnabled(boolean enabled, int displayId) {
         synchronized (mSyncRoot) {
             final DisplayPowerController displayPowerController = mDisplayPowerControllers.get(
diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
index bfdac57..7dce238 100644
--- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
+++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
@@ -60,8 +60,6 @@
                 return setDisplayModeDirectorLoggingEnabled(false);
             case "dwb-set-cct":
                 return setAmbientColorTemperatureOverride();
-            case "constrain-launcher-metrics":
-                return setConstrainLauncherMetrics();
             case "set-user-preferred-display-mode":
                 return setUserPreferredDisplayMode();
             case "clear-user-preferred-display-mode":
@@ -112,9 +110,6 @@
         pw.println("    Disable display mode director logging.");
         pw.println("  dwb-set-cct CCT");
         pw.println("    Sets the ambient color temperature override to CCT (use -1 to disable).");
-        pw.println("  constrain-launcher-metrics [true|false]");
-        pw.println("    Sets if Display#getRealSize and getRealMetrics should be constrained for ");
-        pw.println("    Launcher.");
         pw.println("  set-user-preferred-display-mode WIDTH HEIGHT REFRESH-RATE "
                 + "DISPLAY_ID (optional)");
         pw.println("    Sets the user preferred display mode which has fields WIDTH, HEIGHT and "
@@ -205,17 +200,6 @@
         return 0;
     }
 
-    private int setConstrainLauncherMetrics() {
-        String constrainText = getNextArg();
-        if (constrainText == null) {
-            getErrPrintWriter().println("Error: no value specified");
-            return 1;
-        }
-        boolean constrain = Boolean.parseBoolean(constrainText);
-        mService.setShouldConstrainMetricsForLauncher(constrain);
-        return 0;
-    }
-
     private int setUserPreferredDisplayMode() {
         final String widthText = getNextArg();
         if (widthText == null) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index b7ad4ed..a640497 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -233,8 +233,6 @@
                 info.displayCutout = mOverrideDisplayInfo.displayCutout;
                 info.logicalDensityDpi = mOverrideDisplayInfo.logicalDensityDpi;
                 info.roundedCorners = mOverrideDisplayInfo.roundedCorners;
-                info.shouldConstrainMetricsForLauncher =
-                        mOverrideDisplayInfo.shouldConstrainMetricsForLauncher;
             }
             mInfo.set(info);
         }
diff --git a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
new file mode 100644
index 0000000..d7563e0
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
@@ -0,0 +1,103 @@
+/*
+ * 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.hdmi;
+
+/**
+ * Action to query and track the audio status of the System Audio device when enabling or using
+ * Absolute Volume Control. Must be removed when AVC is disabled. Performs two main functions:
+ * 1. When enabling AVC: queries the starting audio status of the System Audio device and
+ *    enables the feature upon receiving a response.
+ * 2. While AVC is enabled: monitors <Report Audio Status> messages from the System Audio device and
+ *    notifies AudioService if the audio status changes.
+ */
+final class AbsoluteVolumeAudioStatusAction extends HdmiCecFeatureAction {
+    private static final String TAG = "AbsoluteVolumeAudioStatusAction";
+
+    private int mInitialAudioStatusRetriesLeft = 2;
+
+    private static final int STATE_WAIT_FOR_INITIAL_AUDIO_STATUS = 1;
+    private static final int STATE_MONITOR_AUDIO_STATUS = 2;
+
+    private final int mTargetAddress;
+
+    private AudioStatus mLastAudioStatus;
+
+    AbsoluteVolumeAudioStatusAction(HdmiCecLocalDevice source, int targetAddress) {
+        super(source);
+        mTargetAddress = targetAddress;
+    }
+
+    @Override
+    boolean start() {
+        mState = STATE_WAIT_FOR_INITIAL_AUDIO_STATUS;
+        sendGiveAudioStatus();
+        return true;
+    }
+
+    void updateVolume(int volumeIndex) {
+        mLastAudioStatus = new AudioStatus(volumeIndex, mLastAudioStatus.getMute());
+    }
+
+    private void sendGiveAudioStatus() {
+        addTimer(mState, HdmiConfig.TIMEOUT_MS);
+        sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(getSourceAddress(), mTargetAddress));
+    }
+
+    @Override
+    boolean processCommand(HdmiCecMessage cmd) {
+        switch (cmd.getOpcode()) {
+            case Constants.MESSAGE_REPORT_AUDIO_STATUS:
+                return handleReportAudioStatus(cmd);
+        }
+
+        return false;
+    }
+
+    private boolean handleReportAudioStatus(HdmiCecMessage cmd) {
+        if (mTargetAddress != cmd.getSource() || cmd.getParams().length == 0) {
+            return false;
+        }
+
+        boolean mute = HdmiUtils.isAudioStatusMute(cmd);
+        int volume = HdmiUtils.getAudioStatusVolume(cmd);
+        AudioStatus audioStatus = new AudioStatus(volume, mute);
+        if (mState == STATE_WAIT_FOR_INITIAL_AUDIO_STATUS) {
+            localDevice().getService().enableAbsoluteVolumeControl(audioStatus);
+            mState = STATE_MONITOR_AUDIO_STATUS;
+        } else if (mState == STATE_MONITOR_AUDIO_STATUS) {
+            if (audioStatus.getVolume() != mLastAudioStatus.getVolume()) {
+                localDevice().getService().notifyAvcVolumeChange(audioStatus.getVolume());
+            }
+            if (audioStatus.getMute() != mLastAudioStatus.getMute()) {
+                localDevice().getService().notifyAvcMuteChange(audioStatus.getMute());
+            }
+        }
+        mLastAudioStatus = audioStatus;
+
+        return true;
+    }
+
+    @Override
+    void handleTimerEvent(int state) {
+        if (mState != state) {
+            return;
+        } else if (mInitialAudioStatusRetriesLeft > 0) {
+            mInitialAudioStatusRetriesLeft--;
+            sendGiveAudioStatus();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
new file mode 100644
index 0000000..438c1ea
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapper.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hdmi;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.VolumeInfo;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Wrapper for {@link AudioDeviceVolumeManager}. Creates an instance of the class and directly
+ * passes method calls to that instance.
+ */
+public class AudioDeviceVolumeManagerWrapper
+        implements AudioDeviceVolumeManagerWrapperInterface {
+
+    private static final String TAG = "AudioDeviceVolumeManagerWrapper";
+
+    private final AudioDeviceVolumeManager mAudioDeviceVolumeManager;
+
+    public AudioDeviceVolumeManagerWrapper(Context context) {
+        mAudioDeviceVolumeManager = new AudioDeviceVolumeManager(context);
+    }
+
+    @Override
+    public void addOnDeviceVolumeBehaviorChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener)
+            throws SecurityException {
+        mAudioDeviceVolumeManager.addOnDeviceVolumeBehaviorChangedListener(executor, listener);
+    }
+
+    @Override
+    public void removeOnDeviceVolumeBehaviorChangedListener(
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener) {
+        mAudioDeviceVolumeManager.removeOnDeviceVolumeBehaviorChangedListener(listener);
+    }
+
+    @Override
+    public void setDeviceAbsoluteVolumeBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment) {
+        mAudioDeviceVolumeManager.setDeviceAbsoluteVolumeBehavior(device, volume, executor,
+                vclistener, handlesVolumeAdjustment);
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java
new file mode 100644
index 0000000..1a1d4c1
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AudioDeviceVolumeManagerWrapperInterface.java
@@ -0,0 +1,61 @@
+/*
+ * 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.hdmi;
+
+import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener;
+import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.VolumeInfo;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface with the methods from {@link AudioDeviceVolumeManager} used by the HDMI framework.
+ * Allows the class to be faked for tests.
+ */
+public interface AudioDeviceVolumeManagerWrapperInterface {
+
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#addOnDeviceVolumeBehaviorChangedListener(
+     * Executor, OnDeviceVolumeBehaviorChangedListener)}
+     */
+    void addOnDeviceVolumeBehaviorChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener);
+
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#removeOnDeviceVolumeBehaviorChangedListener(
+     * OnDeviceVolumeBehaviorChangedListener)}
+     */
+    void removeOnDeviceVolumeBehaviorChangedListener(
+            @NonNull AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener listener);
+
+    /**
+     * Wrapper for {@link AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior(
+     * AudioDeviceAttributes, VolumeInfo, Executor, OnAudioDeviceVolumeChangedListener, boolean)}
+     */
+    void setDeviceAbsoluteVolumeBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment);
+}
diff --git a/services/core/java/com/android/server/hdmi/AudioStatus.java b/services/core/java/com/android/server/hdmi/AudioStatus.java
new file mode 100644
index 0000000..a884ffb
--- /dev/null
+++ b/services/core/java/com/android/server/hdmi/AudioStatus.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hdmi;
+
+import android.annotation.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Immutable representation of the information in the [Audio Status] operand:
+ * volume status (0 <= N <= 100) and mute status (muted or unmuted).
+ */
+public class AudioStatus {
+    public static final int MAX_VOLUME = 100;
+    public static final int MIN_VOLUME = 0;
+
+    int mVolume;
+    boolean mMute;
+
+    public AudioStatus(int volume, boolean mute) {
+        mVolume = volume;
+        mMute = mute;
+    }
+
+    public int getVolume() {
+        return mVolume;
+    }
+
+    public boolean getMute() {
+        return mMute;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (!(obj instanceof AudioStatus)) {
+            return false;
+        }
+
+        AudioStatus other = (AudioStatus) obj;
+        return mVolume == other.mVolume
+                && mMute == other.mMute;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mVolume, mMute);
+    }
+
+    @Override
+    public String toString() {
+        return "AudioStatus mVolume:" + mVolume + " mMute:" + mMute;
+    }
+}
diff --git a/services/core/java/com/android/server/hdmi/Constants.java b/services/core/java/com/android/server/hdmi/Constants.java
index 751f2db..157057d 100644
--- a/services/core/java/com/android/server/hdmi/Constants.java
+++ b/services/core/java/com/android/server/hdmi/Constants.java
@@ -118,6 +118,7 @@
             MESSAGE_SYSTEM_AUDIO_MODE_REQUEST,
             MESSAGE_GIVE_AUDIO_STATUS,
             MESSAGE_SET_SYSTEM_AUDIO_MODE,
+            MESSAGE_SET_AUDIO_VOLUME_LEVEL,
             MESSAGE_REPORT_AUDIO_STATUS,
             MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS,
             MESSAGE_SYSTEM_AUDIO_MODE_STATUS,
@@ -197,9 +198,9 @@
     static final int MESSAGE_SYSTEM_AUDIO_MODE_REQUEST = 0x70;
     static final int MESSAGE_GIVE_AUDIO_STATUS = 0x71;
     static final int MESSAGE_SET_SYSTEM_AUDIO_MODE = 0x72;
+    static final int MESSAGE_SET_AUDIO_VOLUME_LEVEL = 0x73;
     static final int MESSAGE_REPORT_AUDIO_STATUS = 0x7A;
     static final int MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS = 0x7D;
-    static final int MESSAGE_SET_AUDIO_VOLUME_LEVEL = 0x73;
     static final int MESSAGE_SYSTEM_AUDIO_MODE_STATUS = 0x7E;
     static final int MESSAGE_ROUTING_CHANGE = 0x80;
     static final int MESSAGE_ROUTING_INFORMATION = 0x81;
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index 26a1613..fb2d2ee 100755
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -303,6 +303,13 @@
         if (dispatchMessageToAction(message)) {
             return Constants.HANDLED;
         }
+
+        // If a message type has its own class, all valid messages of that type
+        // will be represented by an instance of that class.
+        if (message instanceof SetAudioVolumeLevelMessage) {
+            return handleSetAudioVolumeLevel((SetAudioVolumeLevelMessage) message);
+        }
+
         switch (message.getOpcode()) {
             case Constants.MESSAGE_ACTIVE_SOURCE:
                 return handleActiveSource(message);
@@ -637,6 +644,11 @@
         return Constants.NOT_HANDLED;
     }
 
+    @Constants.HandleMessageResult
+    protected int handleSetAudioVolumeLevel(SetAudioVolumeLevelMessage message) {
+        return Constants.NOT_HANDLED;
+    }
+
     @Constants.RcProfile
     protected abstract int getRcProfile();
 
@@ -1002,6 +1014,57 @@
         action.start();
     }
 
+    void addAvcAudioStatusAction(int targetAddress) {
+        if (!hasAction(AbsoluteVolumeAudioStatusAction.class)) {
+            addAndStartAction(new AbsoluteVolumeAudioStatusAction(this, targetAddress));
+        }
+    }
+
+    void removeAvcAudioStatusAction() {
+        removeAction(AbsoluteVolumeAudioStatusAction.class);
+    }
+
+    void updateAvcVolume(int volumeIndex) {
+        for (AbsoluteVolumeAudioStatusAction action :
+                getActions(AbsoluteVolumeAudioStatusAction.class)) {
+            action.updateVolume(volumeIndex);
+        }
+    }
+
+    /**
+     * Determines whether {@code targetAddress} supports <Set Audio Volume Level>. Does two things
+     * in parallel: send <Give Features> (to get <Report Features> in response),
+     * and send <Set Audio Volume Level> (to see if it gets a <Feature Abort> in response).
+     */
+    @ServiceThreadOnly
+    void queryAvcSupport(int targetAddress) {
+        assertRunOnServiceThread();
+
+        // Send <Give Features> if using CEC 2.0 or above.
+        if (mService.getCecVersion() >= HdmiControlManager.HDMI_CEC_VERSION_2_0) {
+            synchronized (mLock) {
+                mService.sendCecCommand(HdmiCecMessageBuilder.buildGiveFeatures(
+                        getDeviceInfo().getLogicalAddress(), targetAddress));
+            }
+        }
+
+        // If we don't already have a {@link SetAudioVolumeLevelDiscoveryAction} for the target
+        // device, start one.
+        List<SetAudioVolumeLevelDiscoveryAction> savlDiscoveryActions =
+                getActions(SetAudioVolumeLevelDiscoveryAction.class);
+        if (savlDiscoveryActions.stream().noneMatch(a -> a.getTargetAddress() == targetAddress)) {
+            addAndStartAction(new SetAudioVolumeLevelDiscoveryAction(this, targetAddress,
+                    new IHdmiControlCallback.Stub() {
+                            @Override
+                            public void onComplete(int result) {
+                                if (result == HdmiControlManager.RESULT_SUCCESS) {
+                                    getService().checkAndUpdateAbsoluteVolumeControlState();
+                                }
+                            }
+                        }));
+        }
+    }
+
     @ServiceThreadOnly
     void startQueuedActions() {
         assertRunOnServiceThread();
@@ -1205,6 +1268,9 @@
      */
     protected void disableDevice(
             boolean initiatedByCec, final PendingActionClearedCallback originalCallback) {
+        removeAction(AbsoluteVolumeAudioStatusAction.class);
+        removeAction(SetAudioVolumeLevelDiscoveryAction.class);
+
         mPendingActionClearedCallback =
                 new PendingActionClearedCallback() {
                     @Override
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
index 90b4f76..c0c0202 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceSource.java
@@ -307,6 +307,7 @@
     protected void disableDevice(boolean initiatedByCec, PendingActionClearedCallback callback) {
         removeAction(OneTouchPlayAction.class);
         removeAction(DevicePowerStatusAction.class);
+        removeAction(AbsoluteVolumeAudioStatusAction.class);
 
         super.disableDevice(initiatedByCec, callback);
     }
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 9212fb6..1ea1457 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -1166,6 +1166,19 @@
         return Constants.HANDLED;
     }
 
+    @Override
+    @Constants.HandleMessageResult
+    protected int handleSetAudioVolumeLevel(SetAudioVolumeLevelMessage message) {
+        // <Set Audio Volume Level> should only be sent to the System Audio device, so we don't
+        // handle it when System Audio Mode is enabled.
+        if (mService.isSystemAudioActivated()) {
+            return Constants.ABORT_NOT_IN_CORRECT_MODE;
+        } else {
+            mService.setStreamMusicVolume(message.getAudioVolumeLevel(), 0);
+            return Constants.HANDLED;
+        }
+    }
+
     void announceOneTouchRecordResult(int recorderAddress, int result) {
         mService.invokeOneTouchRecordResult(recorderAddress, result);
     }
@@ -1202,6 +1215,13 @@
         return mService.getHdmiCecNetwork().getSafeCecDeviceInfo(Constants.ADDR_AUDIO_SYSTEM);
     }
 
+    /**
+     * Returns the audio output device used for System Audio Mode.
+     */
+    AudioDeviceAttributes getSystemAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC;
+    }
+
 
     @ServiceThreadOnly
     void handleRemoveActiveRoutingPath(int path) {
@@ -1296,6 +1316,7 @@
         removeAction(OneTouchRecordAction.class);
         removeAction(TimerRecordingAction.class);
         removeAction(NewDeviceAction.class);
+        removeAction(AbsoluteVolumeAudioStatusAction.class);
 
         disableSystemAudioIfExist();
         disableArcIfExist();
@@ -1318,7 +1339,6 @@
         removeAction(SystemAudioActionFromAvr.class);
         removeAction(SystemAudioActionFromTv.class);
         removeAction(SystemAudioAutoInitiationAction.class);
-        removeAction(SystemAudioStatusAction.class);
         removeAction(VolumeControlAction.class);
 
         if (!mService.isControlEnabled()) {
@@ -1589,6 +1609,7 @@
         return DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
                 .setRecordTvScreenSupport(FEATURE_SUPPORTED)
                 .setArcTxSupport(hasArcPort ? FEATURE_SUPPORTED : FEATURE_NOT_SUPPORTED)
+                .setSetAudioVolumeLevelSupport(FEATURE_SUPPORTED)
                 .build();
     }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
index 290cae5..2b84225 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessage.java
@@ -262,6 +262,8 @@
                 return "Give Audio Status";
             case Constants.MESSAGE_SET_SYSTEM_AUDIO_MODE:
                 return "Set System Audio Mode";
+            case Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL:
+                return "Set Audio Volume Level";
             case Constants.MESSAGE_REPORT_AUDIO_STATUS:
                 return "Report Audio Status";
             case Constants.MESSAGE_GIVE_SYSTEM_AUDIO_MODE_STATUS:
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
index 8b6d16a..caaf800 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
@@ -259,6 +259,7 @@
             // The addition of a local device should not notify listeners
             return;
         }
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
         if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) {
             // Don't notify listeners of devices that haven't reported their physical address yet
             return;
@@ -383,7 +384,7 @@
     final void removeCecDevice(HdmiCecLocalDevice localDevice, int address) {
         assertRunOnServiceThread();
         HdmiDeviceInfo info = removeDeviceInfo(HdmiDeviceInfo.idForCecDevice(address));
-
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
         localDevice.mCecMessageCache.flushMessagesFrom(address);
         if (info.getPhysicalAddress() == HdmiDeviceInfo.PATH_INVALID) {
             // Don't notify listeners of devices that haven't reported their physical address yet
@@ -586,6 +587,8 @@
                 .build();
 
         updateCecDevice(newDeviceInfo);
+
+        mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
     }
 
     @ServiceThreadOnly
@@ -617,6 +620,8 @@
                     )
                     .build();
             updateCecDevice(newDeviceInfo);
+
+            mHdmiControlService.checkAndUpdateAbsoluteVolumeControlState();
         }
     }
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 12380ab..9824b4e 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -30,6 +30,7 @@
 import static com.android.server.power.ShutdownThread.SHUTDOWN_ACTION_PROPERTY;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
@@ -38,6 +39,7 @@
 import android.content.IntentFilter;
 import android.database.ContentObserver;
 import android.hardware.display.DisplayManager;
+import android.hardware.hdmi.DeviceFeatures;
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiHotplugEvent;
@@ -56,7 +58,12 @@
 import android.hardware.hdmi.IHdmiVendorCommandListener;
 import android.hardware.tv.cec.V1_0.OptionKey;
 import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.media.AudioAttributes;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceInfo;
+import android.media.AudioDeviceVolumeManager;
 import android.media.AudioManager;
+import android.media.VolumeInfo;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
 import android.media.tv.TvInputManager;
@@ -83,6 +90,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
+import android.view.KeyEvent;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -101,6 +109,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -202,6 +211,27 @@
     public @interface WakeReason {
     }
 
+    @VisibleForTesting
+    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI = new AudioDeviceAttributes(
+            AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_HDMI, "");
+    @VisibleForTesting
+    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_ARC =
+            new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+                    AudioDeviceInfo.TYPE_HDMI_ARC, "");
+    static final AudioDeviceAttributes AUDIO_OUTPUT_DEVICE_HDMI_EARC =
+            new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT,
+                    AudioDeviceInfo.TYPE_HDMI_EARC, "");
+
+    // Audio output devices used for Absolute Volume Control
+    private static final List<AudioDeviceAttributes> AVC_AUDIO_OUTPUT_DEVICES =
+            Collections.unmodifiableList(Arrays.asList(AUDIO_OUTPUT_DEVICE_HDMI,
+                    AUDIO_OUTPUT_DEVICE_HDMI_ARC, AUDIO_OUTPUT_DEVICE_HDMI_EARC));
+
+    // AudioAttributes for STREAM_MUSIC
+    @VisibleForTesting
+    static final AudioAttributes STREAM_MUSIC_ATTRIBUTES =
+            new AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build();
+
     private final Executor mServiceThreadExecutor = new Executor() {
         @Override
         public void execute(Runnable r) {
@@ -209,6 +239,10 @@
         }
     };
 
+    Executor getServiceThreadExecutor() {
+        return mServiceThreadExecutor;
+    }
+
     // Logical address of the active source.
     @GuardedBy("mLock")
     protected final ActiveSource mActiveSource = new ActiveSource();
@@ -222,6 +256,13 @@
     @HdmiControlManager.VolumeControl
     private int mHdmiCecVolumeControl;
 
+    // Caches the volume behaviors of all audio output devices in AVC_AUDIO_OUTPUT_DEVICES.
+    @GuardedBy("mLock")
+    private Map<AudioDeviceAttributes, Integer> mAudioDeviceVolumeBehaviors = new HashMap<>();
+
+    // Maximum volume of AudioManager.STREAM_MUSIC. Set upon gaining access to system services.
+    private int mStreamMusicMaxVolume;
+
     // Make sure HdmiCecConfig is instantiated and the XMLs are read.
     private HdmiCecConfig mHdmiCecConfig;
 
@@ -411,6 +452,12 @@
     private PowerManagerInternalWrapper mPowerManagerInternal;
 
     @Nullable
+    private AudioManager mAudioManager;
+
+    @Nullable
+    private AudioDeviceVolumeManagerWrapperInterface mAudioDeviceVolumeManager;
+
+    @Nullable
     private Looper mIoLooper;
 
     @Nullable
@@ -439,11 +486,21 @@
 
     private final SelectRequestBuffer mSelectRequestBuffer = new SelectRequestBuffer();
 
-    @VisibleForTesting HdmiControlService(Context context, List<Integer> deviceTypes) {
+    /**
+     * Constructor for testing.
+     *
+     * It's critical to use a fake AudioDeviceVolumeManager because a normally instantiated
+     * AudioDeviceVolumeManager can access the "real" AudioService on the DUT.
+     *
+     * @see FakeAudioDeviceVolumeManagerWrapper
+     */
+    @VisibleForTesting HdmiControlService(Context context, List<Integer> deviceTypes,
+            AudioDeviceVolumeManagerWrapperInterface audioDeviceVolumeManager) {
         super(context);
         mLocalDevices = deviceTypes;
         mSettingsObserver = new SettingsObserver(mHandler);
         mHdmiCecConfig = new HdmiCecConfig(context);
+        mAudioDeviceVolumeManager = audioDeviceVolumeManager;
     }
 
     public HdmiControlService(Context context) {
@@ -744,6 +801,14 @@
                     Context.TV_INPUT_SERVICE);
             mPowerManager = new PowerManagerWrapper(getContext());
             mPowerManagerInternal = new PowerManagerInternalWrapper();
+            mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+            mStreamMusicMaxVolume = getAudioManager().getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+            if (mAudioDeviceVolumeManager == null) {
+                mAudioDeviceVolumeManager =
+                        new AudioDeviceVolumeManagerWrapper(getContext());
+            }
+            getAudioDeviceVolumeManager().addOnDeviceVolumeBehaviorChangedListener(
+                    mServiceThreadExecutor, this::onDeviceVolumeBehaviorChanged);
         } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
             runOnServiceThread(this::bootCompleted);
         }
@@ -2419,6 +2484,7 @@
             pw.println("mPowerStatus: " + mPowerStatusController.getPowerStatus());
             pw.println("mIsCecAvailable: " + mIsCecAvailable);
             pw.println("mCecVersion: " + mCecVersion);
+            pw.println("mIsAbsoluteVolumeControlEnabled: " + isAbsoluteVolumeControlEnabled());
 
             // System settings
             pw.println("System_settings:");
@@ -2578,6 +2644,7 @@
             @HdmiControlManager.VolumeControl int hdmiCecVolumeControl) {
         mHdmiCecVolumeControl = hdmiCecVolumeControl;
         announceHdmiCecVolumeControlFeatureChange(hdmiCecVolumeControl);
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
     }
 
     // Get the source address to send out commands to devices connected to the current device
@@ -3086,15 +3153,17 @@
     private void announceHdmiCecVolumeControlFeatureChange(
             @HdmiControlManager.VolumeControl int hdmiCecVolumeControl) {
         assertRunOnServiceThread();
-        mHdmiCecVolumeControlFeatureListenerRecords.broadcast(listener -> {
-            try {
-                listener.onHdmiCecVolumeControlFeature(hdmiCecVolumeControl);
-            } catch (RemoteException e) {
-                Slog.e(TAG,
-                        "Failed to report HdmiControlVolumeControlStatusChange: "
-                                + hdmiCecVolumeControl);
-            }
-        });
+        synchronized (mLock) {
+            mHdmiCecVolumeControlFeatureListenerRecords.broadcast(listener -> {
+                try {
+                    listener.onHdmiCecVolumeControlFeature(hdmiCecVolumeControl);
+                } catch (RemoteException e) {
+                    Slog.e(TAG,
+                            "Failed to report HdmiControlVolumeControlStatusChange: "
+                                    + hdmiCecVolumeControl);
+                }
+            });
+        }
     }
 
     public HdmiCecLocalDeviceTv tv() {
@@ -3131,8 +3200,20 @@
                 HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
     }
 
+    /**
+     * Returns null before the boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}.
+     */
+    @Nullable
     AudioManager getAudioManager() {
-        return (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager;
+    }
+
+    /**
+     * Returns null before the boot phase {@link SystemService#PHASE_SYSTEM_SERVICES_READY}.
+     */
+    @Nullable
+    private AudioDeviceVolumeManagerWrapperInterface getAudioDeviceVolumeManager() {
+        return mAudioDeviceVolumeManager;
     }
 
     boolean isControlEnabled() {
@@ -3486,6 +3567,7 @@
         synchronized (mLock) {
             mSystemAudioActivated = on;
         }
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
     }
 
     @ServiceThreadOnly
@@ -3599,6 +3681,8 @@
             device.addActiveSourceHistoryItem(new ActiveSource(logicalAddress, physicalAddress),
                     deviceIsActiveSource, caller);
         }
+
+        runOnServiceThread(this::checkAndUpdateAbsoluteVolumeControlState);
     }
 
     // This method should only be called when the device can be the active source
@@ -3791,4 +3875,332 @@
             Slog.e(TAG, "Failed to report setting change", e);
         }
     }
+
+    /**
+     * Listener for changes to the volume behavior of an audio output device. Caches the
+     * volume behavior of devices used for Absolute Volume Control.
+     */
+    @VisibleForTesting
+    @ServiceThreadOnly
+    void onDeviceVolumeBehaviorChanged(AudioDeviceAttributes device, int volumeBehavior) {
+        assertRunOnServiceThread();
+        if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) {
+            synchronized (mLock) {
+                mAudioDeviceVolumeBehaviors.put(device, volumeBehavior);
+            }
+            checkAndUpdateAbsoluteVolumeControlState();
+        }
+    }
+
+    /**
+     * Wrapper for {@link AudioManager#getDeviceVolumeBehavior} that takes advantage of cached
+     * results for the volume behaviors of HDMI audio devices.
+     */
+    @AudioManager.DeviceVolumeBehavior
+    private int getDeviceVolumeBehavior(AudioDeviceAttributes device) {
+        if (AVC_AUDIO_OUTPUT_DEVICES.contains(device)) {
+            synchronized (mLock) {
+                if (mAudioDeviceVolumeBehaviors.containsKey(device)) {
+                    return mAudioDeviceVolumeBehaviors.get(device);
+                }
+            }
+        }
+        return getAudioManager().getDeviceVolumeBehavior(device);
+    }
+
+    /**
+     * Returns whether Absolute Volume Control is enabled or not. This is determined by the
+     * volume behavior of the relevant HDMI audio output device(s) for this device's type.
+     */
+    public boolean isAbsoluteVolumeControlEnabled() {
+        if (!isTvDevice() && !isPlaybackDevice()) {
+            return false;
+        }
+        AudioDeviceAttributes avcAudioOutputDevice = getAvcAudioOutputDevice();
+        if (avcAudioOutputDevice == null) {
+            return false;
+        }
+        return getDeviceVolumeBehavior(avcAudioOutputDevice)
+                    == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
+    }
+
+    private AudioDeviceAttributes getAvcAudioOutputDevice() {
+        if (isTvDevice()) {
+            return tv().getSystemAudioOutputDevice();
+        } else if (isPlaybackDevice()) {
+            return AUDIO_OUTPUT_DEVICE_HDMI;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Checks the conditions for Absolute Volume Control (AVC), and enables or disables the feature
+     * if necessary. AVC is enabled precisely when a specific audio output device
+     * (HDMI for playback devices, and HDMI_ARC or HDMI_EARC for TVs) is using absolute volume
+     * behavior.
+     *
+     * AVC must be enabled on a Playback device or TV precisely when it is playing
+     * audio on an external device (the System Audio device) that supports the feature.
+     * This reduces to these conditions:
+     *
+     * 1. If the System Audio Device is an Audio System: System Audio Mode is active
+     * 2. Our HDMI audio output device is using full volume behavior
+     * 3. CEC volume is enabled
+     * 4. The System Audio device supports AVC (i.e. it supports <Set Audio Volume Level>)
+     *
+     * If not all of these conditions are met, this method disables AVC if necessary.
+     *
+     * If all of these conditions are met, this method starts an action to query the System Audio
+     * device's audio status, which enables AVC upon obtaining the audio status.
+     */
+    @ServiceThreadOnly
+    void checkAndUpdateAbsoluteVolumeControlState() {
+        assertRunOnServiceThread();
+
+        // Can't enable or disable AVC before we have access to system services
+        if (getAudioManager() == null) {
+            return;
+        }
+
+        HdmiCecLocalDevice localCecDevice;
+        if (isTvDevice() && tv() != null) {
+            localCecDevice = tv();
+            // Condition 1: TVs need System Audio Mode to be active
+            // (Doesn't apply to Playback Devices, where if SAM isn't active, we assume the
+            // TV is the System Audio Device instead.)
+            if (!isSystemAudioActivated()) {
+                disableAbsoluteVolumeControl();
+                return;
+            }
+        } else if (isPlaybackDevice() && playback() != null) {
+            localCecDevice = playback();
+        } else {
+            // Either this device type doesn't support AVC, or it hasn't fully initialized yet
+            return;
+        }
+
+        HdmiDeviceInfo systemAudioDeviceInfo = getHdmiCecNetwork().getSafeCecDeviceInfo(
+                localCecDevice.findAudioReceiverAddress());
+        @AudioManager.DeviceVolumeBehavior int currentVolumeBehavior =
+                        getDeviceVolumeBehavior(getAvcAudioOutputDevice());
+
+        // Condition 2: Already using full or absolute volume behavior
+        boolean alreadyUsingFullOrAbsoluteVolume =
+                currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL
+                        || currentVolumeBehavior == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE;
+        // Condition 3: CEC volume is enabled
+        boolean cecVolumeEnabled =
+                getHdmiCecVolumeControl() == HdmiControlManager.VOLUME_CONTROL_ENABLED;
+
+        if (!cecVolumeEnabled || !alreadyUsingFullOrAbsoluteVolume) {
+            disableAbsoluteVolumeControl();
+            return;
+        }
+
+        // Check for safety: if the System Audio device is a candidate for AVC, we should already
+        // have received messages from it to trigger the other conditions.
+        if (systemAudioDeviceInfo == null) {
+            disableAbsoluteVolumeControl();
+            return;
+        }
+        // Condition 4: The System Audio device supports AVC (i.e. <Set Audio Volume Level>).
+        switch (systemAudioDeviceInfo.getDeviceFeatures().getSetAudioVolumeLevelSupport()) {
+            case DeviceFeatures.FEATURE_SUPPORTED:
+                if (!isAbsoluteVolumeControlEnabled()) {
+                    // Start an action that will call {@link #enableAbsoluteVolumeControl}
+                    // once the System Audio device sends <Report Audio Status>
+                    localCecDevice.addAvcAudioStatusAction(
+                            systemAudioDeviceInfo.getLogicalAddress());
+                }
+                return;
+            case DeviceFeatures.FEATURE_NOT_SUPPORTED:
+                disableAbsoluteVolumeControl();
+                return;
+            case DeviceFeatures.FEATURE_SUPPORT_UNKNOWN:
+                disableAbsoluteVolumeControl();
+                localCecDevice.queryAvcSupport(systemAudioDeviceInfo.getLogicalAddress());
+                return;
+            default:
+                return;
+        }
+    }
+
+    private void disableAbsoluteVolumeControl() {
+        if (isPlaybackDevice()) {
+            playback().removeAvcAudioStatusAction();
+        } else if (isTvDevice()) {
+            tv().removeAvcAudioStatusAction();
+        }
+        AudioDeviceAttributes device = getAvcAudioOutputDevice();
+        if (getDeviceVolumeBehavior(device) == AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE) {
+            getAudioManager().setDeviceVolumeBehavior(device,
+                    AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        }
+    }
+
+    /**
+     * Enables Absolute Volume Control. Should only be called when all the conditions for
+     * AVC are met (see {@link #checkAndUpdateAbsoluteVolumeControlState}).
+     * @param audioStatus The initial audio status to set the audio output device to
+     */
+    void enableAbsoluteVolumeControl(AudioStatus audioStatus) {
+        HdmiCecLocalDevice localDevice = isPlaybackDevice() ? playback() : tv();
+        HdmiDeviceInfo systemAudioDevice = getHdmiCecNetwork().getDeviceInfo(
+                localDevice.findAudioReceiverAddress());
+        VolumeInfo volumeInfo = new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                .setMuted(audioStatus.getMute())
+                .setVolumeIndex(audioStatus.getVolume())
+                .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                .build();
+        mAbsoluteVolumeChangedListener = new AbsoluteVolumeChangedListener(
+                localDevice, systemAudioDevice);
+
+        // AudioService sets the volume of the stream and device based on the input VolumeInfo
+        // when enabling absolute volume behavior, but not the mute state
+        notifyAvcMuteChange(audioStatus.getMute());
+        getAudioDeviceVolumeManager().setDeviceAbsoluteVolumeBehavior(
+                getAvcAudioOutputDevice(), volumeInfo, mServiceThreadExecutor,
+                mAbsoluteVolumeChangedListener, true);
+    }
+
+    private AbsoluteVolumeChangedListener mAbsoluteVolumeChangedListener;
+
+    @VisibleForTesting
+    AbsoluteVolumeChangedListener getAbsoluteVolumeChangedListener() {
+        return mAbsoluteVolumeChangedListener;
+    }
+
+    /**
+     * Listeners for changes reported by AudioService to the state of an audio output device using
+     * absolute volume behavior.
+     */
+    @VisibleForTesting
+    class AbsoluteVolumeChangedListener implements
+            AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener {
+        private HdmiCecLocalDevice mLocalDevice;
+        private HdmiDeviceInfo mSystemAudioDevice;
+
+        private AbsoluteVolumeChangedListener(HdmiCecLocalDevice localDevice,
+                HdmiDeviceInfo systemAudioDevice) {
+            mLocalDevice = localDevice;
+            mSystemAudioDevice = systemAudioDevice;
+        }
+
+        /**
+         * Called when AudioService sets the volume level of an absolute volume audio output device
+         * to a numeric value.
+         */
+        @Override
+        public void onAudioDeviceVolumeChanged(
+                @NonNull AudioDeviceAttributes audioDevice,
+                @NonNull VolumeInfo volumeInfo) {
+            int localDeviceAddress;
+            synchronized (mLocalDevice.mLock) {
+                localDeviceAddress = mLocalDevice.getDeviceInfo().getLogicalAddress();
+            }
+            sendCecCommand(SetAudioVolumeLevelMessage.build(
+                            localDeviceAddress,
+                            mSystemAudioDevice.getLogicalAddress(),
+                            volumeInfo.getVolumeIndex()),
+                    // If sending the message fails, ask the System Audio device for its
+                    // audio status so that we can update AudioService
+                    (int errorCode) -> {
+                        if (errorCode == SendMessageResult.SUCCESS) {
+                            // Update the volume tracked in our AbsoluteVolumeAudioStatusAction
+                            // so it correctly processes incoming <Report Audio Status> messages
+                            HdmiCecLocalDevice avcDevice = isTvDevice() ? tv() : playback();
+                            avcDevice.updateAvcVolume(volumeInfo.getVolumeIndex());
+                        } else {
+                            sendCecCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(
+                                    localDeviceAddress,
+                                    mSystemAudioDevice.getLogicalAddress()
+                            ));
+                        }
+                    });
+        }
+
+        /**
+         * Called when AudioService adjusts the volume or mute state of an absolute volume
+         * audio output device
+         */
+        @Override
+        public void onAudioDeviceVolumeAdjusted(
+                @NonNull AudioDeviceAttributes audioDevice,
+                @NonNull VolumeInfo volumeInfo,
+                @AudioManager.VolumeAdjustment int direction,
+                @AudioDeviceVolumeManager.VolumeAdjustmentMode int mode
+        ) {
+            int keyCode;
+            switch (direction) {
+                case AudioManager.ADJUST_RAISE:
+                    keyCode = KeyEvent.KEYCODE_VOLUME_UP;
+                    break;
+                case AudioManager.ADJUST_LOWER:
+                    keyCode = KeyEvent.KEYCODE_VOLUME_DOWN;
+                    break;
+                case AudioManager.ADJUST_TOGGLE_MUTE:
+                case AudioManager.ADJUST_MUTE:
+                case AudioManager.ADJUST_UNMUTE:
+                    // Many CEC devices only support toggle mute. Therefore, we send the
+                    // same keycode for all three mute options.
+                    keyCode = KeyEvent.KEYCODE_VOLUME_MUTE;
+                    break;
+                default:
+                    return;
+            }
+            switch (mode) {
+                case AudioDeviceVolumeManager.ADJUST_MODE_NORMAL:
+                    mLocalDevice.sendVolumeKeyEvent(keyCode, true);
+                    mLocalDevice.sendVolumeKeyEvent(keyCode, false);
+                    break;
+                case AudioDeviceVolumeManager.ADJUST_MODE_START:
+                    mLocalDevice.sendVolumeKeyEvent(keyCode, true);
+                    break;
+                case AudioDeviceVolumeManager.ADJUST_MODE_END:
+                    mLocalDevice.sendVolumeKeyEvent(keyCode, false);
+                    break;
+                default:
+                    return;
+            }
+        }
+    }
+
+    /**
+     * Notifies AudioService of a change in the volume of the System Audio device. Has no effect if
+     * AVC is disabled, or the audio output device for AVC is not playing for STREAM_MUSIC
+     */
+    void notifyAvcVolumeChange(int volume) {
+        if (!isAbsoluteVolumeControlEnabled()) return;
+        List<AudioDeviceAttributes> streamMusicDevices =
+                getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES);
+        if (streamMusicDevices.contains(getAvcAudioOutputDevice())) {
+            setStreamMusicVolume(volume, AudioManager.FLAG_ABSOLUTE_VOLUME);
+        }
+    }
+
+    /**
+     * Notifies AudioService of a change in the mute status of the System Audio device. Has no
+     * effect if AVC is disabled, or the audio output device for AVC is not playing for STREAM_MUSIC
+     */
+    void notifyAvcMuteChange(boolean mute) {
+        if (!isAbsoluteVolumeControlEnabled()) return;
+        List<AudioDeviceAttributes> streamMusicDevices =
+                getAudioManager().getDevicesForAttributes(STREAM_MUSIC_ATTRIBUTES);
+        if (streamMusicDevices.contains(getAvcAudioOutputDevice())) {
+            int direction = mute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE;
+            getAudioManager().adjustStreamVolume(AudioManager.STREAM_MUSIC, direction,
+                    AudioManager.FLAG_ABSOLUTE_VOLUME);
+        }
+    }
+
+    /**
+     * Sets the volume index of {@link AudioManager#STREAM_MUSIC}. Rescales the input volume index
+     * from HDMI-CEC volume range to STREAM_MUSIC's.
+     */
+    void setStreamMusicVolume(int volume, int flags) {
+        getAudioManager().setStreamVolume(AudioManager.STREAM_MUSIC,
+                volume * mStreamMusicMaxVolume / AudioStatus.MAX_VOLUME, flags);
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/SendKeyAction.java b/services/core/java/com/android/server/hdmi/SendKeyAction.java
index adcef66..7daeaf1 100644
--- a/services/core/java/com/android/server/hdmi/SendKeyAction.java
+++ b/services/core/java/com/android/server/hdmi/SendKeyAction.java
@@ -172,8 +172,19 @@
     }
 
     private void sendKeyUp() {
-        sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(),
-                mTargetAddress));
+        // When using Absolute Volume Control, query audio status after a volume key is released.
+        // This allows us to notify AudioService of the resulting volume or mute status changes.
+        if (HdmiCecKeycode.isVolumeKeycode(mLastKeycode)
+                && localDevice().getService().isAbsoluteVolumeControlEnabled()) {
+            sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(),
+                    mTargetAddress),
+                    __ -> sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(
+                            getSourceAddress(),
+                            localDevice().findAudioReceiverAddress())));
+        } else {
+            sendCommand(HdmiCecMessageBuilder.buildUserControlReleased(getSourceAddress(),
+                    mTargetAddress));
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
index 96fd003..eb3b33d 100644
--- a/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
+++ b/services/core/java/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryAction.java
@@ -122,4 +122,11 @@
             return true;
         }
     }
+
+    /**
+     * Returns the logical address of this action's target device.
+     */
+    public int getTargetAddress() {
+        return mTargetAddress;
+    }
 }
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioAction.java b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
index 978c25d..e7a3db7 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioAction.java
@@ -153,7 +153,7 @@
                 boolean receivedStatus = HdmiUtils.parseCommandParamSystemAudioStatus(cmd);
                 if (receivedStatus == mTargetAudioStatus) {
                     setSystemAudioMode(receivedStatus);
-                    startAudioStatusAction();
+                    finish();
                     return true;
                 } else {
                     HdmiLogger.debug("Unexpected system audio mode request:" + receivedStatus);
@@ -168,11 +168,6 @@
         }
     }
 
-    protected void startAudioStatusAction() {
-        addAndStartAction(new SystemAudioStatusAction(tv(), mAvrLogicalAddress, mCallbacks));
-        finish();
-    }
-
     protected void removeSystemAudioActionInProgress() {
         removeActionExcept(SystemAudioActionFromTv.class, this);
         removeActionExcept(SystemAudioActionFromAvr.class, this);
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java b/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
index 6ddff91..99148c4 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioActionFromAvr.java
@@ -16,8 +16,8 @@
 
 package com.android.server.hdmi;
 
-import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.IHdmiControlCallback;
 
 /**
@@ -65,7 +65,7 @@
 
         if (mTargetAudioStatus) {
             setSystemAudioMode(true);
-            startAudioStatusAction();
+            finish();
         } else {
             setSystemAudioMode(false);
             finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
deleted file mode 100644
index b4af540..0000000
--- a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (C) 2014 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.hdmi;
-
-import android.hardware.hdmi.HdmiControlManager;
-import android.hardware.hdmi.IHdmiControlCallback;
-import android.hardware.tv.cec.V1_0.SendMessageResult;
-
-import com.android.server.hdmi.HdmiControlService.SendMessageCallback;
-
-import java.util.List;
-
-/**
- * Action to update audio status (volume or mute) of audio amplifier
- */
-final class SystemAudioStatusAction extends HdmiCecFeatureAction {
-    private static final String TAG = "SystemAudioStatusAction";
-
-    // State that waits for <ReportAudioStatus>.
-    private static final int STATE_WAIT_FOR_REPORT_AUDIO_STATUS = 1;
-
-    private final int mAvrAddress;
-
-    SystemAudioStatusAction(
-            HdmiCecLocalDevice source, int avrAddress, List<IHdmiControlCallback> callbacks) {
-        super(source, callbacks);
-        mAvrAddress = avrAddress;
-    }
-
-    SystemAudioStatusAction(HdmiCecLocalDevice source, int avrAddress,
-            IHdmiControlCallback callback) {
-        super(source, callback);
-        mAvrAddress = avrAddress;
-    }
-
-    @Override
-    boolean start() {
-        mState = STATE_WAIT_FOR_REPORT_AUDIO_STATUS;
-        addTimer(mState, HdmiConfig.TIMEOUT_MS);
-        sendGiveAudioStatus();
-        return true;
-    }
-
-    private void sendGiveAudioStatus() {
-        sendCommand(HdmiCecMessageBuilder.buildGiveAudioStatus(getSourceAddress(), mAvrAddress),
-                new SendMessageCallback() {
-            @Override
-            public void onSendCompleted(int error) {
-                if (error != SendMessageResult.SUCCESS) {
-                    handleSendGiveAudioStatusFailure();
-                }
-            }
-        });
-    }
-
-    private void handleSendGiveAudioStatusFailure() {
-
-        // Still return SUCCESS to callback.
-        finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
-    }
-
-    @Override
-    boolean processCommand(HdmiCecMessage cmd) {
-        if (mState != STATE_WAIT_FOR_REPORT_AUDIO_STATUS || mAvrAddress != cmd.getSource()) {
-            return false;
-        }
-
-        switch (cmd.getOpcode()) {
-            case Constants.MESSAGE_REPORT_AUDIO_STATUS:
-                handleReportAudioStatus(cmd);
-                return true;
-        }
-
-        return false;
-    }
-
-    private void handleReportAudioStatus(HdmiCecMessage cmd) {
-        byte[] params = cmd.getParams();
-        boolean mute = HdmiUtils.isAudioStatusMute(cmd);
-        int volume = HdmiUtils.getAudioStatusVolume(cmd);
-        tv().setAudioStatus(mute, volume);
-
-        if (!(tv().isSystemAudioActivated() ^ mute)) {
-            // Toggle AVR's mute status to match with the system audio status.
-            sendUserControlPressedAndReleased(mAvrAddress, HdmiCecKeycode.CEC_KEYCODE_MUTE);
-        }
-        finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
-    }
-
-    @Override
-    void handleTimerEvent(int state) {
-        if (mState != state) {
-            return;
-        }
-
-        handleSendGiveAudioStatusFailure();
-    }
-}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 7360bbc..603d012 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityManagerInternal;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -73,7 +74,6 @@
 import android.os.LocaleList;
 import android.os.Looper;
 import android.os.Message;
-import android.os.MessageQueue;
 import android.os.Process;
 import android.os.RemoteCallbackList;
 import android.os.RemoteException;
@@ -111,6 +111,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
 import com.android.internal.os.SomeArgs;
@@ -146,9 +147,7 @@
 import java.util.Objects;
 import java.util.OptionalInt;
 
-/*
- * Wraps the C++ InputManager and provides its callbacks.
- */
+/** The system implementation of {@link IInputManager} that manages input devices. */
 public class InputManagerService extends IInputManager.Stub
         implements Watchdog.Monitor {
     static final String TAG = "InputManager";
@@ -182,8 +181,7 @@
     /** TODO(b/169067926): Remove this. */
     private static final boolean UNTRUSTED_TOUCHES_TOAST = false;
 
-    // Pointer to native input manager service object.
-    private final long mPtr;
+    private final NativeInputManagerService mNative;
 
     private final Context mContext;
     private final InputManagerHandler mHandler;
@@ -298,92 +296,6 @@
     @GuardedBy("mInputMonitors")
     final Map<IBinder, GestureMonitorSpyWindow> mInputMonitors = new HashMap<>();
 
-    private static native long nativeInit(InputManagerService service,
-            Context context, MessageQueue messageQueue);
-    private static native void nativeStart(long ptr);
-    private static native void nativeSetDisplayViewports(long ptr,
-            DisplayViewport[] viewports);
-
-    private static native int nativeGetScanCodeState(long ptr,
-            int deviceId, int sourceMask, int scanCode);
-    private static native int nativeGetKeyCodeState(long ptr,
-            int deviceId, int sourceMask, int keyCode);
-    private static native int nativeGetSwitchState(long ptr,
-            int deviceId, int sourceMask, int sw);
-    private static native boolean nativeHasKeys(long ptr,
-            int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists);
-    private static native int nativeGetKeyCodeForKeyLocation(long ptr, int deviceId,
-            int locationKeyCode);
-    private static native InputChannel nativeCreateInputChannel(long ptr, String name);
-    private static native InputChannel nativeCreateInputMonitor(long ptr, int displayId,
-            String name, int pid);
-    private static native void nativeRemoveInputChannel(long ptr, IBinder connectionToken);
-    private static native void nativePilferPointers(long ptr, IBinder token);
-    private static native void nativeSetInputFilterEnabled(long ptr, boolean enable);
-    private static native boolean nativeSetInTouchMode(long ptr, boolean inTouchMode, int pid,
-            int uid, boolean hasPermission);
-    private static native void nativeSetMaximumObscuringOpacityForTouch(long ptr, float opacity);
-    private static native void nativeSetBlockUntrustedTouchesMode(long ptr, int mode);
-    private static native int nativeInjectInputEvent(long ptr, InputEvent event,
-            int injectorPid, int injectorUid, int syncMode, int timeoutMillis,
-            int policyFlags);
-    private static native VerifiedInputEvent nativeVerifyInputEvent(long ptr, InputEvent event);
-    private static native void nativeToggleCapsLock(long ptr, int deviceId);
-    private static native void nativeDisplayRemoved(long ptr, int displayId);
-    private static native void nativeSetInputDispatchMode(long ptr, boolean enabled, boolean frozen);
-    private static native void nativeSetSystemUiLightsOut(long ptr, boolean lightsOut);
-    private static native void nativeSetFocusedApplication(long ptr,
-            int displayId, InputApplicationHandle application);
-    private static native void nativeSetFocusedDisplay(long ptr, int displayId);
-    private static native boolean nativeTransferTouchFocus(long ptr,
-            IBinder fromChannelToken, IBinder toChannelToken, boolean isDragDrop);
-    private static native boolean nativeTransferTouch(long ptr, IBinder destChannelToken);
-    private static native void nativeSetPointerSpeed(long ptr, int speed);
-    private static native void nativeSetPointerAcceleration(long ptr, float acceleration);
-    private static native void nativeSetShowTouches(long ptr, boolean enabled);
-    private static native void nativeSetInteractive(long ptr, boolean interactive);
-    private static native void nativeReloadCalibration(long ptr);
-    private static native void nativeVibrate(long ptr, int deviceId, long[] pattern,
-            int[] amplitudes, int repeat, int token);
-    private static native void nativeVibrateCombined(long ptr, int deviceId, long[] pattern,
-            SparseArray<int[]> amplitudes, int repeat, int token);
-    private static native void nativeCancelVibrate(long ptr, int deviceId, int token);
-    private static native boolean nativeIsVibrating(long ptr, int deviceId);
-    private static native int[] nativeGetVibratorIds(long ptr, int deviceId);
-    private static native int nativeGetBatteryCapacity(long ptr, int deviceId);
-    private static native int nativeGetBatteryStatus(long ptr, int deviceId);
-    private static native List<Light> nativeGetLights(long ptr, int deviceId);
-    private static native int nativeGetLightPlayerId(long ptr, int deviceId, int lightId);
-    private static native int nativeGetLightColor(long ptr, int deviceId, int lightId);
-    private static native void nativeSetLightPlayerId(long ptr, int deviceId, int lightId,
-            int playerId);
-    private static native void nativeSetLightColor(long ptr, int deviceId, int lightId, int color);
-    private static native void nativeReloadKeyboardLayouts(long ptr);
-    private static native void nativeReloadDeviceAliases(long ptr);
-    private static native String nativeDump(long ptr);
-    private static native void nativeMonitor(long ptr);
-    private static native boolean nativeIsInputDeviceEnabled(long ptr, int deviceId);
-    private static native void nativeEnableInputDevice(long ptr, int deviceId);
-    private static native void nativeDisableInputDevice(long ptr, int deviceId);
-    private static native void nativeSetPointerIconType(long ptr, int iconId);
-    private static native void nativeReloadPointerIcons(long ptr);
-    private static native void nativeSetCustomPointerIcon(long ptr, PointerIcon icon);
-    private static native void nativeRequestPointerCapture(long ptr, IBinder windowToken,
-            boolean enabled);
-    private static native boolean nativeCanDispatchToDisplay(long ptr, int deviceId, int displayId);
-    private static native void nativeNotifyPortAssociationsChanged(long ptr);
-    private static native void nativeChangeUniqueIdAssociation(long ptr);
-    private static native void nativeNotifyPointerDisplayIdChanged(long ptr);
-    private static native void nativeSetDisplayEligibilityForPointerCapture(long ptr, int displayId,
-            boolean enabled);
-    private static native void nativeSetMotionClassifierEnabled(long ptr, boolean enabled);
-    private static native InputSensorInfo[] nativeGetSensorList(long ptr, int deviceId);
-    private static native boolean nativeFlushSensor(long ptr, int deviceId, int sensorType);
-    private static native boolean nativeEnableSensor(long ptr, int deviceId, int sensorType,
-            int samplingPeriodUs, int maxBatchReportLatencyUs);
-    private static native void nativeDisableSensor(long ptr, int deviceId, int sensorType);
-    private static native void nativeCancelCurrentTouch(long ptr);
-
     // Maximum number of milliseconds to wait for input event injection.
     private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000;
 
@@ -450,18 +362,47 @@
     /** Whether to use the dev/input/event or uevent subsystem for the audio jack. */
     final boolean mUseDevInputEventForAudioJack;
 
+    /** Point of injection for test dependencies. */
+    @VisibleForTesting
+    static class Injector {
+        private final Context mContext;
+        private final Looper mLooper;
+
+        Injector(Context context, Looper looper) {
+            mContext = context;
+            mLooper = looper;
+        }
+
+        Context getContext() {
+            return mContext;
+        }
+
+        Looper getLooper() {
+            return mLooper;
+        }
+
+        NativeInputManagerService getNativeService(InputManagerService service) {
+            return new NativeInputManagerService.NativeImpl(service, mContext, mLooper.getQueue());
+        }
+    }
+
     public InputManagerService(Context context) {
-        this.mContext = context;
-        this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());
+        this(new Injector(context, DisplayThread.get().getLooper()));
+    }
+
+    @VisibleForTesting
+    InputManagerService(Injector injector) {
+        mContext = injector.getContext();
+        mHandler = new InputManagerHandler(injector.getLooper());
+        mNative = injector.getNativeService(this);
 
         mStaticAssociations = loadStaticInputPortAssociations();
         mUseDevInputEventForAudioJack =
-                context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
+                mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
         Slog.i(TAG, "Initializing input manager, mUseDevInputEventForAudioJack="
                 + mUseDevInputEventForAudioJack);
-        mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
 
-        String doubleTouchGestureEnablePath = context.getResources().getString(
+        String doubleTouchGestureEnablePath = mContext.getResources().getString(
                 R.string.config_doubleTouchGestureEnableFile);
         mDoubleTouchGestureEnableFile = TextUtils.isEmpty(doubleTouchGestureEnablePath) ? null :
             new File(doubleTouchGestureEnablePath);
@@ -504,9 +445,9 @@
 
     public void start() {
         Slog.i(TAG, "Starting input manager");
-        nativeStart(mPtr);
+        mNative.start();
 
-        // Add ourself to the Watchdog monitors.
+        // Add ourselves to the Watchdog monitors.
         Watchdog.getInstance().addMonitor(this);
 
         registerPointerSpeedSettingObserver();
@@ -599,14 +540,14 @@
         if (DEBUG) {
             Slog.d(TAG, "Reloading keyboard layouts.");
         }
-        nativeReloadKeyboardLayouts(mPtr);
+        mNative.reloadKeyboardLayouts();
     }
 
     private void reloadDeviceAliases() {
         if (DEBUG) {
             Slog.d(TAG, "Reloading device names.");
         }
-        nativeReloadDeviceAliases(mPtr);
+        mNative.reloadDeviceAliases();
     }
 
     private void setDisplayViewportsInternal(List<DisplayViewport> viewports) {
@@ -615,7 +556,7 @@
             for (int i = viewports.size() - 1; i >= 0; --i) {
                 vArray[i] = viewports.get(i);
             }
-            nativeSetDisplayViewports(mPtr, vArray);
+            mNative.setDisplayViewports(vArray);
 
             if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) {
                 final AdditionalDisplayInputProperties properties =
@@ -642,7 +583,7 @@
      * @return The key state.
      */
     public int getKeyCodeState(int deviceId, int sourceMask, int keyCode) {
-        return nativeGetKeyCodeState(mPtr, deviceId, sourceMask, keyCode);
+        return mNative.getKeyCodeState(deviceId, sourceMask, keyCode);
     }
 
     /**
@@ -655,7 +596,7 @@
      * @return The key state.
      */
     public int getScanCodeState(int deviceId, int sourceMask, int scanCode) {
-        return nativeGetScanCodeState(mPtr, deviceId, sourceMask, scanCode);
+        return mNative.getScanCodeState(deviceId, sourceMask, scanCode);
     }
 
     /**
@@ -668,7 +609,7 @@
      * @return The switch state.
      */
     public int getSwitchState(int deviceId, int sourceMask, int switchCode) {
-        return nativeGetSwitchState(mPtr, deviceId, sourceMask, switchCode);
+        return mNative.getSwitchState(deviceId, sourceMask, switchCode);
     }
 
     /**
@@ -691,7 +632,7 @@
             throw new IllegalArgumentException("keyExists must be at least as large as keyCodes");
         }
 
-        return nativeHasKeys(mPtr, deviceId, sourceMask, keyCodes, keyExists);
+        return mNative.hasKeys(deviceId, sourceMask, keyCodes, keyExists);
     }
 
     /**
@@ -707,7 +648,7 @@
         if (locationKeyCode <= KEYCODE_UNKNOWN || locationKeyCode > KeyEvent.getMaxKeyCode()) {
             return KEYCODE_UNKNOWN;
         }
-        return nativeGetKeyCodeForKeyLocation(mPtr, deviceId, locationKeyCode);
+        return mNative.getKeyCodeForKeyLocation(deviceId, locationKeyCode);
     }
 
     /**
@@ -720,7 +661,7 @@
     public boolean transferTouch(IBinder destChannelToken) {
         // TODO(b/162194035): Replace this with a SPY window
         Objects.requireNonNull(destChannelToken, "destChannelToken must not be null");
-        return nativeTransferTouch(mPtr, destChannelToken);
+        return mNative.transferTouch(destChannelToken);
     }
 
     /**
@@ -736,7 +677,7 @@
             throw new IllegalArgumentException("displayId must >= 0.");
         }
 
-        return nativeCreateInputMonitor(mPtr, displayId, inputChannelName, Binder.getCallingPid());
+        return mNative.createInputMonitor(displayId, inputChannelName, Binder.getCallingPid());
     }
 
     @NonNull
@@ -818,7 +759,7 @@
      * @param name The name of this input channel
      */
     public InputChannel createInputChannel(String name) {
-        return nativeCreateInputChannel(mPtr, name);
+        return mNative.createInputChannel(name);
     }
 
     /**
@@ -827,7 +768,7 @@
      */
     public void removeInputChannel(IBinder connectionToken) {
         Objects.requireNonNull(connectionToken, "connectionToken must not be null");
-        nativeRemoveInputChannel(mPtr, connectionToken);
+        mNative.removeInputChannel(connectionToken);
     }
 
     /**
@@ -869,7 +810,7 @@
                 }
             }
 
-            nativeSetInputFilterEnabled(mPtr, filter != null);
+            mNative.setInputFilterEnabled(filter != null);
         }
     }
 
@@ -893,11 +834,19 @@
      * @return {@code true} if the touch mode was successfully changed, {@code false} otherwise
      */
     public boolean setInTouchMode(boolean inTouchMode, int pid, int uid, boolean hasPermission) {
-        return nativeSetInTouchMode(mPtr, inTouchMode, pid, uid, hasPermission);
+        return mNative.setInTouchMode(inTouchMode, pid, uid, hasPermission);
     }
 
     @Override // Binder call
-    public boolean injectInputEvent(InputEvent event, int mode) {
+    public boolean injectInputEvent(InputEvent event, int mode, int targetUid) {
+        if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+                "injectInputEvent()", true /*checkInstrumentationSource*/)) {
+            throw new SecurityException(
+                    "Injecting input events requires the caller (or the source of the "
+                            + "instrumentation, if any) to have the INJECT_EVENTS permission.");
+        }
+        // We are not checking if targetUid matches the callingUid, since having the permission
+        // already means you can inject into any window.
         Objects.requireNonNull(event, "event must not be null");
         if (mode != InputEventInjectionSync.NONE
                 && mode != InputEventInjectionSync.WAIT_FOR_FINISHED
@@ -906,22 +855,41 @@
         }
 
         final int pid = Binder.getCallingPid();
-        final int uid = Binder.getCallingUid();
         final long ident = Binder.clearCallingIdentity();
+        final boolean injectIntoUid = targetUid != Process.INVALID_UID;
         final int result;
         try {
-            result = nativeInjectInputEvent(mPtr, event, pid, uid, mode,
-                    INJECTION_TIMEOUT_MILLIS, WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
+            result = mNative.injectInputEvent(event, injectIntoUid,
+                    targetUid, mode, INJECTION_TIMEOUT_MILLIS,
+                    WindowManagerPolicy.FLAG_DISABLE_KEY_REPEAT);
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
         switch (result) {
-            case InputEventInjectionResult.PERMISSION_DENIED:
-                Slog.w(TAG, "Input event injection from pid " + pid + " permission denied.");
-                throw new SecurityException(
-                        "Injecting to another application requires INJECT_EVENTS permission");
             case InputEventInjectionResult.SUCCEEDED:
                 return true;
+            case InputEventInjectionResult.TARGET_MISMATCH:
+                if (!injectIntoUid) {
+                    throw new IllegalStateException("Injection should not result in TARGET_MISMATCH"
+                            + " when it is not targeted into to a specific uid.");
+                }
+                // Attempt to inject into a window owned by the instrumentation source of the caller
+                // because it is possible that tests adopt the identity of the shell when launching
+                // activities that they would like to inject into.
+                final ActivityManagerInternal ami =
+                        LocalServices.getService(ActivityManagerInternal.class);
+                Objects.requireNonNull(ami, "ActivityManagerInternal should not be null.");
+                final int instrUid = ami.getInstrumentationSourceUid(Binder.getCallingUid());
+                if (instrUid != Process.INVALID_UID && targetUid != instrUid) {
+                    Slog.w(TAG, "Targeted input event was not directed at a window owned by uid "
+                            + targetUid + ". Attempting to inject into window owned by "
+                            + "instrumentation source uid " + instrUid + ".");
+                    return injectInputEvent(event, mode, instrUid);
+                }
+                throw new IllegalArgumentException(
+                        "Targeted input event injection from pid " + pid
+                                + " was not directed at a window owned by uid "
+                                + targetUid + ".");
             case InputEventInjectionResult.TIMED_OUT:
                 Slog.w(TAG, "Input event injection from pid " + pid + " timed out.");
                 return false;
@@ -935,7 +903,7 @@
     @Override // Binder call
     public VerifiedInputEvent verifyInputEvent(InputEvent event) {
         Objects.requireNonNull(event, "event must not be null");
-        return nativeVerifyInputEvent(mPtr, event);
+        return mNative.verifyInputEvent(event);
     }
 
     /**
@@ -958,7 +926,7 @@
     // Binder call
     @Override
     public boolean isInputDeviceEnabled(int deviceId) {
-        return nativeIsInputDeviceEnabled(mPtr, deviceId);
+        return mNative.isInputDeviceEnabled(deviceId);
     }
 
     // Binder call
@@ -968,7 +936,7 @@
                 "enableInputDevice()")) {
             throw new SecurityException("Requires DISABLE_INPUT_DEVICE permission");
         }
-        nativeEnableInputDevice(mPtr, deviceId);
+        mNative.enableInputDevice(deviceId);
     }
 
     // Binder call
@@ -978,7 +946,7 @@
                 "disableInputDevice()")) {
             throw new SecurityException("Requires DISABLE_INPUT_DEVICE permission");
         }
-        nativeDisableInputDevice(mPtr, deviceId);
+        mNative.disableInputDevice(deviceId);
     }
 
     /**
@@ -1224,7 +1192,7 @@
             try {
                 if (mDataStore.setTouchCalibration(inputDeviceDescriptor, surfaceRotation,
                         calibration)) {
-                    nativeReloadCalibration(mPtr);
+                    mNative.reloadCalibration();
                 }
             } finally {
                 mDataStore.saveIfNeeded();
@@ -1736,11 +1704,11 @@
     }
 
     public void setFocusedApplication(int displayId, InputApplicationHandle application) {
-        nativeSetFocusedApplication(mPtr, displayId, application);
+        mNative.setFocusedApplication(displayId, application);
     }
 
     public void setFocusedDisplay(int displayId) {
-        nativeSetFocusedDisplay(mPtr, displayId);
+        mNative.setFocusedDisplay(displayId);
     }
 
     /** Clean up input window handles of the given display. */
@@ -1750,22 +1718,22 @@
             mPointerIconDisplayContext = null;
         }
 
-        nativeDisplayRemoved(mPtr, displayId);
+        mNative.displayRemoved(displayId);
     }
 
     @Override
     public void requestPointerCapture(IBinder inputChannelToken, boolean enabled) {
         Objects.requireNonNull(inputChannelToken, "event must not be null");
 
-        nativeRequestPointerCapture(mPtr, inputChannelToken, enabled);
+        mNative.requestPointerCapture(inputChannelToken, enabled);
     }
 
     public void setInputDispatchMode(boolean enabled, boolean frozen) {
-        nativeSetInputDispatchMode(mPtr, enabled, frozen);
+        mNative.setInputDispatchMode(enabled, frozen);
     }
 
     public void setSystemUiLightsOut(boolean lightsOut) {
-        nativeSetSystemUiLightsOut(mPtr, lightsOut);
+        mNative.setSystemUiLightsOut(lightsOut);
     }
 
     /**
@@ -1784,7 +1752,7 @@
      */
     public boolean transferTouchFocus(@NonNull InputChannel fromChannel,
             @NonNull InputChannel toChannel, boolean isDragDrop) {
-        return nativeTransferTouchFocus(mPtr, fromChannel.getToken(), toChannel.getToken(),
+        return mNative.transferTouchFocus(fromChannel.getToken(), toChannel.getToken(),
                 isDragDrop);
     }
 
@@ -1805,7 +1773,7 @@
             @NonNull IBinder toChannelToken) {
         Objects.nonNull(fromChannelToken);
         Objects.nonNull(toChannelToken);
-        return nativeTransferTouchFocus(mPtr, fromChannelToken, toChannelToken,
+        return mNative.transferTouchFocus(fromChannelToken, toChannelToken,
                 false /* isDragDrop */);
     }
 
@@ -1831,7 +1799,7 @@
     private void setPointerSpeedUnchecked(int speed) {
         speed = Math.min(Math.max(speed, InputManager.MIN_POINTER_SPEED),
                 InputManager.MAX_POINTER_SPEED);
-        nativeSetPointerSpeed(mPtr, speed);
+        mNative.setPointerSpeed(speed);
     }
 
     private void setPointerAcceleration(float acceleration, int displayId) {
@@ -1854,7 +1822,7 @@
 
     @GuardedBy("mAdditionalDisplayInputPropertiesLock")
     private void updatePointerAccelerationLocked(float acceleration) {
-        nativeSetPointerAcceleration(mPtr, acceleration);
+        mNative.setPointerAcceleration(acceleration);
     }
 
     private void setPointerIconVisible(boolean visible, int displayId) {
@@ -1879,12 +1847,12 @@
     private void updatePointerIconVisibleLocked(boolean visible) {
         if (visible) {
             if (mIconType == PointerIcon.TYPE_CUSTOM) {
-                nativeSetCustomPointerIcon(mPtr, mIcon);
+                mNative.setCustomPointerIcon(mIcon);
             } else {
-                nativeSetPointerIconType(mPtr, mIconType);
+                mNative.setPointerIconType(mIconType);
             }
         } else {
-            nativeSetPointerIconType(mPtr, PointerIcon.TYPE_NULL);
+            mNative.setPointerIconType(PointerIcon.TYPE_NULL);
         }
     }
 
@@ -1911,7 +1879,7 @@
 
     private void updateShowTouchesFromSettings() {
         int setting = getShowTouchesSetting(0);
-        nativeSetShowTouches(mPtr, setting != 0);
+        mNative.setShowTouches(setting != 0);
     }
 
     private void registerShowTouchesSettingObserver() {
@@ -1930,7 +1898,7 @@
                 mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON,
                 0, UserHandle.USER_CURRENT);
         PointerIcon.setUseLargeIcons(accessibilityConfig == 1);
-        nativeReloadPointerIcons(mPtr);
+        mNative.reloadPointerIcons();
     }
 
     private void registerAccessibilityLargePointerSettingObserver() {
@@ -1958,7 +1926,7 @@
                 (enabled ? "Enabling" : "Disabling") + " motion classifier because " + reason
                 + ": feature " + (featureEnabledFlag ? "enabled" : "disabled")
                 + ", long press timeout = " + timeout);
-        nativeSetMotionClassifierEnabled(mPtr, enabled);
+        mNative.setMotionClassifierEnabled(enabled);
     }
 
     private void registerLongPressTimeoutObserver() {
@@ -1986,7 +1954,7 @@
 
     private void updateBlockUntrustedTouchesModeFromSettings() {
         final int mode = InputManager.getInstance().getBlockUntrustedTouchesMode(mContext);
-        nativeSetBlockUntrustedTouchesMode(mPtr, mode);
+        mNative.setBlockUntrustedTouchesMode(mode);
     }
 
     private void registerMaximumObscuringOpacityForTouchSettingObserver() {
@@ -2008,7 +1976,7 @@
                     + ", it should be >= 0 and <= 1, rejecting update.");
             return;
         }
-        nativeSetMaximumObscuringOpacityForTouch(mPtr, opacity);
+        mNative.setMaximumObscuringOpacityForTouch(opacity);
     }
 
     private int getShowTouchesSetting(int defaultValue) {
@@ -2034,7 +2002,7 @@
             }
         }
         // TODO(b/215597605): trigger MousePositionTracker update
-        nativeNotifyPointerDisplayIdChanged(mPtr);
+        mNative.notifyPointerDisplayIdChanged();
     }
 
     private int getVirtualMousePointerDisplayId() {
@@ -2044,7 +2012,7 @@
     }
 
     private void setDisplayEligibilityForPointerCapture(int displayId, boolean isEligible) {
-        nativeSetDisplayEligibilityForPointerCapture(mPtr, displayId, isEligible);
+        mNative.setDisplayEligibilityForPointerCapture(displayId, isEligible);
     }
 
     private static class VibrationInfo {
@@ -2143,7 +2111,7 @@
         VibratorToken v = getVibratorToken(deviceId, token);
         synchronized (v) {
             v.mVibrating = true;
-            nativeVibrate(mPtr, deviceId, info.getPattern(), info.getAmplitudes(),
+            mNative.vibrate(deviceId, info.getPattern(), info.getAmplitudes(),
                     info.getRepeatIndex(), v.mTokenValue);
         }
     }
@@ -2151,13 +2119,13 @@
     // Binder call
     @Override
     public int[] getVibratorIds(int deviceId) {
-        return nativeGetVibratorIds(mPtr, deviceId);
+        return mNative.getVibratorIds(deviceId);
     }
 
     // Binder call
     @Override
     public boolean isVibrating(int deviceId) {
-        return nativeIsVibrating(mPtr, deviceId);
+        return mNative.isVibrating(deviceId);
     }
 
     // Binder call
@@ -2175,7 +2143,7 @@
             if (effect instanceof CombinedVibration.Mono) {
                 CombinedVibration.Mono mono = (CombinedVibration.Mono) effect;
                 VibrationInfo info = new VibrationInfo(mono.getEffect());
-                nativeVibrate(mPtr, deviceId, info.getPattern(), info.getAmplitudes(),
+                mNative.vibrate(deviceId, info.getPattern(), info.getAmplitudes(),
                         info.getRepeatIndex(), v.mTokenValue);
             } else if (effect instanceof CombinedVibration.Stereo) {
                 CombinedVibration.Stereo stereo = (CombinedVibration.Stereo) effect;
@@ -2194,7 +2162,7 @@
                     }
                     amplitudes.put(effects.keyAt(i), info.getAmplitudes());
                 }
-                nativeVibrateCombined(mPtr, deviceId, pattern, amplitudes, repeat,
+                mNative.vibrateCombined(deviceId, pattern, amplitudes, repeat,
                         v.mTokenValue);
             }
         }
@@ -2225,7 +2193,7 @@
     private void cancelVibrateIfNeeded(VibratorToken v) {
         synchronized (v) {
             if (v.mVibrating) {
-                nativeCancelVibrate(mPtr, v.mDeviceId, v.mTokenValue);
+                mNative.cancelVibrate(v.mDeviceId, v.mTokenValue);
                 v.mVibrating = false;
             }
         }
@@ -2321,13 +2289,13 @@
     // Binder call
     @Override
     public int getBatteryStatus(int deviceId) {
-        return nativeGetBatteryStatus(mPtr, deviceId);
+        return mNative.getBatteryStatus(deviceId);
     }
 
     // Binder call
     @Override
     public int getBatteryCapacity(int deviceId) {
-        return nativeGetBatteryCapacity(mPtr, deviceId);
+        return mNative.getBatteryCapacity(deviceId);
     }
 
     // Binder call
@@ -2343,10 +2311,10 @@
                 final AdditionalDisplayInputProperties properties =
                         mAdditionalDisplayInputProperties.get(mOverriddenPointerDisplayId);
                 if (properties == null || properties.pointerIconVisible) {
-                    nativeSetPointerIconType(mPtr, mIconType);
+                    mNative.setPointerIconType(mIconType);
                 }
             } else {
-                nativeSetPointerIconType(mPtr, mIconType);
+                mNative.setPointerIconType(mIconType);
             }
         }
     }
@@ -2364,10 +2332,10 @@
                 if (properties == null || properties.pointerIconVisible) {
                     // Only set the icon if it is not currently hidden; otherwise, it will be set
                     // once it's no longer hidden.
-                    nativeSetCustomPointerIcon(mPtr, mIcon);
+                    mNative.setCustomPointerIcon(mIcon);
                 }
             } else {
-                nativeSetCustomPointerIcon(mPtr, mIcon);
+                mNative.setCustomPointerIcon(mIcon);
             }
         }
     }
@@ -2391,7 +2359,7 @@
         synchronized (mAssociationsLock) {
             mRuntimeAssociations.put(inputPort, displayPort);
         }
-        nativeNotifyPortAssociationsChanged(mPtr);
+        mNative.notifyPortAssociationsChanged();
     }
 
     /**
@@ -2412,7 +2380,7 @@
         synchronized (mAssociationsLock) {
             mRuntimeAssociations.remove(inputPort);
         }
-        nativeNotifyPortAssociationsChanged(mPtr);
+        mNative.notifyPortAssociationsChanged();
     }
 
     @Override // Binder call
@@ -2429,7 +2397,7 @@
         synchronized (mAssociationsLock) {
             mUniqueIdAssociations.put(inputPort, displayUniqueId);
         }
-        nativeChangeUniqueIdAssociation(mPtr);
+        mNative.changeUniqueIdAssociation();
     }
 
     @Override // Binder call
@@ -2445,12 +2413,12 @@
         synchronized (mAssociationsLock) {
             mUniqueIdAssociations.remove(inputPort);
         }
-        nativeChangeUniqueIdAssociation(mPtr);
+        mNative.changeUniqueIdAssociation();
     }
 
     @Override // Binder call
     public InputSensorInfo[] getSensorList(int deviceId) {
-        return nativeGetSensorList(mPtr, deviceId);
+        return mNative.getSensorList(deviceId);
     }
 
     @Override // Binder call
@@ -2511,7 +2479,7 @@
             int callingPid = Binder.getCallingPid();
             SensorEventListenerRecord listener = mSensorEventListeners.get(callingPid);
             if (listener != null) {
-                return nativeFlushSensor(mPtr, deviceId, sensorType);
+                return mNative.flushSensor(deviceId, sensorType);
             }
             return false;
         }
@@ -2521,7 +2489,7 @@
     public boolean enableSensor(int deviceId, int sensorType, int samplingPeriodUs,
             int maxBatchReportLatencyUs) {
         synchronized (mInputDevicesLock) {
-            return nativeEnableSensor(mPtr, deviceId, sensorType, samplingPeriodUs,
+            return mNative.enableSensor(deviceId, sensorType, samplingPeriodUs,
                     maxBatchReportLatencyUs);
         }
     }
@@ -2529,7 +2497,7 @@
     @Override // Binder call
     public void disableSensor(int deviceId, int sensorType) {
         synchronized (mInputDevicesLock) {
-            nativeDisableSensor(mPtr, deviceId, sensorType);
+            mNative.disableSensor(deviceId, sensorType);
         }
     }
 
@@ -2568,7 +2536,7 @@
      */
     @Override // Binder call
     public List<Light> getLights(int deviceId) {
-        return nativeGetLights(mPtr, deviceId);
+        return mNative.getLights(deviceId);
     }
 
     /**
@@ -2581,11 +2549,11 @@
                     + "lightState " + lightState);
         }
         if (light.getType() == Light.LIGHT_TYPE_PLAYER_ID) {
-            nativeSetLightPlayerId(mPtr, deviceId, light.getId(), lightState.getPlayerId());
+            mNative.setLightPlayerId(deviceId, light.getId(), lightState.getPlayerId());
         } else {
             // Set ARGB format color to input device light
             // Refer to https://developer.android.com/reference/kotlin/android/graphics/Color
-            nativeSetLightColor(mPtr, deviceId, light.getId(), lightState.getColor());
+            mNative.setLightColor(deviceId, light.getId(), lightState.getColor());
         }
     }
 
@@ -2593,7 +2561,7 @@
      * Set multiple light states with multiple light ids for a specific input device.
      */
     private void setLightStatesInternal(int deviceId, int[] lightIds, LightState[] lightStates) {
-        final List<Light> lights = nativeGetLights(mPtr, deviceId);
+        final List<Light> lights = mNative.getLights(deviceId);
         SparseArray<Light> lightArray = new SparseArray<>();
         for (int i = 0; i < lights.size(); i++) {
             lightArray.put(lights.get(i).getId(), lights.get(i));
@@ -2629,8 +2597,8 @@
     @Override
     public @Nullable LightState getLightState(int deviceId, int lightId) {
         synchronized (mLightLock) {
-            int color = nativeGetLightColor(mPtr, deviceId, lightId);
-            int playerId = nativeGetLightPlayerId(mPtr, deviceId, lightId);
+            int color = mNative.getLightColor(deviceId, lightId);
+            int playerId = mNative.getLightPlayerId(deviceId, lightId);
 
             return new LightState(color, playerId);
         }
@@ -2682,7 +2650,7 @@
             throw new SecurityException("Requires MONITOR_INPUT permission");
         }
 
-        nativeCancelCurrentTouch(mPtr);
+        mNative.cancelCurrentTouch();
     }
 
     @Override
@@ -2690,7 +2658,7 @@
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
 
         pw.println("INPUT MANAGER (dumpsys input)\n");
-        String dumpStr = nativeDump(mPtr);
+        String dumpStr = mNative.dump();
         if (dumpStr != null) {
             pw.println(dumpStr);
         }
@@ -2757,8 +2725,12 @@
             }
         }
     }
-
     private boolean checkCallingPermission(String permission, String func) {
+        return checkCallingPermission(permission, func, false /*checkInstrumentationSource*/);
+    }
+
+    private boolean checkCallingPermission(String permission, String func,
+            boolean checkInstrumentationSource) {
         // Quick check: if the calling permission is me, it's all okay.
         if (Binder.getCallingPid() == Process.myPid()) {
             return true;
@@ -2767,6 +2739,18 @@
         if (mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) {
             return true;
         }
+
+        if (checkInstrumentationSource) {
+            final ActivityManagerInternal ami =
+                    LocalServices.getService(ActivityManagerInternal.class);
+            Objects.requireNonNull(ami, "ActivityManagerInternal should not be null.");
+            final int instrumentationUid = ami.getInstrumentationSourceUid(Binder.getCallingUid());
+            if (instrumentationUid != Process.INVALID_UID && mContext.checkPermission(permission,
+                    -1 /*pid*/, instrumentationUid) == PackageManager.PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+
         String msg = "Permission Denial: " + func + " from pid="
                 + Binder.getCallingPid()
                 + ", uid=" + Binder.getCallingUid()
@@ -2783,7 +2767,7 @@
         synchronized (mLidSwitchLock) { /* Test if blocked by lid switch lock. */ }
         synchronized (mInputMonitors) { /* Test if blocked by input monitor lock. */ }
         synchronized (mAdditionalDisplayInputPropertiesLock) { /* Test if blocked by props lock */ }
-        nativeMonitor(mPtr);
+        mNative.monitor();
     }
 
     // Native callback.
@@ -3012,13 +2996,6 @@
 
     // Native callback.
     @SuppressWarnings("unused")
-    private boolean checkInjectEventsPermission(int injectorPid, int injectorUid) {
-        return mContext.checkPermission(android.Manifest.permission.INJECT_EVENTS,
-                injectorPid, injectorUid) == PackageManager.PERMISSION_GRANTED;
-    }
-
-    // Native callback.
-    @SuppressWarnings("unused")
     private void onPointerDownOutsideFocus(IBinder touchedToken) {
         mWindowManagerCallbacks.onPointerDownOutsideFocus(touchedToken);
     }
@@ -3117,7 +3094,7 @@
      * @return True if the device could dispatch to the given display, false otherwise.
      */
     public boolean canDispatchToDisplay(int deviceId, int displayId) {
-        return nativeCanDispatchToDisplay(mPtr, deviceId, displayId);
+        return mNative.canDispatchToDisplay(deviceId, displayId);
     }
 
     // Native callback.
@@ -3459,12 +3436,17 @@
 
         @Override
         public void sendInputEvent(InputEvent event, int policyFlags) {
+            if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+                    "sendInputEvent()")) {
+                throw new SecurityException(
+                        "The INJECT_EVENTS permission is required for injecting input events.");
+            }
             Objects.requireNonNull(event, "event must not be null");
 
             synchronized (mInputFilterLock) {
                 if (!mDisconnected) {
-                    nativeInjectInputEvent(mPtr, event, 0, 0,
-                            InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0,
+                    mNative.injectInputEvent(event, false /* injectIntoUid */, -1 /* uid */,
+                            InputManager.INJECT_INPUT_EVENT_MODE_ASYNC, 0 /* timeout */,
                             policyFlags | WindowManagerPolicy.FLAG_FILTERED);
                 }
             }
@@ -3483,7 +3465,7 @@
 
         @Override
         public void pilferPointers() {
-            nativePilferPointers(mPtr, mInputChannelToken);
+            mNative.pilferPointers(mInputChannelToken);
         }
 
         @Override
@@ -3661,12 +3643,12 @@
 
         @Override
         public void setInteractive(boolean interactive) {
-            nativeSetInteractive(mPtr, interactive);
+            mNative.setInteractive(interactive);
         }
 
         @Override
         public void toggleCapsLock(int deviceId) {
-            nativeToggleCapsLock(mPtr, deviceId);
+            mNative.toggleCapsLock(deviceId);
         }
 
         @Override
@@ -3737,7 +3719,7 @@
 
         @Override
         public void pilferPointers(IBinder token) {
-            nativePilferPointers(mPtr, token);
+            mNative.pilferPointers(token);
         }
     }
 
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
new file mode 100644
index 0000000..7178d20
--- /dev/null
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -0,0 +1,389 @@
+/*
+ * 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.input;
+
+import android.content.Context;
+import android.hardware.display.DisplayViewport;
+import android.hardware.input.InputSensorInfo;
+import android.hardware.lights.Light;
+import android.os.IBinder;
+import android.os.MessageQueue;
+import android.util.SparseArray;
+import android.view.InputApplicationHandle;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.PointerIcon;
+import android.view.VerifiedInputEvent;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * An interface for the native methods of InputManagerService. We use a public interface so that
+ * this can be mocked for testing by Mockito.
+ */
+@VisibleForTesting
+public interface NativeInputManagerService {
+
+    void start();
+
+    void setDisplayViewports(DisplayViewport[] viewports);
+
+    int getScanCodeState(int deviceId, int sourceMask, int scanCode);
+
+    int getKeyCodeState(int deviceId, int sourceMask, int keyCode);
+
+    int getSwitchState(int deviceId, int sourceMask, int sw);
+
+    boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes, boolean[] keyExists);
+
+    int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode);
+
+    InputChannel createInputChannel(String name);
+
+    InputChannel createInputMonitor(int displayId, String name, int pid);
+
+    void removeInputChannel(IBinder connectionToken);
+
+    void pilferPointers(IBinder token);
+
+    void setInputFilterEnabled(boolean enable);
+
+    boolean setInTouchMode(boolean inTouchMode, int pid, int uid, boolean hasPermission);
+
+    void setMaximumObscuringOpacityForTouch(float opacity);
+
+    void setBlockUntrustedTouchesMode(int mode);
+
+    int injectInputEvent(InputEvent event, boolean injectIntoUid, int uid, int syncMode,
+            int timeoutMillis, int policyFlags);
+
+    VerifiedInputEvent verifyInputEvent(InputEvent event);
+
+    void toggleCapsLock(int deviceId);
+
+    void displayRemoved(int displayId);
+
+    void setInputDispatchMode(boolean enabled, boolean frozen);
+
+    void setSystemUiLightsOut(boolean lightsOut);
+
+    void setFocusedApplication(int displayId, InputApplicationHandle application);
+
+    void setFocusedDisplay(int displayId);
+
+    boolean transferTouchFocus(IBinder fromChannelToken, IBinder toChannelToken,
+            boolean isDragDrop);
+
+    boolean transferTouch(IBinder destChannelToken);
+
+    void setPointerSpeed(int speed);
+
+    void setPointerAcceleration(float acceleration);
+
+    void setShowTouches(boolean enabled);
+
+    void setInteractive(boolean interactive);
+
+    void reloadCalibration();
+
+    void vibrate(int deviceId, long[] pattern, int[] amplitudes, int repeat, int token);
+
+    void vibrateCombined(int deviceId, long[] pattern, SparseArray<int[]> amplitudes,
+            int repeat, int token);
+
+    void cancelVibrate(int deviceId, int token);
+
+    boolean isVibrating(int deviceId);
+
+    int[] getVibratorIds(int deviceId);
+
+    int getBatteryCapacity(int deviceId);
+
+    int getBatteryStatus(int deviceId);
+
+    List<Light> getLights(int deviceId);
+
+    int getLightPlayerId(int deviceId, int lightId);
+
+    int getLightColor(int deviceId, int lightId);
+
+    void setLightPlayerId(int deviceId, int lightId, int playerId);
+
+    void setLightColor(int deviceId, int lightId, int color);
+
+    void reloadKeyboardLayouts();
+
+    void reloadDeviceAliases();
+
+    String dump();
+
+    void monitor();
+
+    boolean isInputDeviceEnabled(int deviceId);
+
+    void enableInputDevice(int deviceId);
+
+    void disableInputDevice(int deviceId);
+
+    void setPointerIconType(int iconId);
+
+    void reloadPointerIcons();
+
+    void setCustomPointerIcon(PointerIcon icon);
+
+    void requestPointerCapture(IBinder windowToken, boolean enabled);
+
+    boolean canDispatchToDisplay(int deviceId, int displayId);
+
+    void notifyPortAssociationsChanged();
+
+    void changeUniqueIdAssociation();
+
+    void notifyPointerDisplayIdChanged();
+
+    void setDisplayEligibilityForPointerCapture(int displayId, boolean enabled);
+
+    void setMotionClassifierEnabled(boolean enabled);
+
+    InputSensorInfo[] getSensorList(int deviceId);
+
+    boolean flushSensor(int deviceId, int sensorType);
+
+    boolean enableSensor(int deviceId, int sensorType, int samplingPeriodUs,
+            int maxBatchReportLatencyUs);
+
+    void disableSensor(int deviceId, int sensorType);
+
+    void cancelCurrentTouch();
+
+    /** The native implementation of InputManagerService methods. */
+    class NativeImpl implements NativeInputManagerService {
+        /** Pointer to native input manager service object, used by native code. */
+        @SuppressWarnings({"unused", "FieldCanBeLocal"})
+        private final long mPtr;
+
+        NativeImpl(InputManagerService service, Context context, MessageQueue messageQueue) {
+            mPtr = init(service, context, messageQueue);
+        }
+
+        private native long init(InputManagerService service, Context context,
+                MessageQueue messageQueue);
+
+        @Override
+        public native void start();
+
+        @Override
+        public native void setDisplayViewports(DisplayViewport[] viewports);
+
+        @Override
+        public native int getScanCodeState(int deviceId, int sourceMask, int scanCode);
+
+        @Override
+        public native int getKeyCodeState(int deviceId, int sourceMask, int keyCode);
+
+        @Override
+        public native int getSwitchState(int deviceId, int sourceMask, int sw);
+
+        @Override
+        public native boolean hasKeys(int deviceId, int sourceMask, int[] keyCodes,
+                boolean[] keyExists);
+
+        @Override
+        public native int getKeyCodeForKeyLocation(int deviceId, int locationKeyCode);
+
+        @Override
+        public native InputChannel createInputChannel(String name);
+
+        @Override
+        public native InputChannel createInputMonitor(int displayId, String name, int pid);
+
+        @Override
+        public native void removeInputChannel(IBinder connectionToken);
+
+        @Override
+        public native void pilferPointers(IBinder token);
+
+        @Override
+        public native void setInputFilterEnabled(boolean enable);
+
+        @Override
+        public native boolean setInTouchMode(boolean inTouchMode, int pid, int uid,
+                boolean hasPermission);
+
+        @Override
+        public native void setMaximumObscuringOpacityForTouch(float opacity);
+
+        @Override
+        public native void setBlockUntrustedTouchesMode(int mode);
+
+        @Override
+        public native int injectInputEvent(InputEvent event, boolean injectIntoUid, int uid,
+                int syncMode,
+                int timeoutMillis, int policyFlags);
+
+        @Override
+        public native VerifiedInputEvent verifyInputEvent(InputEvent event);
+
+        @Override
+        public native void toggleCapsLock(int deviceId);
+
+        @Override
+        public native void displayRemoved(int displayId);
+
+        @Override
+        public native void setInputDispatchMode(boolean enabled, boolean frozen);
+
+        @Override
+        public native void setSystemUiLightsOut(boolean lightsOut);
+
+        @Override
+        public native void setFocusedApplication(int displayId, InputApplicationHandle application);
+
+        @Override
+        public native void setFocusedDisplay(int displayId);
+
+        @Override
+        public native boolean transferTouchFocus(IBinder fromChannelToken, IBinder toChannelToken,
+                boolean isDragDrop);
+
+        @Override
+        public native boolean transferTouch(IBinder destChannelToken);
+
+        @Override
+        public native void setPointerSpeed(int speed);
+
+        @Override
+        public native void setPointerAcceleration(float acceleration);
+
+        @Override
+        public native void setShowTouches(boolean enabled);
+
+        @Override
+        public native void setInteractive(boolean interactive);
+
+        @Override
+        public native void reloadCalibration();
+
+        @Override
+        public native void vibrate(int deviceId, long[] pattern, int[] amplitudes, int repeat,
+                int token);
+
+        @Override
+        public native void vibrateCombined(int deviceId, long[] pattern,
+                SparseArray<int[]> amplitudes,
+                int repeat, int token);
+
+        @Override
+        public native void cancelVibrate(int deviceId, int token);
+
+        @Override
+        public native boolean isVibrating(int deviceId);
+
+        @Override
+        public native int[] getVibratorIds(int deviceId);
+
+        @Override
+        public native int getBatteryCapacity(int deviceId);
+
+        @Override
+        public native int getBatteryStatus(int deviceId);
+
+        @Override
+        public native List<Light> getLights(int deviceId);
+
+        @Override
+        public native int getLightPlayerId(int deviceId, int lightId);
+
+        @Override
+        public native int getLightColor(int deviceId, int lightId);
+
+        @Override
+        public native void setLightPlayerId(int deviceId, int lightId, int playerId);
+
+        @Override
+        public native void setLightColor(int deviceId, int lightId, int color);
+
+        @Override
+        public native void reloadKeyboardLayouts();
+
+        @Override
+        public native void reloadDeviceAliases();
+
+        @Override
+        public native String dump();
+
+        @Override
+        public native void monitor();
+
+        @Override
+        public native boolean isInputDeviceEnabled(int deviceId);
+
+        @Override
+        public native void enableInputDevice(int deviceId);
+
+        @Override
+        public native void disableInputDevice(int deviceId);
+
+        @Override
+        public native void setPointerIconType(int iconId);
+
+        @Override
+        public native void reloadPointerIcons();
+
+        @Override
+        public native void setCustomPointerIcon(PointerIcon icon);
+
+        @Override
+        public native void requestPointerCapture(IBinder windowToken, boolean enabled);
+
+        @Override
+        public native boolean canDispatchToDisplay(int deviceId, int displayId);
+
+        @Override
+        public native void notifyPortAssociationsChanged();
+
+        @Override
+        public native void changeUniqueIdAssociation();
+
+        @Override
+        public native void notifyPointerDisplayIdChanged();
+
+        @Override
+        public native void setDisplayEligibilityForPointerCapture(int displayId, boolean enabled);
+
+        @Override
+        public native void setMotionClassifierEnabled(boolean enabled);
+
+        @Override
+        public native InputSensorInfo[] getSensorList(int deviceId);
+
+        @Override
+        public native boolean flushSensor(int deviceId, int sensorType);
+
+        @Override
+        public native boolean enableSensor(int deviceId, int sensorType, int samplingPeriodUs,
+                int maxBatchReportLatencyUs);
+
+        @Override
+        public native void disableSensor(int deviceId, int sensorType);
+
+        @Override
+        public native void cancelCurrentTouch();
+    }
+}
diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
index db81393..37a4869 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
@@ -30,8 +30,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
-import android.os.Binder;
 import android.os.HandlerThread;
 import android.os.LocaleList;
 import android.os.RemoteException;
@@ -78,7 +76,7 @@
     private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
 
     private final LocaleManagerService mLocaleManagerService;
-    private final PackageManagerInternal mPackageManagerInternal;
+    private final PackageManager mPackageManager;
     private final Clock mClock;
     private final Context mContext;
     private final Object mStagedDataLock = new Object();
@@ -90,18 +88,18 @@
     private final BroadcastReceiver mUserMonitor;
 
     LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
-            PackageManagerInternal pmInternal, HandlerThread broadcastHandlerThread) {
-        this(localeManagerService.mContext, localeManagerService, pmInternal, Clock.systemUTC(),
+            PackageManager packageManager, HandlerThread broadcastHandlerThread) {
+        this(localeManagerService.mContext, localeManagerService, packageManager, Clock.systemUTC(),
                 new SparseArray<>(), broadcastHandlerThread);
     }
 
     @VisibleForTesting LocaleManagerBackupHelper(Context context,
             LocaleManagerService localeManagerService,
-            PackageManagerInternal pmInternal, Clock clock, SparseArray<StagedData> stagedData,
+            PackageManager packageManager, Clock clock, SparseArray<StagedData> stagedData,
             HandlerThread broadcastHandlerThread) {
         mContext = context;
         mLocaleManagerService = localeManagerService;
-        mPackageManagerInternal = pmInternal;
+        mPackageManager = packageManager;
         mClock = clock;
         mStagedData = stagedData;
 
@@ -130,8 +128,8 @@
         }
 
         HashMap<String, String> pkgStates = new HashMap<>();
-        for (ApplicationInfo appInfo : mPackageManagerInternal.getInstalledApplications(/*flags*/0,
-                userId, Binder.getCallingUid())) {
+        for (ApplicationInfo appInfo : mPackageManager.getInstalledApplicationsAsUser(
+                PackageManager.ApplicationInfoFlags.of(0), userId)) {
             try {
                 LocaleList appLocales = mLocaleManagerService.getApplicationLocales(
                         appInfo.packageName,
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 924db6a..9ef14cc 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -28,7 +28,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
+import android.content.pm.PackageManager.PackageInfoFlags;
 import android.content.res.Configuration;
 import android.os.Binder;
 import android.os.HandlerThread;
@@ -60,7 +60,7 @@
     private final LocaleManagerService.LocaleManagerBinderService mBinderService;
     private ActivityTaskManagerInternal mActivityTaskManagerInternal;
     private ActivityManagerInternal mActivityManagerInternal;
-    private PackageManagerInternal mPackageManagerInternal;
+    private PackageManager mPackageManager;
 
     private LocaleManagerBackupHelper mBackupHelper;
 
@@ -74,7 +74,7 @@
         mBinderService = new LocaleManagerBinderService();
         mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
-        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
+        mPackageManager = mContext.getPackageManager();
 
         HandlerThread broadcastHandlerThread = new HandlerThread(TAG,
                 Process.THREAD_PRIORITY_BACKGROUND);
@@ -90,7 +90,7 @@
         });
 
         mBackupHelper = new LocaleManagerBackupHelper(this,
-                mPackageManagerInternal, broadcastHandlerThread);
+                mPackageManager, broadcastHandlerThread);
 
         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
                 systemAppUpdateTracker);
@@ -102,7 +102,7 @@
     @VisibleForTesting
     LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal,
             ActivityManagerInternal activityManagerInternal,
-            PackageManagerInternal packageManagerInternal,
+            PackageManager packageManager,
             LocaleManagerBackupHelper localeManagerBackupHelper,
             PackageMonitor packageMonitor) {
         super(context);
@@ -110,7 +110,7 @@
         mBinderService = new LocaleManagerBinderService();
         mActivityTaskManagerInternal = activityTaskManagerInternal;
         mActivityManagerInternal = activityManagerInternal;
-        mPackageManagerInternal = packageManagerInternal;
+        mPackageManager = packageManager;
         mBackupHelper = localeManagerBackupHelper;
         mPackageMonitor = packageMonitor;
     }
@@ -419,8 +419,12 @@
     }
 
     private int getPackageUid(String appPackageName, int userId) {
-        return mPackageManagerInternal
-                .getPackageUid(appPackageName, /* flags */ 0, userId);
+        try {
+            return mPackageManager
+                    .getPackageUidAsUser(appPackageName, PackageInfoFlags.of(0), userId);
+        } catch (PackageManager.NameNotFoundException e) {
+            return Process.INVALID_UID;
+        }
     }
 
     @Nullable
diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java
index fac5106..31d5136 100644
--- a/services/core/java/com/android/server/location/LocationManagerService.java
+++ b/services/core/java/com/android/server/location/LocationManagerService.java
@@ -94,7 +94,6 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.Preconditions;
 import com.android.server.FgThread;
@@ -279,6 +278,9 @@
                 this::onLocationUserSettingsChanged);
         mInjector.getSettingsHelper().addOnLocationEnabledChangedListener(
                 this::onLocationModeChanged);
+        mInjector.getSettingsHelper().addAdasAllowlistChangedListener(
+                () -> refreshAppOpsRestrictions(UserHandle.USER_ALL)
+        );
         mInjector.getSettingsHelper().addIgnoreSettingsAllowlistChangedListener(
                 () -> refreshAppOpsRestrictions(UserHandle.USER_ALL));
         mInjector.getUserInfoHelper().addListener((userId, change) -> {
@@ -823,12 +825,6 @@
                 throw new IllegalArgumentException(
                         "adas gnss bypass requests are only allowed on the \"gps\" provider");
             }
-            if (!ArrayUtils.contains(mContext.getResources().getStringArray(
-                    com.android.internal.R.array.config_locationDriverAssistancePackageNames),
-                    identity.getPackageName())) {
-                throw new SecurityException(
-                        "only verified adas packages may use adas gnss bypass requests");
-            }
             if (!isLocationProvider) {
                 LocationPermissions.enforceCallingOrSelfBypassPermission(mContext);
             }
@@ -923,12 +919,6 @@
                 throw new IllegalArgumentException(
                         "adas gnss bypass requests are only allowed on the \"gps\" provider");
             }
-            if (!ArrayUtils.contains(mContext.getResources().getStringArray(
-                    com.android.internal.R.array.config_locationDriverAssistancePackageNames),
-                    identity.getPackageName())) {
-                throw new SecurityException(
-                        "only verified adas packages may use adas gnss bypass requests");
-            }
             if (!isLocationProvider) {
                 LocationPermissions.enforceCallingOrSelfBypassPermission(mContext);
             }
@@ -1542,6 +1532,7 @@
                 }
             }
             builder.add(mInjector.getSettingsHelper().getIgnoreSettingsAllowlist());
+            builder.add(mInjector.getSettingsHelper().getAdasAllowlist());
             allowedPackages = builder.build();
         }
 
diff --git a/services/core/java/com/android/server/location/injector/SettingsHelper.java b/services/core/java/com/android/server/location/injector/SettingsHelper.java
index 148afa7..490bfe1 100644
--- a/services/core/java/com/android/server/location/injector/SettingsHelper.java
+++ b/services/core/java/com/android/server/location/injector/SettingsHelper.java
@@ -146,6 +146,20 @@
     public abstract void removeOnGnssMeasurementsFullTrackingEnabledChangedListener(
             GlobalSettingChangedListener listener);
 
+    /** Retrieve adas allowlist. */
+    public abstract PackageTagsList getAdasAllowlist();
+
+    /**
+     * Add a listener for changes to the ADAS settings package allowlist. Callbacks occur on an
+     * unspecified thread.
+     */
+    public abstract void addAdasAllowlistChangedListener(GlobalSettingChangedListener listener);
+
+    /**
+     * Remove a listener for changes to the ADAS package allowlist.
+     */
+    public abstract void removeAdasAllowlistChangedListener(GlobalSettingChangedListener listener);
+
     /**
      * Retrieve the ignore location settings package+tags allowlist setting.
      */
diff --git a/services/core/java/com/android/server/location/injector/SystemSettingsHelper.java b/services/core/java/com/android/server/location/injector/SystemSettingsHelper.java
index 3e8da7d..777683e 100644
--- a/services/core/java/com/android/server/location/injector/SystemSettingsHelper.java
+++ b/services/core/java/com/android/server/location/injector/SystemSettingsHelper.java
@@ -16,6 +16,7 @@
 
 package com.android.server.location.injector;
 
+import static android.location.LocationDeviceConfig.ADAS_SETTINGS_ALLOWLIST;
 import static android.location.LocationDeviceConfig.IGNORE_SETTINGS_ALLOWLIST;
 import static android.provider.Settings.Global.ENABLE_GNSS_RAW_MEAS_FULL_TRACKING;
 import static android.provider.Settings.Global.LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS;
@@ -80,6 +81,7 @@
     private final StringListCachedSecureSetting mLocationPackageBlacklist;
     private final StringListCachedSecureSetting mLocationPackageWhitelist;
     private final StringSetCachedGlobalSetting mBackgroundThrottlePackageWhitelist;
+    private final PackageTagsListSetting mAdasPackageAllowlist;
     private final PackageTagsListSetting mIgnoreSettingsPackageAllowlist;
 
     public SystemSettingsHelper(Context context) {
@@ -98,6 +100,9 @@
                 LOCATION_BACKGROUND_THROTTLE_PACKAGE_WHITELIST,
                 () -> SystemConfig.getInstance().getAllowUnthrottledLocation(),
                 FgThread.getHandler());
+        mAdasPackageAllowlist = new PackageTagsListSetting(
+                ADAS_SETTINGS_ALLOWLIST,
+                () -> SystemConfig.getInstance().getAllowAdasLocationSettings());
         mIgnoreSettingsPackageAllowlist = new PackageTagsListSetting(
                 IGNORE_SETTINGS_ALLOWLIST,
                 () -> SystemConfig.getInstance().getAllowIgnoreLocationSettings());
@@ -233,6 +238,21 @@
     }
 
     @Override
+    public PackageTagsList getAdasAllowlist() {
+        return mAdasPackageAllowlist.getValue();
+    }
+
+    @Override
+    public void addAdasAllowlistChangedListener(GlobalSettingChangedListener listener) {
+        mAdasPackageAllowlist.addListener(listener);
+    }
+
+    @Override
+    public void removeAdasAllowlistChangedListener(GlobalSettingChangedListener listener) {
+        mAdasPackageAllowlist.removeListener(listener);
+    }
+
+    @Override
     public PackageTagsList getIgnoreSettingsAllowlist() {
         return mIgnoreSettingsPackageAllowlist.getValue();
     }
@@ -359,11 +379,19 @@
 
         PackageTagsList ignoreSettingsAllowlist = mIgnoreSettingsPackageAllowlist.getValue();
         if (!ignoreSettingsAllowlist.isEmpty()) {
-            ipw.println("Bypass Allow Packages:");
+            ipw.println("Emergency Bypass Allow Packages:");
             ipw.increaseIndent();
             ignoreSettingsAllowlist.dump(ipw);
             ipw.decreaseIndent();
         }
+
+        PackageTagsList adasPackageAllowlist = mAdasPackageAllowlist.getValue();
+        if (!adasPackageAllowlist.isEmpty()) {
+            ipw.println("ADAS Bypass Allow Packages:");
+            ipw.increaseIndent();
+            adasPackageAllowlist.dump(ipw);
+            ipw.decreaseIndent();
+        }
     }
 
     private abstract static class ObservingSetting extends ContentObserver {
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 721ef1e..1235352 100644
--- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java
+++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java
@@ -699,6 +699,9 @@
                 } else if (!mLocationSettings.getUserSettings(
                         getIdentity().getUserId()).isAdasGnssLocationEnabled()) {
                     adasGnssBypass = false;
+                } else if (!mSettingsHelper.getAdasAllowlist().contains(
+                        getIdentity().getPackageName(), getIdentity().getAttributionTag())) {
+                    adasGnssBypass = false;
                 }
 
                 builder.setAdasGnssBypass(adasGnssBypass);
@@ -1406,6 +1409,8 @@
             this::onAppForegroundChanged;
     private final GlobalSettingChangedListener mBackgroundThrottleIntervalChangedListener =
             this::onBackgroundThrottleIntervalChanged;
+    private final GlobalSettingChangedListener mAdasPackageAllowlistChangedListener =
+            this::onAdasAllowlistChanged;
     private final GlobalSettingChangedListener mIgnoreSettingsPackageWhitelistChangedListener =
             this::onIgnoreSettingsWhitelistChanged;
     private final LocationPowerSaveModeChangedListener mLocationPowerSaveModeChangedListener =
@@ -1710,6 +1715,9 @@
             } else if (!mLocationSettings.getUserSettings(
                     identity.getUserId()).isAdasGnssLocationEnabled()) {
                 adasGnssBypass = false;
+            } else if (!mSettingsHelper.getAdasAllowlist().contains(
+                    identity.getPackageName(), identity.getAttributionTag())) {
+                adasGnssBypass = false;
             }
 
             builder.setAdasGnssBypass(adasGnssBypass);
@@ -1979,6 +1987,8 @@
                 mBackgroundThrottlePackageWhitelistChangedListener);
         mSettingsHelper.addOnLocationPackageBlacklistChangedListener(
                 mLocationPackageBlacklistChangedListener);
+        mSettingsHelper.addAdasAllowlistChangedListener(
+                mAdasPackageAllowlistChangedListener);
         mSettingsHelper.addIgnoreSettingsAllowlistChangedListener(
                 mIgnoreSettingsPackageWhitelistChangedListener);
         mLocationPermissionsHelper.addListener(mLocationPermissionsListener);
@@ -2000,6 +2010,7 @@
                 mBackgroundThrottlePackageWhitelistChangedListener);
         mSettingsHelper.removeOnLocationPackageBlacklistChangedListener(
                 mLocationPackageBlacklistChangedListener);
+        mSettingsHelper.removeAdasAllowlistChangedListener(mAdasPackageAllowlistChangedListener);
         mSettingsHelper.removeIgnoreSettingsAllowlistChangedListener(
                 mIgnoreSettingsPackageWhitelistChangedListener);
         mLocationPermissionsHelper.removeListener(mLocationPermissionsListener);
@@ -2422,6 +2433,12 @@
         }
     }
 
+    private void onAdasAllowlistChanged() {
+        synchronized (mLock) {
+            updateRegistrations(Registration::onProviderLocationRequestChanged);
+        }
+    }
+
     private void onIgnoreSettingsWhitelistChanged() {
         synchronized (mLock) {
             updateRegistrations(Registration::onProviderLocationRequestChanged);
diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java
index 0aa384c..015bf3c5 100644
--- a/services/core/java/com/android/server/logcat/LogcatManagerService.java
+++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java
@@ -183,7 +183,8 @@
 
                 ActivityManagerInternal ami = LocalServices.getService(
                         ActivityManagerInternal.class);
-                boolean isCallerInstrumented = ami.isUidCurrentlyInstrumented(mUid);
+                boolean isCallerInstrumented =
+                        ami.getInstrumentationSourceUid(mUid) != android.os.Process.INVALID_UID;
 
                 // The instrumented apks only run for testing, so we don't check user permission.
                 if (isCallerInstrumented) {
diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java
index bf1196d..ed71f1e 100644
--- a/services/core/java/com/android/server/pm/BroadcastHelper.java
+++ b/services/core/java/com/android/server/pm/BroadcastHelper.java
@@ -20,7 +20,6 @@
 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
 
 import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
-import static com.android.server.pm.PackageManagerService.EMPTY_INT_ARRAY;
 import static com.android.server.pm.PackageManagerService.PACKAGE_SCHEME;
 import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
 import static com.android.server.pm.PackageManagerService.TAG;
@@ -86,11 +85,14 @@
             } else {
                 resolvedUserIds = userIds;
             }
-            doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver,
-                    resolvedUserIds, false, broadcastAllowList, bOptions);
-            if (instantUserIds != null && instantUserIds != EMPTY_INT_ARRAY) {
+
+            if (ArrayUtils.isEmpty(instantUserIds)) {
                 doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver,
-                        instantUserIds, true, null, bOptions);
+                        resolvedUserIds, false /* isInstantApp */, broadcastAllowList, bOptions);
+            } else {
+                // send restricted broadcasts for instant apps
+                doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver,
+                        instantUserIds, true /* isInstantApp */, null, bOptions);
             }
         } catch (RemoteException ex) {
         }
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index bb2ba5c..249099d 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -358,9 +358,7 @@
         }
         final long callingId = Binder.clearCallingIdentity();
         try {
-            synchronized (mPm.mInstallLock) {
-                return performDexOptInternalWithDependenciesLI(p, pkgSetting, options);
-            }
+            return performDexOptInternalWithDependenciesLI(p, pkgSetting, options);
         } finally {
             Binder.restoreCallingIdentity(callingId);
         }
@@ -429,20 +427,18 @@
             throw new IllegalArgumentException("Unknown package: " + packageName);
         }
 
-        synchronized (mPm.mInstallLock) {
-            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
+        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "dexopt");
 
-            // Whoever is calling forceDexOpt wants a compiled package.
-            // Don't use profiles since that may cause compilation to be skipped.
-            final int res = performDexOptInternalWithDependenciesLI(pkg, packageState,
-                    new DexoptOptions(packageName,
-                            getDefaultCompilerFilter(),
-                            DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE));
+        // Whoever is calling forceDexOpt wants a compiled package.
+        // Don't use profiles since that may cause compilation to be skipped.
+        final int res = performDexOptInternalWithDependenciesLI(pkg, packageState,
+                new DexoptOptions(packageName,
+                        getDefaultCompilerFilter(),
+                        DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE));
 
-            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
-            if (res != PackageDexOptimizer.DEX_OPT_PERFORMED) {
-                throw new IllegalStateException("Failed to dexopt: " + res);
-            }
+        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+        if (res != PackageDexOptimizer.DEX_OPT_PERFORMED) {
+            throw new IllegalStateException("Failed to dexopt: " + res);
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/InitAppsHelper.java b/services/core/java/com/android/server/pm/InitAppsHelper.java
index 15f26e7..154f32a 100644
--- a/services/core/java/com/android/server/pm/InitAppsHelper.java
+++ b/services/core/java/com/android/server/pm/InitAppsHelper.java
@@ -30,7 +30,7 @@
 import static com.android.server.pm.PackageManagerService.SCAN_REQUIRE_KNOWN;
 import static com.android.server.pm.PackageManagerService.SYSTEM_PARTITIONS;
 import static com.android.server.pm.PackageManagerService.TAG;
-import static com.android.server.pm.pkg.parsing.ParsingPackageUtils.PARSE_CHECK_MAX_SDK_VERSION;
+import static com.android.server.pm.pkg.parsing.ParsingPackageUtils.PARSE_APK_IN_APEX;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -359,7 +359,7 @@
         try {
             if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) {
                 // when scanning apk in apexes, we want to check the maxSdkVersion
-                parseFlags |= PARSE_CHECK_MAX_SDK_VERSION;
+                parseFlags |= PARSE_APK_IN_APEX;
             }
             mInstallPackageHelper.installPackagesFromDir(scanDir, frameworkSplits, parseFlags,
                     scanFlags, packageParser, executorService);
diff --git a/services/core/java/com/android/server/pm/IntentResolverInterceptor.java b/services/core/java/com/android/server/pm/IntentResolverInterceptor.java
index 603badb..f5eee5a 100644
--- a/services/core/java/com/android/server/pm/IntentResolverInterceptor.java
+++ b/services/core/java/com/android/server/pm/IntentResolverInterceptor.java
@@ -23,11 +23,12 @@
 import android.annotation.RequiresPermission;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
 import android.content.res.Resources;
 import android.provider.DeviceConfig;
+import android.util.Slog;
 
 import com.android.internal.R;
+import com.android.internal.app.ChooserActivity;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.server.LocalServices;
 import com.android.server.wm.ActivityInterceptorCallback;
@@ -35,25 +36,28 @@
 import com.android.server.wm.ActivityTaskManagerInternal;
 
 /**
- * Service to register an {@code ActivityInterceptorCallback} that modifies any {@code Intent}
- * that's being used to launch a user-space {@code ChooserActivity} by setting the destination
- * component to the delegated component when appropriate.
+ * Redirects Activity starts for the system bundled {@link ChooserActivity} to an external
+ * Sharesheet implementation by modifying the target component when appropriate.
+ * <p>
+ * Note: config_chooserActivity (Used also by ActivityTaskSupervisor) is already updated to point
+ * to the new instance. This value is read and used for the new target component.
  */
 public final class IntentResolverInterceptor {
     private static final String TAG = "IntentResolverIntercept";
-
     private final Context mContext;
-    private boolean mUseDelegateChooser;
+    private final ComponentName mFrameworkChooserComponent;
+    private final ComponentName mUnbundledChooserComponent;
+    private boolean mUseUnbundledSharesheet;
 
     private final ActivityInterceptorCallback mActivityInterceptorCallback =
             new ActivityInterceptorCallback() {
                 @Nullable
                 @Override
                 public ActivityInterceptResult intercept(ActivityInterceptorInfo info) {
-                    if (mUseDelegateChooser && isChooserActivity(info)) {
-                        return new ActivityInterceptResult(
-                                modifyChooserIntent(info.intent),
-                                info.checkedOptions);
+                    if (mUseUnbundledSharesheet && isSystemChooserActivity(info)) {
+                        Slog.d(TAG, "Redirecting to UNBUNDLED Sharesheet");
+                        info.intent.setComponent(mUnbundledChooserComponent);
+                        return new ActivityInterceptResult(info.intent, info.checkedOptions);
                     }
                     return null;
                 }
@@ -61,10 +65,13 @@
 
     public IntentResolverInterceptor(Context context) {
         mContext = context;
+        mFrameworkChooserComponent = new ComponentName(mContext, ChooserActivity.class);
+        mUnbundledChooserComponent =  ComponentName.unflattenFromString(
+                Resources.getSystem().getString(R.string.config_chooserActivity));
     }
 
     /**
-     * Start listening for intents and USE_DELEGATE_CHOOSER property changes.
+     * Start listening for intents and USE_UNBUNDLED_SHARESHEET property changes.
      */
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
     public void registerListeners() {
@@ -73,36 +80,25 @@
                         mActivityInterceptorCallback);
 
         DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
-                mContext.getMainExecutor(), properties -> updateUseDelegateChooser());
-        updateUseDelegateChooser();
+                mContext.getMainExecutor(), properties -> updateUseUnbundledSharesheet());
+        updateUseUnbundledSharesheet();
     }
 
     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
-    private void updateUseDelegateChooser() {
-        mUseDelegateChooser = DeviceConfig.getBoolean(
+    private void updateUseUnbundledSharesheet() {
+        mUseUnbundledSharesheet = DeviceConfig.getBoolean(
                 DeviceConfig.NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.USE_UNBUNDLED_SHARESHEET,
                 false);
+        if (mUseUnbundledSharesheet) {
+            Slog.d(TAG, "using UNBUNDLED Sharesheet");
+        } else {
+            Slog.d(TAG, "using FRAMEWORK Sharesheet");
+        }
     }
 
-    private Intent modifyChooserIntent(Intent intent) {
-        intent.setComponent(getUnbundledChooserComponentName());
-        return intent;
-    }
-
-    private static boolean isChooserActivity(ActivityInterceptorInfo info) {
-        ComponentName targetComponent = new ComponentName(info.aInfo.packageName, info.aInfo.name);
-
-        return targetComponent.equals(getSystemChooserComponentName())
-                || targetComponent.equals(getUnbundledChooserComponentName());
-    }
-
-    private static ComponentName getSystemChooserComponentName() {
-        return new ComponentName("android", "com.android.internal.app.ChooserActivity");
-    }
-
-    private static ComponentName getUnbundledChooserComponentName() {
-        return ComponentName.unflattenFromString(
-                Resources.getSystem().getString(R.string.config_chooserActivity));
+    private boolean isSystemChooserActivity(ActivityInterceptorInfo info) {
+        return mFrameworkChooserComponent.getPackageName().equals(info.aInfo.packageName)
+                && mFrameworkChooserComponent.getClassName().equals(info.aInfo.name);
     }
 }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index bf80a46..f6d8d72 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -3536,6 +3536,8 @@
                     EventLog.writeEvent(0x534e4554, "145981139", packageInfo.applicationInfo.uid,
                             "");
                 }
+                Log.w(TAG, "Missing required system package: " + packageName + (packageInfo != null
+                        ? ", but found with extended search." : "."));
                 return null;
             }
         } finally {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index e142ed6..d6ab78b 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -3694,7 +3694,7 @@
             }
             List<PermissionInfo> ps = mPermissionManager
                     .queryPermissionsByGroup(groupList.get(i), 0 /*flags*/);
-            final int count = ps.size();
+            final int count = (ps == null ? 0 : ps.size());
             boolean first = true;
             for (int p = 0 ; p < count ; p++) {
                 PermissionInfo pi = ps.get(p);
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index d340561..ee0fdc0 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -5413,190 +5413,170 @@
         (new Shell()).exec(this, in, out, err, args, callback, resultReceiver);
     }
 
-    private static final String PREFIX_HELP_COMMAND = "  ";
-    private static final String PREFIX_HELP_DESCRIPTION = "    ";
-    private static final String PREFIX_HELP_DESCRIPTION_EXTRA_LINES = "      ";
-
-    private static final String CMD_HELP = "help";
-    private static final String CMD_LIST = "list";
-    private static final String CMD_REPORT_SYSTEM_USER_PACKAGE_ALLOWLIST_PROBLEMS =
-            "report-system-user-package-whitelist-problems";
-
-    private static final String ARG_V = "-v";
-    private static final String ARG_VERBOSE = "--verbose";
-    private static final String ARG_ALL = "--all";
-    private static final String ARG_CRITICAL_ONLY = "--critical-only";
-    private static final String ARG_MODE = "--mode";
-
     private final class Shell extends ShellCommand {
 
-    @Override
-    public void onHelp() {
-        final PrintWriter pw = getOutPrintWriter();
-        pw.printf("User manager (user) commands:\n");
-
-        pw.printf("%s%s\n", PREFIX_HELP_COMMAND, CMD_HELP);
-        pw.printf("%sPrints this help text.\n\n", PREFIX_HELP_DESCRIPTION);
-
-        pw.printf("%s%s [%s] [%s]\n", PREFIX_HELP_COMMAND, CMD_LIST, ARG_V, ARG_ALL);
-        pw.printf("%sPrints all users on the system.\n\n", PREFIX_HELP_DESCRIPTION);
-
-        pw.printf("%s%s [%s | %s] [%s] [%s MODE]\n", PREFIX_HELP_COMMAND,
-                CMD_REPORT_SYSTEM_USER_PACKAGE_ALLOWLIST_PROBLEMS,
-                ARG_V, ARG_VERBOSE, ARG_CRITICAL_ONLY, ARG_MODE);
-
-        pw.printf("%sReports all issues on user-type package allowlist XML files. Options:\n",
-                PREFIX_HELP_DESCRIPTION);
-        pw.printf("%s%s | %s: shows extra info, like number of issues\n",
-                PREFIX_HELP_DESCRIPTION, ARG_V, ARG_VERBOSE);
-        pw.printf("%s%s: show only critical issues, excluding warnings\n",
-                PREFIX_HELP_DESCRIPTION, ARG_CRITICAL_ONLY);
-        pw.printf("%s%s MODE: shows what errors would be if device used mode MODE\n"
-                + "%s(where MODE is the allowlist mode integer as defined by "
-                + "config_userTypePackageWhitelistMode)\n\n",
-                PREFIX_HELP_DESCRIPTION, ARG_MODE, PREFIX_HELP_DESCRIPTION_EXTRA_LINES);
-    }
-
-    @Override
-    public int onCommand(String cmd) {
-        if (cmd == null) {
-            return handleDefaultCommands(cmd);
+        @Override
+        public void onHelp() {
+            final PrintWriter pw = getOutPrintWriter();
+            pw.println("User manager (user) commands:");
+            pw.println("  help");
+            pw.println("    Prints this help text.");
+            pw.println();
+            pw.println("  list [-v | --verbose] [--all]");
+            pw.println("    Prints all users on the system.");
+            pw.println();
+            pw.println("  report-system-user-package-whitelist-problems [-v | --verbose] "
+                    + "[--critical-only] [--mode MODE]");
+            pw.println("    Reports all issues on user-type package allowlist XML files. Options:");
+            pw.println("    -v | --verbose: shows extra info, like number of issues");
+            pw.println("    --critical-only: show only critical issues, excluding warnings");
+            pw.println("    --mode MODE: shows what errors would be if device used mode MODE");
+            pw.println("      (where MODE is the allowlist mode integer as defined by "
+                    + "config_userTypePackageWhitelistMode)");
         }
 
-        try {
-            switch(cmd) {
-                case CMD_LIST:
-                    return runList();
-                case CMD_REPORT_SYSTEM_USER_PACKAGE_ALLOWLIST_PROBLEMS:
-                    return runReportPackageAllowlistProblems();
-                default:
-                    return handleDefaultCommands(cmd);
+        @Override
+        public int onCommand(String cmd) {
+            if (cmd == null) {
+                return handleDefaultCommands(cmd);
             }
-        } catch (RemoteException e) {
-            getOutPrintWriter().println("Remote exception: " + e);
-        }
-        return -1;
-    }
 
-    private int runList() throws RemoteException {
-        final PrintWriter pw = getOutPrintWriter();
-        boolean all = false;
-        boolean verbose = false;
-        String opt;
-        while ((opt = getNextOption()) != null) {
-            switch (opt) {
-                case ARG_V:
-                    verbose = true;
-                    break;
-                case ARG_ALL:
-                    all = true;
-                    break;
-                default:
-                    pw.println("Invalid option: " + opt);
-                    return -1;
+            try {
+                switch(cmd) {
+                    case "list":
+                        return runList();
+                    case "report-system-user-package-whitelist-problems":
+                        return runReportPackageAllowlistProblems();
+                    default:
+                        return handleDefaultCommands(cmd);
+                }
+            } catch (RemoteException e) {
+                getOutPrintWriter().println("Remote exception: " + e);
             }
+            return -1;
         }
-        final IActivityManager am = ActivityManager.getService();
-        final List<UserInfo> users = getUsers(/* excludePartial= */ !all,
-                /* excludingDying=*/ false, /* excludePreCreated= */ !all);
-        if (users == null) {
-            pw.println("Error: couldn't get users");
-            return 1;
-        } else {
-            final int size = users.size();
-            int currentUser = UserHandle.USER_NULL;
-            if (verbose) {
-                pw.printf("%d users:\n\n", size);
-                currentUser = am.getCurrentUser().id;
+
+        private int runList() throws RemoteException {
+            final PrintWriter pw = getOutPrintWriter();
+            boolean all = false;
+            boolean verbose = false;
+            String opt;
+            while ((opt = getNextOption()) != null) {
+                switch (opt) {
+                    case "-v":
+                    case "--verbose":
+                        verbose = true;
+                        break;
+                    case "--all":
+                        all = true;
+                        break;
+                    default:
+                        pw.println("Invalid option: " + opt);
+                        return -1;
+                }
+            }
+            final IActivityManager am = ActivityManager.getService();
+            final List<UserInfo> users = getUsers(/* excludePartial= */ !all,
+                    /* excludeDying= */ false, /* excludePreCreated= */ !all);
+            if (users == null) {
+                pw.println("Error: couldn't get users");
+                return 1;
             } else {
-                // NOTE: the standard "list users" command is used by integration tests and
-                // hence should not be changed. If you need to add more info, use the
-                // verbose option.
-                pw.println("Users:");
-            }
-            for (int i = 0; i < size; i++) {
-                final UserInfo user = users.get(i);
-                final boolean running = am.isUserRunning(user.id, 0);
-                final boolean current = user.id == currentUser;
-                final boolean hasParent = user.profileGroupId != user.id
-                        && user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
+                final int size = users.size();
+                int currentUser = UserHandle.USER_NULL;
                 if (verbose) {
-                    final DevicePolicyManagerInternal dpm = getDevicePolicyManagerInternal();
-                    String deviceOwner = "";
-                    String profileOwner = "";
-                    if (dpm != null) {
-                        final long ident = Binder.clearCallingIdentity();
-                        // NOTE: dpm methods below CANNOT be called while holding the mUsersLock
-                        try {
-                            if (dpm.getDeviceOwnerUserId() == user.id) {
-                                deviceOwner = " (device-owner)";
-                            }
-                            if (dpm.getProfileOwnerAsUser(user.id) != null) {
-                                profileOwner = " (profile-owner)";
-                            }
-                        } finally {
-                            Binder.restoreCallingIdentity(ident);
-                        }
-                    }
-                    pw.printf("%d: id=%d, name=%s, type=%s, flags=%s%s%s%s%s%s%s%s%s\n",
-                            i,
-                            user.id,
-                            user.name,
-                            user.userType.replace("android.os.usertype.", ""),
-                            UserInfo.flagsToString(user.flags),
-                            hasParent ? " (parentId=" + user.profileGroupId + ")" : "",
-                            running ? " (running)" : "",
-                            user.partial ? " (partial)" : "",
-                            user.preCreated ? " (pre-created)" : "",
-                            user.convertedFromPreCreated ? " (converted)" : "",
-                            deviceOwner, profileOwner,
-                            current ? " (current)" : "");
+                    pw.printf("%d users:\n\n", size);
+                    currentUser = am.getCurrentUser().id;
                 } else {
                     // NOTE: the standard "list users" command is used by integration tests and
                     // hence should not be changed. If you need to add more info, use the
                     // verbose option.
-                    pw.printf("\t%s%s\n", user, running ? " running" : "");
+                    pw.println("Users:");
                 }
+                for (int i = 0; i < size; i++) {
+                    final UserInfo user = users.get(i);
+                    final boolean running = am.isUserRunning(user.id, 0);
+                    final boolean current = user.id == currentUser;
+                    final boolean hasParent = user.profileGroupId != user.id
+                            && user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID;
+                    if (verbose) {
+                        final DevicePolicyManagerInternal dpm = getDevicePolicyManagerInternal();
+                        String deviceOwner = "";
+                        String profileOwner = "";
+                        if (dpm != null) {
+                            final long ident = Binder.clearCallingIdentity();
+                            // NOTE: dpm methods below CANNOT be called while holding the mUsersLock
+                            try {
+                                if (dpm.getDeviceOwnerUserId() == user.id) {
+                                    deviceOwner = " (device-owner)";
+                                }
+                                if (dpm.getProfileOwnerAsUser(user.id) != null) {
+                                    profileOwner = " (profile-owner)";
+                                }
+                            } finally {
+                                Binder.restoreCallingIdentity(ident);
+                            }
+                        }
+                        pw.printf("%d: id=%d, name=%s, type=%s, flags=%s%s%s%s%s%s%s%s%s\n",
+                                i,
+                                user.id,
+                                user.name,
+                                user.userType.replace("android.os.usertype.", ""),
+                                UserInfo.flagsToString(user.flags),
+                                hasParent ? " (parentId=" + user.profileGroupId + ")" : "",
+                                running ? " (running)" : "",
+                                user.partial ? " (partial)" : "",
+                                user.preCreated ? " (pre-created)" : "",
+                                user.convertedFromPreCreated ? " (converted)" : "",
+                                deviceOwner, profileOwner,
+                                current ? " (current)" : "");
+                    } else {
+                        // NOTE: the standard "list users" command is used by integration tests and
+                        // hence should not be changed. If you need to add more info, use the
+                        // verbose option.
+                        pw.printf("\t%s%s\n", user, running ? " running" : "");
+                    }
+                }
+                return 0;
+            }
+        }
+
+        private int runReportPackageAllowlistProblems() {
+            final PrintWriter pw = getOutPrintWriter();
+            boolean verbose = false;
+            boolean criticalOnly = false;
+            int mode = UserSystemPackageInstaller.USER_TYPE_PACKAGE_WHITELIST_MODE_NONE;
+            String opt;
+            while ((opt = getNextOption()) != null) {
+                switch (opt) {
+                    case "-v":
+                    case "--verbose":
+                        verbose = true;
+                        break;
+                    case "--critical-only":
+                        criticalOnly = true;
+                        break;
+                    case "--mode":
+                        mode = Integer.parseInt(getNextArgRequired());
+                        break;
+                    default:
+                        pw.println("Invalid option: " + opt);
+                        return -1;
+                }
+            }
+
+            Slog.d(LOG_TAG, "runReportPackageAllowlistProblems(): verbose=" + verbose
+                    + ", criticalOnly=" + criticalOnly
+                    + ", mode=" + UserSystemPackageInstaller.modeToString(mode));
+
+            try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ")) {
+                mSystemPackageInstaller.dumpPackageWhitelistProblems(ipw, mode, verbose,
+                        criticalOnly);
             }
             return 0;
         }
-    }
 
-    private int runReportPackageAllowlistProblems() {
-        final PrintWriter pw = getOutPrintWriter();
-        boolean verbose = false;
-        boolean criticalOnly = false;
-        int mode = UserSystemPackageInstaller.USER_TYPE_PACKAGE_WHITELIST_MODE_NONE;
-        String opt;
-        while ((opt = getNextOption()) != null) {
-            switch (opt) {
-                case ARG_V:
-                case ARG_VERBOSE:
-                    verbose = true;
-                    break;
-                case ARG_CRITICAL_ONLY:
-                    criticalOnly = true;
-                    break;
-                case ARG_MODE:
-                    mode = Integer.parseInt(getNextArgRequired());
-                    break;
-                default:
-                    pw.println("Invalid option: " + opt);
-                    return -1;
-            }
-        }
-
-        Slog.d(LOG_TAG, "runReportPackageAllowlistProblems(): verbose=" + verbose
-                + ", criticalOnly=" + criticalOnly
-                + ", mode=" + UserSystemPackageInstaller.modeToString(mode));
-
-        try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw, "  ")) {
-            mSystemPackageInstaller.dumpPackageWhitelistProblems(ipw, mode, verbose,
-                    criticalOnly);
-        }
-        return 0;
-    }
-    }
+    } // class Shell
 
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
diff --git a/services/core/java/com/android/server/pm/dex/ArtManagerService.java b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
index 7e4da94..4fae6b8 100644
--- a/services/core/java/com/android/server/pm/dex/ArtManagerService.java
+++ b/services/core/java/com/android/server/pm/dex/ArtManagerService.java
@@ -45,7 +45,6 @@
 import android.util.Log;
 import android.util.Slog;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.os.RoSystemProperties;
 import com.android.internal.util.ArrayUtils;
@@ -94,8 +93,6 @@
 
     private final Context mContext;
     private IPackageManager mPackageManager;
-    private final Object mInstallLock;
-    @GuardedBy("mInstallLock")
     private final Installer mInstaller;
 
     private final Handler mHandler;
@@ -105,10 +102,9 @@
     }
 
     public ArtManagerService(Context context, Installer installer,
-            Object installLock) {
+            Object ignored) {
         mContext = context;
         mInstaller = installer;
-        mInstallLock = installLock;
         mHandler = new Handler(BackgroundThread.getHandler().getLooper());
 
         LocalServices.addService(ArtManagerInternal.class, new ArtManagerInternalImpl());
@@ -273,16 +269,14 @@
     private void createProfileSnapshot(String packageName, String profileName, String classpath,
             int appId, ISnapshotRuntimeProfileCallback callback) {
         // Ask the installer to snapshot the profile.
-        synchronized (mInstallLock) {
-            try {
-                if (!mInstaller.createProfileSnapshot(appId, packageName, profileName, classpath)) {
-                    postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR);
-                    return;
-                }
-            } catch (InstallerException e) {
+        try {
+            if (!mInstaller.createProfileSnapshot(appId, packageName, profileName, classpath)) {
                 postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR);
                 return;
             }
+        } catch (InstallerException e) {
+            postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR);
+            return;
         }
 
         // Open the snapshot and invoke the callback.
@@ -308,13 +302,11 @@
             Slog.d(TAG, "Destroying profile snapshot for" + packageName + ":" + profileName);
         }
 
-        synchronized (mInstallLock) {
-            try {
-                mInstaller.destroyProfileSnapshot(packageName, profileName);
-            } catch (InstallerException e) {
-                Slog.e(TAG, "Failed to destroy profile snapshot for " +
-                    packageName + ":" + profileName, e);
-            }
+        try {
+            mInstaller.destroyProfileSnapshot(packageName, profileName);
+        } catch (InstallerException e) {
+            Slog.e(TAG, "Failed to destroy profile snapshot for " + packageName + ":" + profileName,
+                    e);
         }
     }
 
@@ -480,9 +472,7 @@
             for (int i = packageProfileNames.size() - 1; i >= 0; i--) {
                 String codePath = packageProfileNames.keyAt(i);
                 String profileName = packageProfileNames.valueAt(i);
-                synchronized (mInstallLock) {
-                    mInstaller.dumpProfiles(sharedGid, pkg.getPackageName(), profileName, codePath);
-                }
+                mInstaller.dumpProfiles(sharedGid, pkg.getPackageName(), profileName, codePath);
             }
         } catch (InstallerException e) {
             Slog.w(TAG, "Failed to dump profiles", e);
@@ -512,10 +502,8 @@
                     ") to " + outDexFile);
             final long callingId = Binder.clearCallingIdentity();
             try {
-                synchronized (mInstallLock) {
-                    return mInstaller.compileLayouts(apkPath, packageName, outDexFile,
-                            pkg.getUid());
-                }
+                return mInstaller.compileLayouts(apkPath, packageName, outDexFile,
+                        pkg.getUid());
             } finally {
                 Binder.restoreCallingIdentity(callingId);
             }
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 f255db4..9897c42 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
@@ -209,8 +209,6 @@
 
     public static final int SDK_VERSION = Build.VERSION.SDK_INT;
     public static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES;
-    public static final String[] PREVIOUS_CODENAMES =
-            Build.VERSION.KNOWN_CODENAMES.toArray(new String[]{});
 
     public static boolean sCompatibilityModeEnabled = true;
     public static boolean sUseRoundIcon = false;
@@ -238,7 +236,7 @@
      */
     public static final int PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY = 1 << 7;
     public static final int PARSE_FRAMEWORK_RES_SPLITS = 1 << 8;
-    public static final int PARSE_CHECK_MAX_SDK_VERSION = 1 << 9;
+    public static final int PARSE_APK_IN_APEX = 1 << 9;
 
     public static final int PARSE_CHATTY = 1 << 31;
 
@@ -1534,7 +1532,7 @@
             ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags)
             throws IOException, XmlPullParserException {
         if (SDK_VERSION > 0) {
-            final boolean checkMaxSdkVersion = (flags & PARSE_CHECK_MAX_SDK_VERSION) != 0;
+            final boolean isApkInApex = (flags & PARSE_APK_IN_APEX) != 0;
             TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesSdk);
             try {
                 int minVers = ParsingUtils.DEFAULT_MIN_SDK_VERSION;
@@ -1569,7 +1567,7 @@
                     targetCode = minCode;
                 }
 
-                if (checkMaxSdkVersion) {
+                if (isApkInApex) {
                     val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_maxSdkVersion);
                     if (val != null) {
                         // maxSdkVersion only supports integer
@@ -1578,7 +1576,8 @@
                 }
 
                 ParseResult<Integer> targetSdkVersionResult = FrameworkParsingPackageUtils
-                        .computeTargetSdkVersion(targetVers, targetCode, SDK_CODENAMES, input);
+                        .computeTargetSdkVersion(targetVers, targetCode, SDK_CODENAMES, input,
+                                isApkInApex);
                 if (targetSdkVersionResult.isError()) {
                     return input.error(targetSdkVersionResult);
                 }
@@ -1601,7 +1600,7 @@
 
                 pkg.setMinSdkVersion(minSdkVersion)
                         .setTargetSdkVersion(targetSdkVersion);
-                if (checkMaxSdkVersion) {
+                if (isApkInApex) {
                     ParseResult<Integer> maxSdkVersionResult = FrameworkParsingPackageUtils
                             .computeMaxSdkVersion(maxVers, SDK_VERSION, input);
                     if (maxSdkVersionResult.isError()) {
diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java
index 70d69c6..e157a27 100644
--- a/services/core/java/com/android/server/policy/AppOpsPolicy.java
+++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java
@@ -187,8 +187,12 @@
 
         initializeActivityRecognizersTags();
 
-        // If this device does not have telephony, restrict the phone call ops
-        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
+        // Restrict phone call ops if the TelecomService will not start (conditioned on having
+        // FEATURE_MICROPHONE, FEATURE_TELECOM, or FEATURE_TELEPHONY).
+        PackageManager pm = mContext.getPackageManager();
+        if (!pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+                && !pm.hasSystemFeature(PackageManager.FEATURE_MICROPHONE)
+                && !pm.hasSystemFeature(PackageManager.FEATURE_TELECOM)) {
             AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
             appOps.setUserRestrictionForUser(AppOpsManager.OP_PHONE_CALL_MICROPHONE, true, mToken,
                     null, UserHandle.USER_ALL);
diff --git a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
index 2f68f56..bce1cce 100644
--- a/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
+++ b/services/core/java/com/android/server/sensorprivacy/SensorPrivacyService.java
@@ -29,6 +29,7 @@
 import static android.content.Intent.EXTRA_PACKAGE_NAME;
 import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
 import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.hardware.SensorPrivacyManager.EXTRA_ALL_SENSORS;
 import static android.hardware.SensorPrivacyManager.EXTRA_SENSOR;
@@ -77,6 +78,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
 import android.content.res.Configuration;
 import android.graphics.drawable.Icon;
 import android.hardware.ISensorPrivacyListener;
@@ -155,6 +157,7 @@
     private final AppOpsManager mAppOpsManager;
     private final AppOpsManagerInternal mAppOpsManagerInternal;
     private final TelephonyManager mTelephonyManager;
+    private final PackageManagerInternal mPackageManagerInternal;
 
     private CameraPrivacyLightController mCameraPrivacyLightController;
 
@@ -178,6 +181,7 @@
         mActivityManagerInternal = getLocalService(ActivityManagerInternal.class);
         mActivityTaskManager = context.getSystemService(ActivityTaskManager.class);
         mTelephonyManager = context.getSystemService(TelephonyManager.class);
+        mPackageManagerInternal = getLocalService(PackageManagerInternal.class);
         mSensorPrivacyServiceImpl = new SensorPrivacyServiceImpl();
     }
 
@@ -828,6 +832,12 @@
          * sensor privacy.
          */
         private void enforceObserveSensorPrivacyPermission() {
+            String systemUIPackage = mContext.getString(R.string.config_systemUi);
+            if (Binder.getCallingUid() == mPackageManagerInternal
+                    .getPackageUid(systemUIPackage, MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM)) {
+                // b/221782106, possible race condition with role grant might bootloop device.
+                return;
+            }
             enforcePermission(android.Manifest.permission.OBSERVE_SENSOR_PRIVACY,
                     "Observing sensor privacy changes requires the following permission: "
                             + android.Manifest.permission.OBSERVE_SENSOR_PRIVACY);
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java
index c0ab65a..05d92be 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java
@@ -17,7 +17,6 @@
 package com.android.server.soundtrigger_middleware;
 
 import android.annotation.NonNull;
-import android.media.permission.SafeCloseable;
 import android.media.soundtrigger.ModelParameterRange;
 import android.media.soundtrigger.PhraseRecognitionEvent;
 import android.media.soundtrigger.PhraseSoundModel;
@@ -30,6 +29,7 @@
 import android.os.IBinder;
 
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.Map;
 import java.util.Queue;
@@ -63,18 +63,24 @@
  */
 public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal,
         ICaptureStateNotifier.Listener {
-    private final @NonNull ISoundTriggerHal mDelegate;
+    @NonNull private final ISoundTriggerHal mDelegate;
     private GlobalCallback mGlobalCallback;
+    /**
+     * This lock must be held to synchronize forward calls (start/stop/onCaptureStateChange) that
+     * update the mActiveModels set and mCaptureState.
+     * It must not be locked in HAL callbacks to avoid deadlocks.
+     */
+    @NonNull private final Object mStartStopLock = new Object();
 
     /**
      * Information about a model that is currently loaded. This is needed in order to be able to
      * send abort events to its designated callback.
      */
     private static class LoadedModel {
-        final int type;
-        final @NonNull ModelCallback callback;
+        public final int type;
+        @NonNull public final ModelCallback callback;
 
-        private LoadedModel(int type, @NonNull ModelCallback callback) {
+        LoadedModel(int type, @NonNull ModelCallback callback) {
             this.type = type;
             this.callback = callback;
         }
@@ -83,19 +89,19 @@
     /**
      * This map holds the model type for every model that is loaded.
      */
-    private final @NonNull Map<Integer, LoadedModel> mLoadedModels = new ConcurrentHashMap<>();
+    @NonNull private final Map<Integer, LoadedModel> mLoadedModels = new ConcurrentHashMap<>();
 
     /**
      * A set of all models that are currently active.
      * We use this in order to know which models to stop in case of external capture.
      * Used as a lock to synchronize operations that effect activity.
      */
-    private final @NonNull Set<Integer> mActiveModels = new HashSet<>();
+    @NonNull private final Set<Integer> mActiveModels = new HashSet<>();
 
     /**
      * Notifier for changes in capture state.
      */
-    private final @NonNull ICaptureStateNotifier mNotifier;
+    @NonNull private final ICaptureStateNotifier mNotifier;
 
     /**
      * Whether capture is active.
@@ -106,10 +112,10 @@
      * Since we're wrapping the death recipient, we need to keep a translation map for unlinking.
      * Key is the client recipient, value is the wrapper.
      */
-    private final @NonNull Map<IBinder.DeathRecipient, IBinder.DeathRecipient>
+    @NonNull private final Map<IBinder.DeathRecipient, IBinder.DeathRecipient>
             mDeathRecipientMap = new ConcurrentHashMap<>();
 
-    private final @NonNull CallbackThread mCallbackThread = new CallbackThread();
+    @NonNull private final CallbackThread mCallbackThread = new CallbackThread();
 
     public SoundTriggerHalConcurrentCaptureHandler(
             @NonNull ISoundTriggerHal delegate,
@@ -122,20 +128,28 @@
     @Override
     public void startRecognition(int modelHandle, int deviceHandle, int ioHandle,
             RecognitionConfig config) {
-        synchronized (mActiveModels) {
-            if (mCaptureState) {
-                throw new RecoverableException(Status.RESOURCE_CONTENTION);
+        synchronized (mStartStopLock) {
+            synchronized (mActiveModels) {
+                if (mCaptureState) {
+                    throw new RecoverableException(Status.RESOURCE_CONTENTION);
+                }
+                mDelegate.startRecognition(modelHandle, deviceHandle, ioHandle, config);
+                mActiveModels.add(modelHandle);
             }
-            mDelegate.startRecognition(modelHandle, deviceHandle, ioHandle, config);
-            mActiveModels.add(modelHandle);
         }
     }
 
     @Override
     public void stopRecognition(int modelHandle) {
-        synchronized (mActiveModels) {
-            mDelegate.stopRecognition(modelHandle);
-            mActiveModels.remove(modelHandle);
+        synchronized (mStartStopLock) {
+            boolean wasActive;
+            synchronized (mActiveModels) {
+                wasActive = mActiveModels.remove(modelHandle);
+            }
+            if (wasActive) {
+                // Must be done outside of the lock, since it may trigger synchronous callbacks.
+                mDelegate.stopRecognition(modelHandle);
+            }
         }
         // Block until all previous events are delivered. Since this is potentially blocking on
         // upward calls, it must be done outside the lock.
@@ -144,27 +158,38 @@
 
     @Override
     public void onCaptureStateChange(boolean active) {
-        synchronized (mActiveModels) {
+        synchronized (mStartStopLock) {
             if (active) {
-                // Abort all active models. This must be done as one transaction to the event
-                // thread, in order to be able to dedupe events before they are delivered.
-                try (SafeCloseable ignored = mCallbackThread.stallReader()) {
-                    for (int modelHandle : mActiveModels) {
-                        mDelegate.stopRecognition(modelHandle);
-                        LoadedModel model = mLoadedModels.get(modelHandle);
-                        // An abort event must be the last one for its model.
-                        mCallbackThread.pushWithDedupe(modelHandle, true,
-                                () -> notifyAbort(modelHandle, model));
-                    }
-                }
+                abortAllActiveModels();
             } else {
-                mGlobalCallback.onResourcesAvailable();
+                if (mGlobalCallback != null) {
+                    mGlobalCallback.onResourcesAvailable();
+                }
             }
-
             mCaptureState = active;
         }
     }
 
+    private void abortAllActiveModels() {
+        while (true) {
+            int toStop;
+            synchronized (mActiveModels) {
+                Iterator<Integer> iterator = mActiveModels.iterator();
+                if (!iterator.hasNext()) {
+                    return;
+                }
+                toStop = iterator.next();
+                mActiveModels.remove(toStop);
+            }
+            // Invoke stop outside of the lock.
+            mDelegate.stopRecognition(toStop);
+
+            LoadedModel model = mLoadedModels.get(toStop);
+            // Queue an abort event (no need to flush).
+            mCallbackThread.push(() -> notifyAbort(toStop, model));
+        }
+    }
+
     @Override
     public int loadSoundModel(SoundModel soundModel, ModelCallback callback) {
         int handle = mDelegate.loadSoundModel(soundModel, new CallbackWrapper(callback));
@@ -188,23 +213,13 @@
 
     @Override
     public void registerCallback(GlobalCallback callback) {
-        mGlobalCallback = new GlobalCallback() {
-            @Override
-            public void onResourcesAvailable() {
-                mCallbackThread.push(callback::onResourcesAvailable);
-            }
-        };
+        mGlobalCallback = () -> mCallbackThread.push(callback::onResourcesAvailable);
         mDelegate.registerCallback(mGlobalCallback);
     }
 
     @Override
     public void linkToDeath(IBinder.DeathRecipient recipient) {
-        IBinder.DeathRecipient wrapper = new IBinder.DeathRecipient() {
-            @Override
-            public void binderDied() {
-                mCallbackThread.push(() -> recipient.binderDied());
-            }
-        };
+        IBinder.DeathRecipient wrapper = () -> mCallbackThread.push(recipient::binderDied);
         mDelegate.linkToDeath(wrapper);
         mDeathRecipientMap.put(recipient, wrapper);
     }
@@ -215,7 +230,7 @@
     }
 
     private class CallbackWrapper implements ISoundTriggerHal.ModelCallback {
-        private final @NonNull ISoundTriggerHal.ModelCallback mDelegateCallback;
+        @NonNull private final ISoundTriggerHal.ModelCallback mDelegateCallback;
 
         private CallbackWrapper(@NonNull ModelCallback delegateCallback) {
             mDelegateCallback = delegateCallback;
@@ -223,18 +238,36 @@
 
         @Override
         public void recognitionCallback(int modelHandle, RecognitionEvent event) {
-            // A recognition event must be the last one for its model, unless it is a forced one
-            // (those leave the model active).
-            mCallbackThread.pushWithDedupe(modelHandle, !event.recognitionStillActive,
-                    () -> mDelegateCallback.recognitionCallback(modelHandle, event));
+            synchronized (mActiveModels) {
+                if (!mActiveModels.contains(modelHandle)) {
+                    // Discard the event.
+                    return;
+                }
+                if (!event.recognitionStillActive) {
+                    mActiveModels.remove(modelHandle);
+                }
+                // A recognition event must be the last one for its model, unless it indicates that
+                // recognition is still active.
+                mCallbackThread.push(
+                        () -> mDelegateCallback.recognitionCallback(modelHandle, event));
+            }
         }
 
         @Override
         public void phraseRecognitionCallback(int modelHandle, PhraseRecognitionEvent event) {
-            // A recognition event must be the last one for its model, unless it is a forced one
-            // (those leave the model active).
-            mCallbackThread.pushWithDedupe(modelHandle, !event.common.recognitionStillActive,
-                    () -> mDelegateCallback.phraseRecognitionCallback(modelHandle, event));
+            synchronized (mActiveModels) {
+                if (!mActiveModels.contains(modelHandle)) {
+                    // Discard the event.
+                    return;
+                }
+                if (!event.common.recognitionStillActive) {
+                    mActiveModels.remove(modelHandle);
+                }
+                // A recognition event must be the last one for its model, unless it indicates that
+                // recognition is still active.
+                mCallbackThread.push(
+                        () -> mDelegateCallback.phraseRecognitionCallback(modelHandle, event));
+            }
         }
 
         @Override
@@ -254,36 +287,12 @@
      * <ul>
      * <li>Events are processed on a separate thread than the thread that pushed them, in the order
      * they were pushed.
-     * <li>Events can be deduped upon entry to the queue. This is achieved as follows:
-     * <ul>
-     *     <li>Temporarily stall the reader via {@link #stallReader()}.
-     *     <li>Within this scope, push as many events as needed via
-     *     {@link #pushWithDedupe(int, boolean, Runnable)}.
-     *     If an event with the same model handle as the one being pushed is already in the queue
-     *     and has been marked as "lastForModel", the new event will be discarded before entering
-     *     the queue.
-     *     <li>Finally, un-stall the reader by existing the scope.
-     *     <li>Events that do not require deduping can be pushed via {@link #push(Runnable)}.
-     * </ul>
      * <li>Events can be flushed via {@link #flush()}. This will block until all events pushed prior
      * to this call have been fully processed.
      * </ul>
      */
     private static class CallbackThread {
-        private static class Entry {
-            final boolean lastForModel;
-            final int modelHandle;
-            final Runnable runnable;
-
-            private Entry(boolean lastForModel, int modelHandle, Runnable runnable) {
-                this.lastForModel = lastForModel;
-                this.modelHandle = modelHandle;
-                this.runnable = runnable;
-            }
-        }
-
-        private boolean mStallReader = false;
-        private final Queue<Entry> mList = new LinkedList<>();
+        private final Queue<Runnable> mList = new LinkedList<>();
         private int mPushCount = 0;
         private int mProcessedCount = 0;
 
@@ -312,23 +321,11 @@
          * @param runnable The runnable to push.
          */
         void push(Runnable runnable) {
-            pushEntry(new Entry(false, 0, runnable), false);
-        }
-
-
-        /**
-         * Push a new runnable to the queue, with deduping.
-         * If an entry with the same model handle is already in the queue and was designated as
-         * last for model, this one will be discarded.
-         *
-         * @param modelHandle The model handle, used for deduping purposes.
-         * @param lastForModel If true, this entry will be considered the last one for this model
-         *                     and any subsequence calls for this handle (whether lastForModel or
-         *                     not) will be discarded while this entry is in the queue.
-         * @param runnable    The runnable to push.
-         */
-        void pushWithDedupe(int modelHandle, boolean lastForModel, Runnable runnable) {
-            pushEntry(new Entry(lastForModel, modelHandle, runnable), true);
+            synchronized (mList) {
+                mList.add(runnable);
+                mPushCount++;
+                mList.notifyAll();
+            }
         }
 
         /**
@@ -346,45 +343,15 @@
             }
         }
 
-        /**
-         * Creates a scope (using a try-with-resources block), within which events that are pushed
-         * remain queued and processed. This is useful in order to utilize deduping.
-         */
-        SafeCloseable stallReader() {
-            synchronized (mList) {
-                mStallReader = true;
-                return () -> {
-                    synchronized (mList) {
-                        mStallReader = false;
-                        mList.notifyAll();
-                    }
-                };
-            }
-        }
-
-        private void pushEntry(Entry entry, boolean dedupe) {
-            synchronized (mList) {
-                if (dedupe) {
-                    for (Entry existing : mList) {
-                        if (existing.lastForModel && existing.modelHandle == entry.modelHandle) {
-                            return;
-                        }
-                    }
-                }
-                mList.add(entry);
-                mPushCount++;
-                mList.notifyAll();
-            }
-        }
-
         private Runnable pop() throws InterruptedException {
             synchronized (mList) {
-                while (mStallReader || mList.isEmpty()) {
+                while (mList.isEmpty()) {
                     mList.wait();
                 }
-                return mList.remove().runnable;
+                return mList.remove();
             }
         }
+
     }
 
     /** Notify the client that recognition has been aborted. */
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index eb5ca9c..95de040 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -897,6 +897,7 @@
                         proc.getThread(), r.token);
 
                 final boolean isTransitionForward = r.isTransitionForward();
+                final IBinder fragmentToken = r.getTaskFragment().getFragmentToken();
                 clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
                         System.identityHashCode(r), r.info,
                         // TODO: Have this take the merged configuration instead of separate global
@@ -907,7 +908,7 @@
                         proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),
                         results, newIntents, r.takeOptions(), isTransitionForward,
                         proc.createProfilerInfoIfNeeded(), r.assistToken, activityClientController,
-                        r.shareableActivityToken, r.getLaunchedFromBubble()));
+                        r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken));
 
                 // Set desired final state.
                 final ActivityLifecycleItem lifecycleItem;
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 8d1425d..67dd89e 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -29,6 +29,7 @@
 import android.os.Debug;
 import android.os.IBinder;
 import android.util.Slog;
+import android.view.Display;
 import android.view.InputApplicationHandle;
 import android.view.KeyEvent;
 import android.view.SurfaceControl;
@@ -194,6 +195,9 @@
             int firstExternalDisplayId = DEFAULT_DISPLAY;
             for (int i = mService.mRoot.mChildren.size() - 1; i >= 0; --i) {
                 final DisplayContent displayContent = mService.mRoot.mChildren.get(i);
+                if (displayContent.getDisplayInfo().state == Display.STATE_OFF) {
+                    continue;
+                }
                 // Heuristic solution here. Currently when "Freeform windows" developer option is
                 // enabled we automatically put secondary displays in freeform mode and emulating
                 // "desktop mode". It also makes sense to show the pointer on the same display.
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index 00f7e636..65062e5 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -249,6 +249,8 @@
         inputWindowHandle.setPaused(w.mActivityRecord != null && w.mActivityRecord.paused);
         inputWindowHandle.setWindowToken(w.mClient);
 
+        inputWindowHandle.setName(w.getName());
+
         // Update layout params flags to force the window to be not touch modal. We do this to
         // restrict the window's touchable region to the task even if it requests touches outside
         // its window bounds. An example is a dialog in primary split should get touches outside its
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 3a458fd..900963e 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2262,7 +2262,7 @@
                 mFragmentToken,
                 mRemoteToken.toWindowContainerToken(),
                 getConfiguration(),
-                getChildCount() == 0,
+                runningActivityCount[0] == 0,
                 runningActivityCount[0],
                 isVisible(),
                 childActivities,
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 31b5579..fa5e450 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -106,7 +106,6 @@
     jmethodID interceptMotionBeforeQueueingNonInteractive;
     jmethodID interceptKeyBeforeDispatching;
     jmethodID dispatchUnhandledKey;
-    jmethodID checkInjectEventsPermission;
     jmethodID onPointerDownOutsideFocus;
     jmethodID getVirtualKeyQuietTimeMillis;
     jmethodID getExcludedDeviceNames;
@@ -131,6 +130,11 @@
 
 static struct {
     jclass clazz;
+    jfieldID mPtr;
+} gNativeInputManagerServiceImpl;
+
+static struct {
+    jclass clazz;
 } gInputDeviceClassInfo;
 
 static struct {
@@ -260,17 +264,16 @@
 
     void setDisplayViewports(JNIEnv* env, jobjectArray viewportObjArray);
 
-    base::Result<std::unique_ptr<InputChannel>> createInputChannel(JNIEnv* env,
-                                                                   const std::string& name);
-    base::Result<std::unique_ptr<InputChannel>> createInputMonitor(JNIEnv* env, int32_t displayId,
+    base::Result<std::unique_ptr<InputChannel>> createInputChannel(const std::string& name);
+    base::Result<std::unique_ptr<InputChannel>> createInputMonitor(int32_t displayId,
                                                                    const std::string& name,
                                                                    int32_t pid);
-    status_t removeInputChannel(JNIEnv* env, const sp<IBinder>& connectionToken);
+    status_t removeInputChannel(const sp<IBinder>& connectionToken);
     status_t pilferPointers(const sp<IBinder>& token);
 
     void displayRemoved(JNIEnv* env, int32_t displayId);
     void setFocusedApplication(JNIEnv* env, int32_t displayId, jobject applicationHandleObj);
-    void setFocusedDisplay(JNIEnv* env, int32_t displayId);
+    void setFocusedDisplay(int32_t displayId);
     void setInputDispatchMode(bool enabled, bool frozen);
     void setSystemUiLightsOut(bool lightsOut);
     void setPointerSpeed(int32_t speed);
@@ -329,7 +332,6 @@
     bool dispatchUnhandledKey(const sp<IBinder>& token, const KeyEvent* keyEvent,
                               uint32_t policyFlags, KeyEvent* outFallbackKeyEvent) override;
     void pokeUserActivity(nsecs_t eventTime, int32_t eventType, int32_t displayId) override;
-    bool checkInjectEventsPermissionNonReentrant(int32_t injectorPid, int32_t injectorUid) override;
     void onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) override;
     void setPointerCapture(const PointerCaptureRequest& request) override;
     void notifyDropWindow(const sp<IBinder>& token, float x, float y) override;
@@ -512,19 +514,18 @@
 }
 
 base::Result<std::unique_ptr<InputChannel>> NativeInputManager::createInputChannel(
-        JNIEnv* /* env */, const std::string& name) {
+        const std::string& name) {
     ATRACE_CALL();
     return mInputManager->getDispatcher().createInputChannel(name);
 }
 
 base::Result<std::unique_ptr<InputChannel>> NativeInputManager::createInputMonitor(
-        JNIEnv* /* env */, int32_t displayId, const std::string& name, int32_t pid) {
+        int32_t displayId, const std::string& name, int32_t pid) {
     ATRACE_CALL();
     return mInputManager->getDispatcher().createInputMonitor(displayId, name, pid);
 }
 
-status_t NativeInputManager::removeInputChannel(JNIEnv* /* env */,
-                                                const sp<IBinder>& connectionToken) {
+status_t NativeInputManager::removeInputChannel(const sp<IBinder>& connectionToken) {
     ATRACE_CALL();
     return mInputManager->getDispatcher().removeInputChannel(connectionToken);
 }
@@ -1002,7 +1003,7 @@
     mInputManager->getDispatcher().setFocusedApplication(displayId, applicationHandle);
 }
 
-void NativeInputManager::setFocusedDisplay(JNIEnv* env, int32_t displayId) {
+void NativeInputManager::setFocusedDisplay(int32_t displayId) {
     mInputManager->getDispatcher().setFocusedDisplay(displayId);
 }
 
@@ -1368,18 +1369,6 @@
     android_server_PowerManagerService_userActivity(eventTime, eventType, displayId);
 }
 
-bool NativeInputManager::checkInjectEventsPermissionNonReentrant(
-        int32_t injectorPid, int32_t injectorUid) {
-    ATRACE_CALL();
-    JNIEnv* env = jniEnv();
-    jboolean result = env->CallBooleanMethod(mServiceObj,
-            gServiceClassInfo.checkInjectEventsPermission, injectorPid, injectorUid);
-    if (checkAndClearExceptionFromCallback(env, "checkInjectEventsPermission")) {
-        result = false;
-    }
-    return result;
-}
-
 void NativeInputManager::onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) {
     ATRACE_CALL();
     JNIEnv* env = jniEnv();
@@ -1504,6 +1493,11 @@
 
 // ----------------------------------------------------------------------------
 
+static NativeInputManager* getNativeInputManager(JNIEnv* env, jobject clazz) {
+    return reinterpret_cast<NativeInputManager*>(
+            env->GetLongField(clazz, gNativeInputManagerServiceImpl.mPtr));
+}
+
 static jlong nativeInit(JNIEnv* env, jclass /* clazz */,
         jobject serviceObj, jobject contextObj, jobject messageQueueObj) {
     sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
@@ -1518,8 +1512,8 @@
     return reinterpret_cast<jlong>(im);
 }
 
-static void nativeStart(JNIEnv* env, jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeStart(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     status_t result = im->getInputManager()->start();
     if (result) {
@@ -1527,39 +1521,39 @@
     }
 }
 
-static void nativeSetDisplayViewports(JNIEnv* env, jclass /* clazz */, jlong ptr,
-        jobjectArray viewportObjArray) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetDisplayViewports(JNIEnv* env, jobject nativeImplObj,
+                                      jobjectArray viewportObjArray) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->setDisplayViewports(env, viewportObjArray);
 }
 
-static jint nativeGetScanCodeState(JNIEnv* /* env */, jclass /* clazz */,
-        jlong ptr, jint deviceId, jint sourceMask, jint scanCode) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jint nativeGetScanCodeState(JNIEnv* env, jobject nativeImplObj, jint deviceId,
+                                   jint sourceMask, jint scanCode) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     return (jint)im->getInputManager()->getReader().getScanCodeState(deviceId, uint32_t(sourceMask),
                                                                      scanCode);
 }
 
-static jint nativeGetKeyCodeState(JNIEnv* /* env */, jclass /* clazz */,
-        jlong ptr, jint deviceId, jint sourceMask, jint keyCode) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jint nativeGetKeyCodeState(JNIEnv* env, jobject nativeImplObj, jint deviceId,
+                                  jint sourceMask, jint keyCode) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     return (jint)im->getInputManager()->getReader().getKeyCodeState(deviceId, uint32_t(sourceMask),
                                                                     keyCode);
 }
 
-static jint nativeGetSwitchState(JNIEnv* /* env */, jclass /* clazz */,
-        jlong ptr, jint deviceId, jint sourceMask, jint sw) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jint nativeGetSwitchState(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint sourceMask,
+                                 jint sw) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     return (jint)im->getInputManager()->getReader().getSwitchState(deviceId, uint32_t(sourceMask),
                                                                    sw);
 }
 
-static jboolean nativeHasKeys(JNIEnv* env, jclass /* clazz */,
-        jlong ptr, jint deviceId, jint sourceMask, jintArray keyCodes, jbooleanArray outFlags) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jboolean nativeHasKeys(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint sourceMask,
+                              jintArray keyCodes, jbooleanArray outFlags) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     int32_t* codes = env->GetIntArrayElements(keyCodes, nullptr);
     uint8_t* flags = env->GetBooleanArrayElements(outFlags, nullptr);
@@ -1581,9 +1575,9 @@
     return result;
 }
 
-static jint nativeGetKeyCodeForKeyLocation(JNIEnv* env, jclass /* clazz */, jlong ptr,
-                                           jint deviceId, jint locationKeyCode) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jint nativeGetKeyCodeForKeyLocation(JNIEnv* env, jobject nativeImplObj, jint deviceId,
+                                           jint locationKeyCode) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     return (jint)im->getInputManager()->getReader().getKeyCodeForKeyLocation(deviceId,
                                                                              locationKeyCode);
 }
@@ -1596,17 +1590,16 @@
     ALOGW("Input channel object '%s' was disposed without first being removed with "
           "the input manager!",
           inputChannel->getName().c_str());
-    im->removeInputChannel(env, inputChannel->getConnectionToken());
+    im->removeInputChannel(inputChannel->getConnectionToken());
 }
 
-static jobject nativeCreateInputChannel(JNIEnv* env, jclass /* clazz */, jlong ptr,
-                                        jstring nameObj) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jobject nativeCreateInputChannel(JNIEnv* env, jobject nativeImplObj, jstring nameObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     ScopedUtfChars nameChars(env, nameObj);
     std::string name = nameChars.c_str();
 
-    base::Result<std::unique_ptr<InputChannel>> inputChannel = im->createInputChannel(env, name);
+    base::Result<std::unique_ptr<InputChannel>> inputChannel = im->createInputChannel(name);
 
     if (!inputChannel.ok()) {
         std::string message = inputChannel.error().message();
@@ -1626,9 +1619,9 @@
     return inputChannelObj;
 }
 
-static jobject nativeCreateInputMonitor(JNIEnv* env, jclass /* clazz */, jlong ptr, jint displayId,
+static jobject nativeCreateInputMonitor(JNIEnv* env, jobject nativeImplObj, jint displayId,
                                         jstring nameObj, jint pid) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     if (displayId == ADISPLAY_ID_NONE) {
         std::string message = "InputChannel used as a monitor must be associated with a display";
@@ -1640,7 +1633,7 @@
     std::string name = nameChars.c_str();
 
     base::Result<std::unique_ptr<InputChannel>> inputChannel =
-            im->createInputMonitor(env, displayId, name, pid);
+            im->createInputMonitor(displayId, name, pid);
 
     if (!inputChannel.ok()) {
         std::string message = inputChannel.error().message();
@@ -1657,11 +1650,11 @@
     return inputChannelObj;
 }
 
-static void nativeRemoveInputChannel(JNIEnv* env, jclass /* clazz */, jlong ptr, jobject tokenObj) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeRemoveInputChannel(JNIEnv* env, jobject nativeImplObj, jobject tokenObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     sp<IBinder> token = ibinderForJavaObject(env, tokenObj);
 
-    status_t status = im->removeInputChannel(env, token);
+    status_t status = im->removeInputChannel(token);
     if (status && status != BAD_VALUE) { // ignore already removed channel
         std::string message;
         message += StringPrintf("Failed to remove input channel.  status=%d", status);
@@ -1669,48 +1662,46 @@
     }
 }
 
-static void nativePilferPointers(JNIEnv* env, jclass /* clazz */, jlong ptr, jobject tokenObj) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativePilferPointers(JNIEnv* env, jobject nativeImplObj, jobject tokenObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     sp<IBinder> token = ibinderForJavaObject(env, tokenObj);
     im->pilferPointers(token);
 }
 
-static void nativeSetInputFilterEnabled(JNIEnv* /* env */, jclass /* clazz */,
-        jlong ptr, jboolean enabled) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetInputFilterEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getDispatcher().setInputFilterEnabled(enabled);
 }
 
-static jboolean nativeSetInTouchMode(JNIEnv* /* env */, jclass /* clazz */, jlong ptr,
-                                     jboolean inTouchMode, jint pid, jint uid,
-                                     jboolean hasPermission) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jboolean nativeSetInTouchMode(JNIEnv* env, jobject nativeImplObj, jboolean inTouchMode,
+                                     jint pid, jint uid, jboolean hasPermission) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     return im->getInputManager()->getDispatcher().setInTouchMode(inTouchMode, pid, uid,
                                                                  hasPermission);
 }
 
-static void nativeSetMaximumObscuringOpacityForTouch(JNIEnv* /* env */, jclass /* clazz */,
-                                                     jlong ptr, jfloat opacity) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetMaximumObscuringOpacityForTouch(JNIEnv* env, jobject nativeImplObj,
+                                                     jfloat opacity) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getDispatcher().setMaximumObscuringOpacityForTouch(opacity);
 }
 
-static void nativeSetBlockUntrustedTouchesMode(JNIEnv* env, jclass /* clazz */, jlong ptr,
-                                               jint mode) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetBlockUntrustedTouchesMode(JNIEnv* env, jobject nativeImplObj, jint mode) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getDispatcher().setBlockUntrustedTouchesMode(
             static_cast<BlockUntrustedTouchesMode>(mode));
 }
 
-static jint nativeInjectInputEvent(JNIEnv* env, jclass /* clazz */,
-        jlong ptr, jobject inputEventObj, jint injectorPid, jint injectorUid,
-        jint syncMode, jint timeoutMillis, jint policyFlags) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jint nativeInjectInputEvent(JNIEnv* env, jobject nativeImplObj, jobject inputEventObj,
+                                   jboolean injectIntoUid, jint uid, jint syncMode,
+                                   jint timeoutMillis, jint policyFlags) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
+    const std::optional<int32_t> targetUid = injectIntoUid ? std::make_optional(uid) : std::nullopt;
     // static_cast is safe because the value was already checked at the Java layer
     InputEventInjectionSync mode = static_cast<InputEventInjectionSync>(syncMode);
 
@@ -1723,8 +1714,7 @@
         }
 
         const InputEventInjectionResult result =
-                im->getInputManager()->getDispatcher().injectInputEvent(&keyEvent, injectorPid,
-                                                                        injectorUid, mode,
+                im->getInputManager()->getDispatcher().injectInputEvent(&keyEvent, targetUid, mode,
                                                                         std::chrono::milliseconds(
                                                                                 timeoutMillis),
                                                                         uint32_t(policyFlags));
@@ -1737,8 +1727,8 @@
         }
 
         const InputEventInjectionResult result =
-                im->getInputManager()->getDispatcher().injectInputEvent(motionEvent, injectorPid,
-                                                                        injectorUid, mode,
+                im->getInputManager()->getDispatcher().injectInputEvent(motionEvent, targetUid,
+                                                                        mode,
                                                                         std::chrono::milliseconds(
                                                                                 timeoutMillis),
                                                                         uint32_t(policyFlags));
@@ -1749,9 +1739,8 @@
     }
 }
 
-static jobject nativeVerifyInputEvent(JNIEnv* env, jclass /* clazz */, jlong ptr,
-                                      jobject inputEventObj) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jobject nativeVerifyInputEvent(JNIEnv* env, jobject nativeImplObj, jobject inputEventObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     if (env->IsInstanceOf(inputEventObj, gKeyEventClassInfo.clazz)) {
         KeyEvent keyEvent;
@@ -1792,56 +1781,53 @@
     }
 }
 
-static void nativeToggleCapsLock(JNIEnv* env, jclass /* clazz */,
-         jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeToggleCapsLock(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().toggleCapsLockState(deviceId);
 }
 
-static void nativeDisplayRemoved(JNIEnv* env, jclass /* clazz */, jlong ptr, jint displayId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeDisplayRemoved(JNIEnv* env, jobject nativeImplObj, jint displayId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->displayRemoved(env, displayId);
 }
 
-static void nativeSetFocusedApplication(JNIEnv* env, jclass /* clazz */,
-        jlong ptr, jint displayId, jobject applicationHandleObj) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetFocusedApplication(JNIEnv* env, jobject nativeImplObj, jint displayId,
+                                        jobject applicationHandleObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setFocusedApplication(env, displayId, applicationHandleObj);
 }
 
-static void nativeSetFocusedDisplay(JNIEnv* env, jclass /* clazz */,
-        jlong ptr, jint displayId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetFocusedDisplay(JNIEnv* env, jobject nativeImplObj, jint displayId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
-    im->setFocusedDisplay(env, displayId);
+    im->setFocusedDisplay(displayId);
 }
 
-static void nativeRequestPointerCapture(JNIEnv* env, jclass /* clazz */, jlong ptr,
-                                        jobject tokenObj, jboolean enabled) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeRequestPointerCapture(JNIEnv* env, jobject nativeImplObj, jobject tokenObj,
+                                        jboolean enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     sp<IBinder> windowToken = ibinderForJavaObject(env, tokenObj);
 
     im->requestPointerCapture(windowToken, enabled);
 }
 
-static void nativeSetInputDispatchMode(JNIEnv* /* env */,
-        jclass /* clazz */, jlong ptr, jboolean enabled, jboolean frozen) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetInputDispatchMode(JNIEnv* env, jobject nativeImplObj, jboolean enabled,
+                                       jboolean frozen) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setInputDispatchMode(enabled, frozen);
 }
 
-static void nativeSetSystemUiLightsOut(JNIEnv* /* env */, jclass /* clazz */, jlong ptr,
-                                       jboolean lightsOut) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetSystemUiLightsOut(JNIEnv* env, jobject nativeImplObj, jboolean lightsOut) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setSystemUiLightsOut(lightsOut);
 }
 
-static jboolean nativeTransferTouchFocus(JNIEnv* env, jclass /* clazz */, jlong ptr,
+static jboolean nativeTransferTouchFocus(JNIEnv* env, jobject nativeImplObj,
                                          jobject fromChannelTokenObj, jobject toChannelTokenObj,
                                          jboolean isDragDrop) {
     if (fromChannelTokenObj == nullptr || toChannelTokenObj == nullptr) {
@@ -1851,7 +1837,7 @@
     sp<IBinder> fromChannelToken = ibinderForJavaObject(env, fromChannelTokenObj);
     sp<IBinder> toChannelToken = ibinderForJavaObject(env, toChannelTokenObj);
 
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     if (im->getInputManager()->getDispatcher().transferTouchFocus(fromChannelToken, toChannelToken,
                                                                   isDragDrop)) {
         return JNI_TRUE;
@@ -1860,11 +1846,11 @@
     }
 }
 
-static jboolean nativeTransferTouch(JNIEnv* env, jclass /* clazz */, jlong ptr,
+static jboolean nativeTransferTouch(JNIEnv* env, jobject nativeImplObj,
                                     jobject destChannelTokenObj) {
     sp<IBinder> destChannelToken = ibinderForJavaObject(env, destChannelTokenObj);
 
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     if (im->getInputManager()->getDispatcher().transferTouch(destChannelToken)) {
         return JNI_TRUE;
     } else {
@@ -1872,42 +1858,39 @@
     }
 }
 
-static void nativeSetPointerSpeed(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint speed) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setPointerSpeed(speed);
 }
 
-static void nativeSetPointerAcceleration(JNIEnv* /* env */, jclass /* clazz */, jlong ptr,
-                                         jfloat acceleration) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetPointerAcceleration(JNIEnv* env, jobject nativeImplObj, jfloat acceleration) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setPointerAcceleration(acceleration);
 }
 
-static void nativeSetShowTouches(JNIEnv* /* env */,
-        jclass /* clazz */, jlong ptr, jboolean enabled) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetShowTouches(JNIEnv* env, jobject nativeImplObj, jboolean enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setShowTouches(enabled);
 }
 
-static void nativeSetInteractive(JNIEnv* env,
-        jclass clazz, jlong ptr, jboolean interactive) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetInteractive(JNIEnv* env, jobject nativeImplObj, jboolean interactive) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setInteractive(interactive);
 }
 
-static void nativeReloadCalibration(JNIEnv* env, jclass clazz, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeReloadCalibration(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->reloadCalibration();
 }
 
-static void nativeVibrate(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
-                          jlongArray patternObj, jintArray amplitudesObj, jint repeat, jint token) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeVibrate(JNIEnv* env, jobject nativeImplObj, jint deviceId, jlongArray patternObj,
+                          jintArray amplitudesObj, jint repeat, jint token) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     size_t patternSize = env->GetArrayLength(patternObj);
     if (patternSize > MAX_VIBRATE_PATTERN_SIZE) {
@@ -1940,10 +1923,10 @@
     im->getInputManager()->getReader().vibrate(deviceId, sequence, repeat, token);
 }
 
-static void nativeVibrateCombined(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
+static void nativeVibrateCombined(JNIEnv* env, jobject nativeImplObj, jint deviceId,
                                   jlongArray patternObj, jobject amplitudesObj, jint repeat,
                                   jint token) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     size_t patternSize = env->GetArrayLength(patternObj);
 
@@ -1990,21 +1973,20 @@
     im->getInputManager()->getReader().vibrate(deviceId, sequence, repeat, token);
 }
 
-static void nativeCancelVibrate(JNIEnv* /* env */,
-        jclass /* clazz */, jlong ptr, jint deviceId, jint token) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeCancelVibrate(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint token) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().cancelVibrate(deviceId, token);
 }
 
-static bool nativeIsVibrating(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static bool nativeIsVibrating(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     return im->getInputManager()->getReader().isVibrating(deviceId);
 }
 
-static jintArray nativeGetVibratorIds(JNIEnv* env, jclass clazz, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jintArray nativeGetVibratorIds(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     std::vector<int32_t> vibrators = im->getInputManager()->getReader().getVibratorIds(deviceId);
 
     jintArray vibIdArray = env->NewIntArray(vibrators.size());
@@ -2014,8 +1996,8 @@
     return vibIdArray;
 }
 
-static jobject nativeGetLights(JNIEnv* env, jclass clazz, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jobject nativeGetLights(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     jobject jLights = env->NewObject(gArrayListClassInfo.clazz, gArrayListClassInfo.constructor);
 
     std::vector<InputDeviceLightInfo> lights =
@@ -2059,9 +2041,9 @@
     return jLights;
 }
 
-static jint nativeGetLightPlayerId(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
+static jint nativeGetLightPlayerId(JNIEnv* env, jobject nativeImplObj, jint deviceId,
                                    jint lightId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     std::optional<int32_t> ret =
             im->getInputManager()->getReader().getLightPlayerId(deviceId, lightId);
@@ -2069,54 +2051,51 @@
     return static_cast<jint>(ret.value_or(0));
 }
 
-static jint nativeGetLightColor(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
-                                jint lightId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jint nativeGetLightColor(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint lightId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     std::optional<int32_t> ret =
             im->getInputManager()->getReader().getLightColor(deviceId, lightId);
     return static_cast<jint>(ret.value_or(0));
 }
 
-static void nativeSetLightPlayerId(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
-                                   jint lightId, jint playerId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetLightPlayerId(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint lightId,
+                                   jint playerId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().setLightPlayerId(deviceId, lightId, playerId);
 }
 
-static void nativeSetLightColor(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
-                                jint lightId, jint color) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetLightColor(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint lightId,
+                                jint color) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().setLightColor(deviceId, lightId, color);
 }
 
-static jint nativeGetBatteryCapacity(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jint nativeGetBatteryCapacity(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     std::optional<int32_t> ret = im->getInputManager()->getReader().getBatteryCapacity(deviceId);
     return static_cast<jint>(ret.value_or(android::os::IInputConstants::INVALID_BATTERY_CAPACITY));
 }
 
-static jint nativeGetBatteryStatus(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jint nativeGetBatteryStatus(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     std::optional<int32_t> ret = im->getInputManager()->getReader().getBatteryStatus(deviceId);
     return static_cast<jint>(ret.value_or(BATTERY_STATUS_UNKNOWN));
 }
 
-static void nativeReloadKeyboardLayouts(JNIEnv* /* env */,
-        jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeReloadKeyboardLayouts(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().requestRefreshConfiguration(
             InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUTS);
 }
 
-static void nativeReloadDeviceAliases(JNIEnv* /* env */,
-        jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeReloadDeviceAliases(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().requestRefreshConfiguration(
             InputReaderConfiguration::CHANGE_DEVICE_ALIAS);
@@ -2131,58 +2110,54 @@
     return out;
 }
 
-static jstring nativeDump(JNIEnv* env, jclass /* clazz */, jlong ptr) {
+static jstring nativeDump(JNIEnv* env, jobject nativeImplObj) {
     std::string dump = dumpInputProperties();
 
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->dump(dump);
 
     return env->NewStringUTF(dump.c_str());
 }
 
-static void nativeMonitor(JNIEnv* /* env */, jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeMonitor(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().monitor();
     im->getInputManager()->getDispatcher().monitor();
 }
 
-static jboolean nativeIsInputDeviceEnabled(JNIEnv* env /* env */,
-        jclass /* clazz */, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jboolean nativeIsInputDeviceEnabled(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     return im->getInputManager()->getReader().isInputDeviceEnabled(deviceId);
 }
 
-static void nativeEnableInputDevice(JNIEnv* /* env */,
-        jclass /* clazz */, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeEnableInputDevice(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setInputDeviceEnabled(deviceId, true);
 }
 
-static void nativeDisableInputDevice(JNIEnv* /* env */,
-        jclass /* clazz */, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeDisableInputDevice(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setInputDeviceEnabled(deviceId, false);
 }
 
-static void nativeSetPointerIconType(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint iconId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetPointerIconType(JNIEnv* env, jobject nativeImplObj, jint iconId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setPointerIconType(iconId);
 }
 
-static void nativeReloadPointerIcons(JNIEnv* /* env */, jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeReloadPointerIcons(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->reloadPointerIcons();
 }
 
-static void nativeSetCustomPointerIcon(JNIEnv* env, jclass /* clazz */,
-                                       jlong ptr, jobject iconObj) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetCustomPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     PointerIcon pointerIcon;
     status_t result = android_view_PointerIcon_getLoadedIcon(env, iconObj, &pointerIcon);
@@ -2196,40 +2171,38 @@
     im->setCustomPointerIcon(spriteIcon);
 }
 
-static jboolean nativeCanDispatchToDisplay(JNIEnv* env, jclass /* clazz */, jlong ptr,
-        jint deviceId, jint displayId) {
-
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jboolean nativeCanDispatchToDisplay(JNIEnv* env, jobject nativeImplObj, jint deviceId,
+                                           jint displayId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     return im->getInputManager()->getReader().canDispatchToDisplay(deviceId, displayId);
 }
 
-static void nativeNotifyPortAssociationsChanged(JNIEnv* env, jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeNotifyPortAssociationsChanged(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getReader().requestRefreshConfiguration(
             InputReaderConfiguration::CHANGE_DISPLAY_INFO);
 }
 
-static void nativeNotifyPointerDisplayIdChanged(JNIEnv* env, jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeNotifyPointerDisplayIdChanged(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->notifyPointerDisplayIdChanged();
 }
 
-static void nativeSetDisplayEligibilityForPointerCapture(JNIEnv* env, jclass /* clazz */, jlong ptr,
+static void nativeSetDisplayEligibilityForPointerCapture(JNIEnv* env, jobject nativeImplObj,
                                                          jint displayId, jboolean isEligible) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getDispatcher().setDisplayEligibilityForPointerCapture(displayId,
                                                                                   isEligible);
 }
 
-static void nativeChangeUniqueIdAssociation(JNIEnv* env, jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeChangeUniqueIdAssociation(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getReader().requestRefreshConfiguration(
             InputReaderConfiguration::CHANGE_DISPLAY_INFO);
 }
 
-static void nativeSetMotionClassifierEnabled(JNIEnv* /* env */, jclass /* clazz */, jlong ptr,
-                                             jboolean enabled) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeSetMotionClassifierEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->setMotionClassifierEnabled(enabled);
 }
@@ -2265,8 +2238,8 @@
     return sensorInfo;
 }
 
-static jobjectArray nativeGetSensorList(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static jobjectArray nativeGetSensorList(JNIEnv* env, jobject nativeImplObj, jint deviceId) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     std::vector<InputDeviceSensorInfo> sensors =
             im->getInputManager()->getReader().getSensors(deviceId);
 
@@ -2295,10 +2268,10 @@
     return arr;
 }
 
-static jboolean nativeEnableSensor(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
+static jboolean nativeEnableSensor(JNIEnv* env, jobject nativeImplObj, jint deviceId,
                                    jint sensorType, jint samplingPeriodUs,
                                    jint maxBatchReportLatencyUs) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     return im->getInputManager()
             ->getReader()
@@ -2307,18 +2280,18 @@
                           std::chrono::microseconds(maxBatchReportLatencyUs));
 }
 
-static void nativeDisableSensor(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
+static void nativeDisableSensor(JNIEnv* env, jobject nativeImplObj, jint deviceId,
                                 jint sensorType) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().disableSensor(deviceId,
                                                      static_cast<InputDeviceSensorType>(
                                                              sensorType));
 }
 
-static jboolean nativeFlushSensor(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId,
+static jboolean nativeFlushSensor(JNIEnv* env, jobject nativeImplObj, jint deviceId,
                                   jint sensorType) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
 
     im->getInputManager()->getReader().flushSensor(deviceId,
                                                    static_cast<InputDeviceSensorType>(sensorType));
@@ -2327,8 +2300,8 @@
                                                                       sensorType));
 }
 
-static void nativeCancelCurrentTouch(JNIEnv* env, jclass /* clazz */, jlong ptr) {
-    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
+static void nativeCancelCurrentTouch(JNIEnv* env, jobject nativeImplObj) {
+    NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
     im->getInputManager()->getDispatcher().cancelCurrentTouch();
 }
 
@@ -2336,87 +2309,84 @@
 
 static const JNINativeMethod gInputManagerMethods[] = {
         /* name, signature, funcPtr */
-        {"nativeInit",
+        {"init",
          "(Lcom/android/server/input/InputManagerService;Landroid/content/Context;Landroid/os/"
          "MessageQueue;)J",
          (void*)nativeInit},
-        {"nativeStart", "(J)V", (void*)nativeStart},
-        {"nativeSetDisplayViewports", "(J[Landroid/hardware/display/DisplayViewport;)V",
+        {"start", "()V", (void*)nativeStart},
+        {"setDisplayViewports", "([Landroid/hardware/display/DisplayViewport;)V",
          (void*)nativeSetDisplayViewports},
-        {"nativeGetScanCodeState", "(JIII)I", (void*)nativeGetScanCodeState},
-        {"nativeGetKeyCodeState", "(JIII)I", (void*)nativeGetKeyCodeState},
-        {"nativeGetSwitchState", "(JIII)I", (void*)nativeGetSwitchState},
-        {"nativeHasKeys", "(JII[I[Z)Z", (void*)nativeHasKeys},
-        {"nativeGetKeyCodeForKeyLocation", "(JII)I", (void*)nativeGetKeyCodeForKeyLocation},
-        {"nativeCreateInputChannel", "(JLjava/lang/String;)Landroid/view/InputChannel;",
+        {"getScanCodeState", "(III)I", (void*)nativeGetScanCodeState},
+        {"getKeyCodeState", "(III)I", (void*)nativeGetKeyCodeState},
+        {"getSwitchState", "(III)I", (void*)nativeGetSwitchState},
+        {"hasKeys", "(II[I[Z)Z", (void*)nativeHasKeys},
+        {"getKeyCodeForKeyLocation", "(II)I", (void*)nativeGetKeyCodeForKeyLocation},
+        {"createInputChannel", "(Ljava/lang/String;)Landroid/view/InputChannel;",
          (void*)nativeCreateInputChannel},
-        {"nativeCreateInputMonitor", "(JILjava/lang/String;I)Landroid/view/InputChannel;",
+        {"createInputMonitor", "(ILjava/lang/String;I)Landroid/view/InputChannel;",
          (void*)nativeCreateInputMonitor},
-        {"nativeRemoveInputChannel", "(JLandroid/os/IBinder;)V", (void*)nativeRemoveInputChannel},
-        {"nativePilferPointers", "(JLandroid/os/IBinder;)V", (void*)nativePilferPointers},
-        {"nativeSetInputFilterEnabled", "(JZ)V", (void*)nativeSetInputFilterEnabled},
-        {"nativeSetInTouchMode", "(JZIIZ)Z", (void*)nativeSetInTouchMode},
-        {"nativeSetMaximumObscuringOpacityForTouch", "(JF)V",
+        {"removeInputChannel", "(Landroid/os/IBinder;)V", (void*)nativeRemoveInputChannel},
+        {"pilferPointers", "(Landroid/os/IBinder;)V", (void*)nativePilferPointers},
+        {"setInputFilterEnabled", "(Z)V", (void*)nativeSetInputFilterEnabled},
+        {"setInTouchMode", "(ZIIZ)Z", (void*)nativeSetInTouchMode},
+        {"setMaximumObscuringOpacityForTouch", "(F)V",
          (void*)nativeSetMaximumObscuringOpacityForTouch},
-        {"nativeSetBlockUntrustedTouchesMode", "(JI)V", (void*)nativeSetBlockUntrustedTouchesMode},
-        {"nativeInjectInputEvent", "(JLandroid/view/InputEvent;IIIII)I",
-         (void*)nativeInjectInputEvent},
-        {"nativeVerifyInputEvent", "(JLandroid/view/InputEvent;)Landroid/view/VerifiedInputEvent;",
+        {"setBlockUntrustedTouchesMode", "(I)V", (void*)nativeSetBlockUntrustedTouchesMode},
+        {"injectInputEvent", "(Landroid/view/InputEvent;ZIIII)I", (void*)nativeInjectInputEvent},
+        {"verifyInputEvent", "(Landroid/view/InputEvent;)Landroid/view/VerifiedInputEvent;",
          (void*)nativeVerifyInputEvent},
-        {"nativeToggleCapsLock", "(JI)V", (void*)nativeToggleCapsLock},
-        {"nativeDisplayRemoved", "(JI)V", (void*)nativeDisplayRemoved},
-        {"nativeSetFocusedApplication", "(JILandroid/view/InputApplicationHandle;)V",
+        {"toggleCapsLock", "(I)V", (void*)nativeToggleCapsLock},
+        {"displayRemoved", "(I)V", (void*)nativeDisplayRemoved},
+        {"setFocusedApplication", "(ILandroid/view/InputApplicationHandle;)V",
          (void*)nativeSetFocusedApplication},
-        {"nativeSetFocusedDisplay", "(JI)V", (void*)nativeSetFocusedDisplay},
-        {"nativeRequestPointerCapture", "(JLandroid/os/IBinder;Z)V",
-         (void*)nativeRequestPointerCapture},
-        {"nativeSetInputDispatchMode", "(JZZ)V", (void*)nativeSetInputDispatchMode},
-        {"nativeSetSystemUiLightsOut", "(JZ)V", (void*)nativeSetSystemUiLightsOut},
-        {"nativeTransferTouchFocus", "(JLandroid/os/IBinder;Landroid/os/IBinder;Z)Z",
+        {"setFocusedDisplay", "(I)V", (void*)nativeSetFocusedDisplay},
+        {"requestPointerCapture", "(Landroid/os/IBinder;Z)V", (void*)nativeRequestPointerCapture},
+        {"setInputDispatchMode", "(ZZ)V", (void*)nativeSetInputDispatchMode},
+        {"setSystemUiLightsOut", "(Z)V", (void*)nativeSetSystemUiLightsOut},
+        {"transferTouchFocus", "(Landroid/os/IBinder;Landroid/os/IBinder;Z)Z",
          (void*)nativeTransferTouchFocus},
-        {"nativeTransferTouch", "(JLandroid/os/IBinder;)Z", (void*)nativeTransferTouch},
-        {"nativeSetPointerSpeed", "(JI)V", (void*)nativeSetPointerSpeed},
-        {"nativeSetPointerAcceleration", "(JF)V", (void*)nativeSetPointerAcceleration},
-        {"nativeSetShowTouches", "(JZ)V", (void*)nativeSetShowTouches},
-        {"nativeSetInteractive", "(JZ)V", (void*)nativeSetInteractive},
-        {"nativeReloadCalibration", "(J)V", (void*)nativeReloadCalibration},
-        {"nativeVibrate", "(JI[J[III)V", (void*)nativeVibrate},
-        {"nativeVibrateCombined", "(JI[JLandroid/util/SparseArray;II)V",
-         (void*)nativeVibrateCombined},
-        {"nativeCancelVibrate", "(JII)V", (void*)nativeCancelVibrate},
-        {"nativeIsVibrating", "(JI)Z", (void*)nativeIsVibrating},
-        {"nativeGetVibratorIds", "(JI)[I", (void*)nativeGetVibratorIds},
-        {"nativeGetLights", "(JI)Ljava/util/List;", (void*)nativeGetLights},
-        {"nativeGetLightPlayerId", "(JII)I", (void*)nativeGetLightPlayerId},
-        {"nativeGetLightColor", "(JII)I", (void*)nativeGetLightColor},
-        {"nativeSetLightPlayerId", "(JIII)V", (void*)nativeSetLightPlayerId},
-        {"nativeSetLightColor", "(JIII)V", (void*)nativeSetLightColor},
-        {"nativeGetBatteryCapacity", "(JI)I", (void*)nativeGetBatteryCapacity},
-        {"nativeGetBatteryStatus", "(JI)I", (void*)nativeGetBatteryStatus},
-        {"nativeReloadKeyboardLayouts", "(J)V", (void*)nativeReloadKeyboardLayouts},
-        {"nativeReloadDeviceAliases", "(J)V", (void*)nativeReloadDeviceAliases},
-        {"nativeDump", "(J)Ljava/lang/String;", (void*)nativeDump},
-        {"nativeMonitor", "(J)V", (void*)nativeMonitor},
-        {"nativeIsInputDeviceEnabled", "(JI)Z", (void*)nativeIsInputDeviceEnabled},
-        {"nativeEnableInputDevice", "(JI)V", (void*)nativeEnableInputDevice},
-        {"nativeDisableInputDevice", "(JI)V", (void*)nativeDisableInputDevice},
-        {"nativeSetPointerIconType", "(JI)V", (void*)nativeSetPointerIconType},
-        {"nativeReloadPointerIcons", "(J)V", (void*)nativeReloadPointerIcons},
-        {"nativeSetCustomPointerIcon", "(JLandroid/view/PointerIcon;)V",
+        {"transferTouch", "(Landroid/os/IBinder;)Z", (void*)nativeTransferTouch},
+        {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
+        {"setPointerAcceleration", "(F)V", (void*)nativeSetPointerAcceleration},
+        {"setShowTouches", "(Z)V", (void*)nativeSetShowTouches},
+        {"setInteractive", "(Z)V", (void*)nativeSetInteractive},
+        {"reloadCalibration", "()V", (void*)nativeReloadCalibration},
+        {"vibrate", "(I[J[III)V", (void*)nativeVibrate},
+        {"vibrateCombined", "(I[JLandroid/util/SparseArray;II)V", (void*)nativeVibrateCombined},
+        {"cancelVibrate", "(II)V", (void*)nativeCancelVibrate},
+        {"isVibrating", "(I)Z", (void*)nativeIsVibrating},
+        {"getVibratorIds", "(I)[I", (void*)nativeGetVibratorIds},
+        {"getLights", "(I)Ljava/util/List;", (void*)nativeGetLights},
+        {"getLightPlayerId", "(II)I", (void*)nativeGetLightPlayerId},
+        {"getLightColor", "(II)I", (void*)nativeGetLightColor},
+        {"setLightPlayerId", "(III)V", (void*)nativeSetLightPlayerId},
+        {"setLightColor", "(III)V", (void*)nativeSetLightColor},
+        {"getBatteryCapacity", "(I)I", (void*)nativeGetBatteryCapacity},
+        {"getBatteryStatus", "(I)I", (void*)nativeGetBatteryStatus},
+        {"reloadKeyboardLayouts", "()V", (void*)nativeReloadKeyboardLayouts},
+        {"reloadDeviceAliases", "()V", (void*)nativeReloadDeviceAliases},
+        {"dump", "()Ljava/lang/String;", (void*)nativeDump},
+        {"monitor", "()V", (void*)nativeMonitor},
+        {"isInputDeviceEnabled", "(I)Z", (void*)nativeIsInputDeviceEnabled},
+        {"enableInputDevice", "(I)V", (void*)nativeEnableInputDevice},
+        {"disableInputDevice", "(I)V", (void*)nativeDisableInputDevice},
+        {"setPointerIconType", "(I)V", (void*)nativeSetPointerIconType},
+        {"reloadPointerIcons", "()V", (void*)nativeReloadPointerIcons},
+        {"setCustomPointerIcon", "(Landroid/view/PointerIcon;)V",
          (void*)nativeSetCustomPointerIcon},
-        {"nativeCanDispatchToDisplay", "(JII)Z", (void*)nativeCanDispatchToDisplay},
-        {"nativeNotifyPortAssociationsChanged", "(J)V", (void*)nativeNotifyPortAssociationsChanged},
-        {"nativeChangeUniqueIdAssociation", "(J)V", (void*)nativeChangeUniqueIdAssociation},
-        {"nativeNotifyPointerDisplayIdChanged", "(J)V", (void*)nativeNotifyPointerDisplayIdChanged},
-        {"nativeSetDisplayEligibilityForPointerCapture", "(JIZ)V",
+        {"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay},
+        {"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged},
+        {"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation},
+        {"notifyPointerDisplayIdChanged", "()V", (void*)nativeNotifyPointerDisplayIdChanged},
+        {"setDisplayEligibilityForPointerCapture", "(IZ)V",
          (void*)nativeSetDisplayEligibilityForPointerCapture},
-        {"nativeSetMotionClassifierEnabled", "(JZ)V", (void*)nativeSetMotionClassifierEnabled},
-        {"nativeGetSensorList", "(JI)[Landroid/hardware/input/InputSensorInfo;",
+        {"setMotionClassifierEnabled", "(Z)V", (void*)nativeSetMotionClassifierEnabled},
+        {"getSensorList", "(I)[Landroid/hardware/input/InputSensorInfo;",
          (void*)nativeGetSensorList},
-        {"nativeEnableSensor", "(JIIII)Z", (void*)nativeEnableSensor},
-        {"nativeDisableSensor", "(JII)V", (void*)nativeDisableSensor},
-        {"nativeFlushSensor", "(JII)Z", (void*)nativeFlushSensor},
-        {"nativeCancelCurrentTouch", "(J)V", (void*)nativeCancelCurrentTouch},
+        {"enableSensor", "(IIII)Z", (void*)nativeEnableSensor},
+        {"disableSensor", "(II)V", (void*)nativeDisableSensor},
+        {"flushSensor", "(II)Z", (void*)nativeFlushSensor},
+        {"cancelCurrentTouch", "()V", (void*)nativeCancelCurrentTouch},
 };
 
 #define FIND_CLASS(var, className) \
@@ -2436,11 +2406,21 @@
         LOG_FATAL_IF(! (var), "Unable to find field " fieldName);
 
 int register_android_server_InputManager(JNIEnv* env) {
-    int res = jniRegisterNativeMethods(env, "com/android/server/input/InputManagerService",
-            gInputManagerMethods, NELEM(gInputManagerMethods));
-    (void) res;  // Faked use when LOG_NDEBUG.
+    int res = jniRegisterNativeMethods(env,
+                                       "com/android/server/input/"
+                                       "NativeInputManagerService$NativeImpl",
+                                       gInputManagerMethods, NELEM(gInputManagerMethods));
+    (void)res; // Faked use when LOG_NDEBUG.
     LOG_FATAL_IF(res < 0, "Unable to register native methods.");
 
+    FIND_CLASS(gNativeInputManagerServiceImpl.clazz,
+               "com/android/server/input/"
+               "NativeInputManagerService$NativeImpl");
+    gNativeInputManagerServiceImpl.clazz =
+            jclass(env->NewGlobalRef(gNativeInputManagerServiceImpl.clazz));
+    gNativeInputManagerServiceImpl.mPtr =
+            env->GetFieldID(gNativeInputManagerServiceImpl.clazz, "mPtr", "J");
+
     // Callbacks
 
     jclass clazz;
@@ -2499,9 +2479,6 @@
             "dispatchUnhandledKey",
             "(Landroid/os/IBinder;Landroid/view/KeyEvent;I)Landroid/view/KeyEvent;");
 
-    GET_METHOD_ID(gServiceClassInfo.checkInjectEventsPermission, clazz,
-            "checkInjectEventsPermission", "(II)Z");
-
     GET_METHOD_ID(gServiceClassInfo.onPointerDownOutsideFocus, clazz,
             "onPointerDownOutsideFocus", "(Landroid/os/IBinder;)V");
 
diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java
index e1fe1d8..90fd8ed 100644
--- a/services/midi/java/com/android/server/midi/MidiService.java
+++ b/services/midi/java/com/android/server/midi/MidiService.java
@@ -685,11 +685,13 @@
 
     private boolean hasNonMidiUuids(BluetoothDevice btDevice) {
         ParcelUuid[] uuidParcels = btDevice.getUuids();
-        // The assumption is that these services are indicative of devices that
-        // ARE NOT MIDI devices.
-        for (ParcelUuid parcel : uuidParcels) {
-            if (mNonMidiUUIDs.contains(parcel)) {
-                return true;
+        if (uuidParcels != null) {
+            // The assumption is that these services are indicative of devices that
+            // ARE NOT MIDI devices.
+            for (ParcelUuid parcel : uuidParcels) {
+                if (mNonMidiUUIDs.contains(parcel)) {
+                    return true;
+                }
             }
         }
         return false;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
index 77cf543..c0b4f0f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BackgroundRestrictionTest.java
@@ -671,7 +671,7 @@
             mAppFGSTracker.onForegroundServiceStateChanged(testPkgName, testUid,
                     testPid, true);
             mAppFGSTracker.onForegroundServiceNotificationUpdated(
-                    testPkgName, testUid, notificationId);
+                    testPkgName, testUid, notificationId, false);
             mAppFGSTracker.mNotificationListener.onNotificationPosted(new StatusBarNotification(
                     testPkgName, null, notificationId, null, testUid, testPid,
                     new Notification(), UserHandle.of(testUser), null, mCurrentTimeMillis), null);
@@ -848,7 +848,7 @@
 
             // Pretend we have the notification dismissed.
             mAppFGSTracker.onForegroundServiceNotificationUpdated(
-                    testPkgName, testUid, -notificationId);
+                    testPkgName, testUid, notificationId, true);
             clearInvocations(mInjector.getAppStandbyInternal());
             clearInvocations(mInjector.getNotificationManager());
             clearInvocations(mBgRestrictionController);
@@ -885,7 +885,7 @@
 
             // Pretend notification is back on.
             mAppFGSTracker.onForegroundServiceNotificationUpdated(
-                    testPkgName, testUid, notificationId);
+                    testPkgName, testUid, notificationId, false);
             // Now we'll prompt the user even it has a FGS with notification.
             bgPromptFgsWithNotiToBgRestricted.set(true);
             clearInvocations(mInjector.getAppStandbyInternal());
@@ -1224,7 +1224,7 @@
             mAppFGSTracker.onForegroundServiceStateChanged(testPkgName1, testUid1,
                     testPid1, true);
             mAppFGSTracker.onForegroundServiceNotificationUpdated(
-                    testPkgName1, testUid1, fgsNotificationId);
+                    testPkgName1, testUid1, fgsNotificationId, false);
             mAppFGSTracker.mNotificationListener.onNotificationPosted(new StatusBarNotification(
                     testPkgName1, null, fgsNotificationId, null, testUid1, testPid1,
                     new Notification(), UserHandle.of(testUser1), null, mCurrentTimeMillis), null);
@@ -1235,7 +1235,7 @@
 
             // Pretend we have the notification dismissed.
             mAppFGSTracker.onForegroundServiceNotificationUpdated(
-                    testPkgName1, testUid1, -fgsNotificationId);
+                    testPkgName1, testUid1, fgsNotificationId, true);
 
             // Verify we have the notification.
             notificationId = checkNotificationShown(
@@ -1500,7 +1500,7 @@
         if (withNotification) {
             final int notificationId = 1000;
             mAppFGSTracker.onForegroundServiceNotificationUpdated(
-                    packageName, uid, notificationId);
+                    packageName, uid, notificationId, false);
             final StatusBarNotification noti = new StatusBarNotification(
                     packageName, null, notificationId, null, uid, pid,
                     new Notification(), UserHandle.of(UserHandle.getUserId(uid)),
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 9a4f8e2..43ba39a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -2164,6 +2164,49 @@
         }
     }
 
+    @Test
+    public void testIsWithinEJQuotaLocked_TempAllowlisting_Restricted() {
+        setDischarging();
+        JobStatus js = createExpeditedJobStatus("testIsWithinEJQuotaLocked_TempAllowlisting_Restricted", 1);
+        setStandbyBucket(RESTRICTED_INDEX, js);
+        setDeviceConfigLong(QcConstants.KEY_EJ_LIMIT_FREQUENT_MS, 10 * MINUTE_IN_MILLIS);
+        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), true);
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (30 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), true);
+        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
+                createTimingSession(now - (5 * MINUTE_IN_MILLIS), 4 * MINUTE_IN_MILLIS, 5), true);
+        synchronized (mQuotaController.mLock) {
+            assertFalse(mQuotaController.isWithinEJQuotaLocked(js));
+        }
+
+        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+        final long gracePeriodMs = 15 * SECOND_IN_MILLIS;
+        setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, gracePeriodMs);
+        Handler handler = mQuotaController.getHandler();
+        spyOn(handler);
+
+        // The temp allowlist should not enable RESTRICTED apps' to schedule & start EJs if they're
+        // out of quota.
+        mTempAllowlistListener.onAppAdded(mSourceUid);
+        synchronized (mQuotaController.mLock) {
+            assertFalse(mQuotaController.isWithinEJQuotaLocked(js));
+        }
+        advanceElapsedClock(10 * SECOND_IN_MILLIS);
+        mTempAllowlistListener.onAppRemoved(mSourceUid);
+        advanceElapsedClock(10 * SECOND_IN_MILLIS);
+        // Still in grace period
+        synchronized (mQuotaController.mLock) {
+            assertFalse(mQuotaController.isWithinEJQuotaLocked(js));
+        }
+        advanceElapsedClock(6 * SECOND_IN_MILLIS);
+        // Out of grace period.
+        synchronized (mQuotaController.mLock) {
+            assertFalse(mQuotaController.isWithinEJQuotaLocked(js));
+        }
+    }
+
     /**
      * Tests that Timers properly track sessions when an app becomes top and is closed.
      */
@@ -5559,6 +5602,111 @@
                 mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
     }
 
+    @Test
+    public void testEJTimerTracking_TempAllowlisting_Restricted() {
+        setDischarging();
+        setProcessState(ActivityManager.PROCESS_STATE_RECEIVER);
+        final long gracePeriodMs = 15 * SECOND_IN_MILLIS;
+        setDeviceConfigLong(QcConstants.KEY_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS, gracePeriodMs);
+        Handler handler = mQuotaController.getHandler();
+        spyOn(handler);
+
+        JobStatus job = createExpeditedJobStatus("testEJTimerTracking_TempAllowlisting_Restricted", 1);
+        setStandbyBucket(RESTRICTED_INDEX, job);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job, null);
+        }
+        assertNull(mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+        List<TimingSession> expected = new ArrayList<>();
+
+        long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.prepareForExecutionLocked(job);
+        }
+        advanceElapsedClock(10 * SECOND_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
+        }
+        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+        assertEquals(expected,
+                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+        advanceElapsedClock(SECOND_IN_MILLIS);
+
+        // Job starts after app is added to temp allowlist and stops before removal.
+        start = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mTempAllowlistListener.onAppAdded(mSourceUid);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job, null);
+            mQuotaController.prepareForExecutionLocked(job);
+        }
+        advanceElapsedClock(10 * SECOND_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+        }
+        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+        assertEquals(expected,
+                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+        // Job starts after app is added to temp allowlist and stops after removal,
+        // before grace period ends.
+        start = JobSchedulerService.sElapsedRealtimeClock.millis();
+        mTempAllowlistListener.onAppAdded(mSourceUid);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job, null);
+            mQuotaController.prepareForExecutionLocked(job);
+        }
+        advanceElapsedClock(10 * SECOND_IN_MILLIS);
+        mTempAllowlistListener.onAppRemoved(mSourceUid);
+        long elapsedGracePeriodMs = 2 * SECOND_IN_MILLIS;
+        advanceElapsedClock(elapsedGracePeriodMs);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job, null, false);
+        }
+        expected.add(createTimingSession(start, 12 * SECOND_IN_MILLIS, 1));
+        assertEquals(expected,
+                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+        advanceElapsedClock(SECOND_IN_MILLIS);
+        elapsedGracePeriodMs += SECOND_IN_MILLIS;
+
+        // Job starts during grace period and ends after grace period ends
+        start = JobSchedulerService.sElapsedRealtimeClock.millis();
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job, null);
+            mQuotaController.prepareForExecutionLocked(job);
+        }
+        final long remainingGracePeriod = gracePeriodMs - elapsedGracePeriodMs;
+        advanceElapsedClock(remainingGracePeriod);
+        // Wait for handler to update Timer
+        // Can't directly evaluate the message because for some reason, the captured message returns
+        // the wrong 'what' even though the correct message goes to the handler and the correct
+        // path executes.
+        verify(handler, timeout(gracePeriodMs + 5 * SECOND_IN_MILLIS)).handleMessage(any());
+        advanceElapsedClock(10 * SECOND_IN_MILLIS);
+        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS + remainingGracePeriod, 1));
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
+        }
+        assertEquals(expected,
+                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+        // Job starts and runs completely after temp allowlist grace period.
+        advanceElapsedClock(10 * SECOND_IN_MILLIS);
+        start = JobSchedulerService.sElapsedRealtimeClock.millis();
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStartTrackingJobLocked(job, null);
+            mQuotaController.prepareForExecutionLocked(job);
+        }
+        advanceElapsedClock(10 * SECOND_IN_MILLIS);
+        synchronized (mQuotaController.mLock) {
+            mQuotaController.maybeStopTrackingJobLocked(job, job, true);
+        }
+        expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
+        assertEquals(expected,
+                mQuotaController.getEJTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+    }
+
     /**
      * Tests that Timers properly track sessions when TOP state and temp allowlisting overlaps.
      */
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeSettingsHelper.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeSettingsHelper.java
index cd70020..b76abe6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeSettingsHelper.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/FakeSettingsHelper.java
@@ -84,6 +84,8 @@
     private final Setting mBackgroundThrottlePackageWhitelistSetting = new Setting(
             Collections.emptySet());
     private final Setting mGnssMeasurementsFullTrackingSetting = new Setting(Boolean.FALSE);
+    private final Setting mAdasPackageAllowlist = new Setting(
+            new PackageTagsList.Builder().build());
     private final Setting mIgnoreSettingsAllowlist = new Setting(
             new PackageTagsList.Builder().build());
     private final Setting mBackgroundThrottleProximityAlertIntervalSetting = new Setting(
@@ -194,10 +196,29 @@
     }
 
     @Override
+    public PackageTagsList getAdasAllowlist() {
+        return mAdasPackageAllowlist.getValue(PackageTagsList.class);
+    }
+
+    @Override
+    public void addAdasAllowlistChangedListener(GlobalSettingChangedListener listener) {
+        mAdasPackageAllowlist.addListener(listener);
+    }
+
+    @Override
+    public void removeAdasAllowlistChangedListener(GlobalSettingChangedListener listener) {
+        mAdasPackageAllowlist.removeListener(listener);
+    }
+
+    @Override
     public PackageTagsList getIgnoreSettingsAllowlist() {
         return mIgnoreSettingsAllowlist.getValue(PackageTagsList.class);
     }
 
+    public void setAdasSettingsAllowlist(PackageTagsList newValue) {
+        mAdasPackageAllowlist.setValue(newValue);
+    }
+
     public void setIgnoreSettingsAllowlist(PackageTagsList newValue) {
         mIgnoreSettingsAllowlist.setValue(newValue);
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
index d8f409d..71cc65b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java
@@ -1107,6 +1107,10 @@
         doReturn(true).when(mPackageManager).hasSystemFeature(FEATURE_AUTOMOTIVE);
         doReturn(true).when(mResources).getBoolean(R.bool.config_defaultAdasGnssLocationEnabled);
 
+        mInjector.getSettingsHelper().setAdasSettingsAllowlist(
+                new PackageTagsList.Builder().add(
+                        IDENTITY.getPackageName()).build());
+
         createManager(GPS_PROVIDER);
 
         ILocationListener listener1 = createMockLocationListener();
@@ -1136,6 +1140,10 @@
         doReturn(true).when(mPackageManager).hasSystemFeature(FEATURE_AUTOMOTIVE);
         doReturn(true).when(mResources).getBoolean(R.bool.config_defaultAdasGnssLocationEnabled);
 
+        mInjector.getSettingsHelper().setAdasSettingsAllowlist(
+                new PackageTagsList.Builder().add(
+                        IDENTITY.getPackageName()).build());
+
         createManager(GPS_PROVIDER);
 
         ILocationListener listener1 = createMockLocationListener();
@@ -1160,11 +1168,16 @@
 
     @Test
     public void testProviderRequest_AdasGnssBypass_ProviderDisabled_AdasDisabled() {
+        doReturn(true).when(mPackageManager).hasSystemFeature(FEATURE_AUTOMOTIVE);
+        doReturn(true).when(mResources).getBoolean(R.bool.config_defaultAdasGnssLocationEnabled);
+
         mInjector.getSettingsHelper().setIgnoreSettingsAllowlist(
                 new PackageTagsList.Builder().add(
                         IDENTITY.getPackageName()).build());
-        doReturn(true).when(mPackageManager).hasSystemFeature(FEATURE_AUTOMOTIVE);
-        doReturn(true).when(mResources).getBoolean(R.bool.config_defaultAdasGnssLocationEnabled);
+
+        mInjector.getSettingsHelper().setAdasSettingsAllowlist(
+                new PackageTagsList.Builder().add(
+                        IDENTITY.getPackageName()).build());
 
         createManager(GPS_PROVIDER);
 
diff --git a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
index b154d6f..1171518 100644
--- a/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/backup/transport/BackupTransportClientTest.java
@@ -110,10 +110,7 @@
 
         Thread thread = new Thread(() -> {
             try {
-                /*String name =*/ client.transportDirName();
-                fail("transportDirName should be cancelled");
-            } catch (CancellationException ex) {
-                // This is expected.
+                assertThat(client.transportDirName()).isNull();
             } catch (Exception ex) {
                 fail("unexpected Exception: " + ex.getClass().getCanonicalName());
             }
@@ -189,7 +186,7 @@
     }
 
     @Test
-    public void testFinishBackup_canceledBeforeCompletion_throwsException() throws Exception {
+    public void testFinishBackup_canceledBeforeCompletion_returnsError() throws Exception {
         TestCallbacksFakeTransportBinder binder = new TestCallbacksFakeTransportBinder();
         BackupTransportClient client = new BackupTransportClient(binder);
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
index 18f2642..112db76 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
@@ -61,7 +61,8 @@
     public void setUp() throws Exception {
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -93,6 +94,7 @@
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x2000;
@@ -153,7 +155,6 @@
                 mHdmiControlService);
         audioDevice.init();
         mLocalDevices.add(audioDevice);
-        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mTestLooper.dispatchAll();
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
index e4c5ad67..e4eecc6 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
@@ -68,7 +68,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         HdmiControlService hdmiControlService =
-                new HdmiControlService(mContextSpy, Collections.emptyList()) {
+                new HdmiControlService(mContextSpy, Collections.emptyList(),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isPowerStandby() {
                         return false;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index d73cdb5..5b11466 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -68,7 +68,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         HdmiControlService hdmiControlService =
-                new HdmiControlService(mContextSpy, Collections.emptyList()) {
+                new HdmiControlService(mContextSpy, Collections.emptyList(),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     AudioManager getAudioManager() {
                         return mAudioManager;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
new file mode 100644
index 0000000..e06877f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeControlTest.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2021 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.hdmi;
+
+import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
+
+import static com.android.server.hdmi.HdmiCecKeycode.CEC_KEYCODE_VOLUME_UP;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_BOOT_UP;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.hardware.hdmi.DeviceFeatures;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.tv.cec.V1_0.SendMessageResult;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.AudioManager;
+import android.media.VolumeInfo;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.server.SystemService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Tests that Absolute Volume Control (AVC) is enabled and disabled correctly, and that
+ * the device responds correctly to incoming <Report Audio Status> messages and API calls
+ * from AudioService when AVC is active.
+ *
+ * This is an abstract base class. Concrete subclasses specify the type of the local device, and the
+ * type of the System Audio device. This allows each test to be run for multiple setups.
+ *
+ * We test the following pairs of (local device, System Audio device):
+ * (Playback, TV): {@link PlaybackDeviceToTvAvcTest}
+ * (Playback, Audio System): {@link PlaybackDeviceToAudioSystemAvcTest}
+ * (TV, Audio System): {@link TvToAudioSystemAvcTest}
+ */
+public abstract class BaseAbsoluteVolumeControlTest {
+    private HdmiControlService mHdmiControlService;
+    private HdmiCecController mHdmiCecController;
+    private HdmiCecLocalDevice mHdmiCecLocalDevice;
+    private FakeHdmiCecConfig mHdmiCecConfig;
+    private FakePowerManagerWrapper mPowerManager;
+    private Looper mLooper;
+    private Context mContextSpy;
+    private ArrayList<HdmiCecLocalDevice> mLocalDevices = new ArrayList<>();
+
+    @Mock protected AudioManager mAudioManager;
+    protected FakeAudioDeviceVolumeManagerWrapper mAudioDeviceVolumeManager;
+
+    protected TestLooper mTestLooper = new TestLooper();
+    protected FakeNativeWrapper mNativeWrapper;
+
+    // Audio Status given by the System Audio device in its initial <Report Audio Status> that
+    // triggers AVC being enabled
+    private static final AudioStatus INITIAL_SYSTEM_AUDIO_DEVICE_STATUS =
+            new AudioStatus(50, false);
+
+    // VolumeInfo passed to AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior to enable AVC
+    private static final VolumeInfo ENABLE_AVC_VOLUME_INFO =
+            new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
+                    .setMuted(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute())
+                    .setVolumeIndex(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume())
+                    .setMaxVolumeIndex(AudioStatus.MAX_VOLUME)
+                    .setMinVolumeIndex(AudioStatus.MIN_VOLUME)
+                    .build();
+
+    protected abstract HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService);
+
+    protected abstract int getPhysicalAddress();
+    protected abstract int getDeviceType();
+    protected abstract AudioDeviceAttributes getAudioOutputDevice();
+
+    protected abstract int getSystemAudioDeviceLogicalAddress();
+    protected abstract int getSystemAudioDeviceType();
+
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+
+        mContextSpy = spy(new ContextWrapper(
+                InstrumentationRegistry.getInstrumentation().getTargetContext()));
+
+        mAudioDeviceVolumeManager = spy(new FakeAudioDeviceVolumeManagerWrapper());
+
+        mHdmiControlService =
+                new HdmiControlService(InstrumentationRegistry.getTargetContext(),
+                        Collections.singletonList(getDeviceType()),
+                        mAudioDeviceVolumeManager) {
+                    @Override
+                    AudioManager getAudioManager() {
+                        return mAudioManager;
+                    }
+
+                    @Override
+                    protected void writeStringSystemProperty(String key, String value) {
+                        // do nothing
+                    }
+                };
+
+        mLooper = mTestLooper.getLooper();
+        mHdmiControlService.setIoLooper(mLooper);
+
+        mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
+        mHdmiCecConfig.setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_HDMI_CEC_VERSION,
+                HdmiControlManager.HDMI_CEC_VERSION_2_0);
+        mHdmiCecConfig.setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE,
+                HdmiControlManager.VOLUME_CONTROL_DISABLED);
+        mHdmiControlService.setHdmiCecConfig(mHdmiCecConfig);
+
+        mNativeWrapper = new FakeNativeWrapper();
+        mNativeWrapper.setPhysicalAddress(getPhysicalAddress());
+        mNativeWrapper.setPollAddressResponse(Constants.ADDR_TV, SendMessageResult.SUCCESS);
+
+        mHdmiCecController = HdmiCecController.createWithNativeWrapper(
+                mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
+        mHdmiControlService.setCecController(mHdmiCecController);
+        mHdmiControlService.setHdmiMhlController(
+                HdmiMhlControllerStub.create(mHdmiControlService));
+        mHdmiControlService.initService();
+        mPowerManager = new FakePowerManagerWrapper(mContextSpy);
+        mHdmiControlService.setPowerManager(mPowerManager);
+
+        mHdmiCecLocalDevice = createLocalDevice(mHdmiControlService);
+        mHdmiCecLocalDevice.init();
+        mLocalDevices.add(mHdmiCecLocalDevice);
+
+        HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
+        hdmiPortInfos[0] =
+                new HdmiPortInfo(1, HdmiPortInfo.PORT_OUTPUT, 0x0000, true, false, false);
+        mNativeWrapper.setPortInfo(hdmiPortInfos);
+        mNativeWrapper.setPortConnectionStatus(1, true);
+
+        mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_BOOT_UP);
+        mTestLooper.dispatchAll();
+
+        // Simulate AudioManager's behavior and response when setDeviceVolumeBehavior is called
+        doAnswer(invocation -> {
+            setDeviceVolumeBehavior(invocation.getArgument(0), invocation.getArgument(1));
+            return null;
+        }).when(mAudioManager).setDeviceVolumeBehavior(any(), anyInt());
+
+        // Set starting volume behavior
+        doReturn(AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE)
+                .when(mAudioManager).getDeviceVolumeBehavior(eq(getAudioOutputDevice()));
+
+        // Audio service always plays STREAM_MUSIC on the device we need
+        doReturn(Collections.singletonList(getAudioOutputDevice())).when(mAudioManager)
+                .getDevicesForAttributes(HdmiControlService.STREAM_MUSIC_ATTRIBUTES);
+
+        // Max volume of STREAM_MUSIC
+        doReturn(25).when(mAudioManager).getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+
+        // Receive messages from devices to make sure they're registered in HdmiCecNetwork
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
+                Constants.ADDR_TV, getLogicalAddress()));
+        if (getSystemAudioDeviceType() == DEVICE_AUDIO_SYSTEM) {
+            mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
+                    Constants.ADDR_AUDIO_SYSTEM, getLogicalAddress()));
+        }
+
+        mHdmiControlService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
+        mHdmiControlService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED);
+        mTestLooper.dispatchAll();
+    }
+
+    protected int getLogicalAddress() {
+        synchronized (mHdmiCecLocalDevice.mLock) {
+            return mHdmiCecLocalDevice.getDeviceInfo().getLogicalAddress();
+        }
+    }
+
+    /**
+     * Simulates the volume behavior of {@code device} being set to {@code behavior}.
+     */
+    protected void setDeviceVolumeBehavior(AudioDeviceAttributes device,
+            @AudioManager.DeviceVolumeBehavior int behavior) {
+        doReturn(behavior).when(mAudioManager).getDeviceVolumeBehavior(eq(device));
+        mHdmiControlService.onDeviceVolumeBehaviorChanged(device, behavior);
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Changes the setting for CEC volume.
+     */
+    protected void setCecVolumeControlSetting(@HdmiControlManager.VolumeControl int setting) {
+        mHdmiControlService.getHdmiCecConfig().setIntValue(
+                HdmiControlManager.CEC_SETTING_NAME_VOLUME_CONTROL_MODE, setting);
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Has the device receive a <Report Features> message from the System Audio device specifying
+     * whether <Set Audio Volume Level> is supported or not.
+     */
+    protected void receiveSetAudioVolumeLevelSupport(
+            @DeviceFeatures.FeatureSupportStatus int featureSupportStatus) {
+        // <Report Features> can't specify an unknown feature support status
+        if (featureSupportStatus != DeviceFeatures.FEATURE_SUPPORT_UNKNOWN) {
+            mNativeWrapper.onCecMessage(ReportFeaturesMessage.build(
+                    getSystemAudioDeviceLogicalAddress(), HdmiControlManager.HDMI_CEC_VERSION_2_0,
+                    Arrays.asList(getSystemAudioDeviceType()), Constants.RC_PROFILE_SOURCE,
+                    Collections.emptyList(),
+                    DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
+                            .setSetAudioVolumeLevelSupport(featureSupportStatus)
+                            .build()));
+            mTestLooper.dispatchAll();
+        }
+    }
+
+    /**
+     * Enables System Audio mode if the System Audio device is an Audio System.
+     */
+    protected void enableSystemAudioModeIfNeeded() {
+        if (getSystemAudioDeviceType() == DEVICE_AUDIO_SYSTEM) {
+            receiveSetSystemAudioMode(true);
+        }
+    }
+
+    /**
+     * Sets System Audio Mode by having the device receive <Set System Audio Mode>
+     * from the Audio System.
+     */
+    protected void receiveSetSystemAudioMode(boolean status) {
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildSetSystemAudioMode(
+                Constants.ADDR_AUDIO_SYSTEM, Constants.ADDR_BROADCAST, status));
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Has the device receive a <Report Audio Status> reporting the status in
+     * {@link #INITIAL_SYSTEM_AUDIO_DEVICE_STATUS}
+     */
+    protected void receiveInitialReportAudioStatus() {
+        receiveReportAudioStatus(
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(),
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute());
+    }
+
+    /**
+     * Has the device receive a <Report Audio Status> message from the System Audio Device.
+     */
+    protected void receiveReportAudioStatus(int volume, boolean mute) {
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus(
+                getSystemAudioDeviceLogicalAddress(),
+                getLogicalAddress(),
+                volume,
+                mute));
+        mTestLooper.dispatchAll();
+    }
+
+    /**
+     * Triggers all the conditions required to enable Absolute Volume Control.
+     */
+    protected void enableAbsoluteVolumeControl() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        enableSystemAudioModeIfNeeded();
+        receiveInitialReportAudioStatus();
+
+        verifyAbsoluteVolumeEnabled();
+    }
+
+    /**
+     * Verifies that AVC was enabled - that is the audio output device's volume behavior was last
+     * set to absolute volume behavior.
+     */
+    protected void verifyAbsoluteVolumeEnabled() {
+        InOrder inOrder = inOrder(mAudioManager, mAudioDeviceVolumeManager);
+        inOrder.verify(mAudioDeviceVolumeManager, atLeastOnce()).setDeviceAbsoluteVolumeBehavior(
+                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
+        inOrder.verify(mAudioManager, never()).setDeviceVolumeBehavior(
+                eq(getAudioOutputDevice()), not(eq(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE)));
+    }
+
+    /**
+     * Verifies that AVC was disabled - that is, the audio output device's volume behavior was
+     * last set to something other than absolute volume behavior.
+     */
+    protected void verifyAbsoluteVolumeDisabled() {
+        InOrder inOrder = inOrder(mAudioManager, mAudioDeviceVolumeManager);
+        inOrder.verify(mAudioManager, atLeastOnce()).setDeviceVolumeBehavior(
+                eq(getAudioOutputDevice()), not(eq(AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE)));
+        inOrder.verify(mAudioDeviceVolumeManager, never()).setDeviceAbsoluteVolumeBehavior(
+                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
+    }
+
+    protected void verifyGiveAudioStatusNeverSent() {
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(
+                        getLogicalAddress(), getSystemAudioDeviceLogicalAddress()));
+    }
+
+    protected void verifyGiveAudioStatusSent() {
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(
+                        getLogicalAddress(), getSystemAudioDeviceLogicalAddress()));
+    }
+
+    @Test
+    public void allConditionsExceptSavlSupportMet_sendsSetAudioVolumeLevelAndGiveFeatures() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                SetAudioVolumeLevelMessage.build(
+                        getLogicalAddress(), getSystemAudioDeviceLogicalAddress(),
+                        Constants.AUDIO_VOLUME_STATUS_UNKNOWN));
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveFeatures(
+                        getLogicalAddress(), getSystemAudioDeviceLogicalAddress()));
+    }
+
+    @Test
+    public void allConditionsMet_savlSupportLast_reportFeatures_giveAudioStatusSent() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        verifyGiveAudioStatusNeverSent();
+
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void allConditionsMet_savlSupportLast_noFeatureAbort_giveAudioStatusSent() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        verifyGiveAudioStatusNeverSent();
+
+        mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+        mTestLooper.dispatchAll();
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void allConditionsMet_cecVolumeEnabledLast_giveAudioStatusSent() {
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        verifyGiveAudioStatusNeverSent();
+
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void allConditionsMet_fullVolumeBehaviorLast_giveAudioStatusSent() {
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        verifyGiveAudioStatusNeverSent();
+
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void allConditionsMet_systemAudioModeEnabledLast_giveAudioStatusSent() {
+        // Only run when the System Audio device is an Audio System.
+        assume().that(getSystemAudioDeviceType()).isEqualTo(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        verifyGiveAudioStatusNeverSent();
+
+        receiveSetSystemAudioMode(true);
+        verifyGiveAudioStatusSent();
+    }
+
+    @Test
+    public void giveAudioStatusSent_systemAudioDeviceSendsReportAudioStatus_avcEnabled() {
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_ENABLED);
+        enableSystemAudioModeIfNeeded();
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED);
+        setDeviceVolumeBehavior(getAudioOutputDevice(), AudioManager.DEVICE_VOLUME_BEHAVIOR_FULL);
+
+        // Verify that AVC was never enabled
+        verify(mAudioDeviceVolumeManager, never()).setDeviceAbsoluteVolumeBehavior(
+                eq(getAudioOutputDevice()), any(), any(), any(), anyBoolean());
+        receiveInitialReportAudioStatus();
+
+        verifyAbsoluteVolumeEnabled();
+    }
+
+    @Test
+    public void avcEnabled_cecVolumeDisabled_absoluteVolumeDisabled() {
+        enableAbsoluteVolumeControl();
+
+        setCecVolumeControlSetting(HdmiControlManager.VOLUME_CONTROL_DISABLED);
+        verifyAbsoluteVolumeDisabled();
+    }
+
+    @Test
+    public void avcEnabled_setAudioVolumeLevelNotSupported_absoluteVolumeDisabled() {
+        enableAbsoluteVolumeControl();
+
+        receiveSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_NOT_SUPPORTED);
+        verifyAbsoluteVolumeDisabled();
+    }
+
+    @Test
+    public void avcEnabled_setAudioVolumeLevelFeatureAborted_absoluteVolumeDisabled() {
+        enableAbsoluteVolumeControl();
+
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                getSystemAudioDeviceLogicalAddress(), getLogicalAddress(),
+                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL, Constants.ABORT_UNRECOGNIZED_OPCODE));
+        mTestLooper.dispatchAll();
+        verifyAbsoluteVolumeDisabled();
+    }
+
+    @Test
+    public void avcEnabled_systemAudioModeDisabled_absoluteVolumeDisabled() {
+        // Only run when the System Audio device is an Audio System.
+        assume().that(getSystemAudioDeviceType()).isEqualTo(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM);
+
+        enableAbsoluteVolumeControl();
+
+        receiveSetSystemAudioMode(false);
+        verifyAbsoluteVolumeDisabled();
+    }
+
+    @Test
+    public void avcEnabled_receiveReportAudioStatus_notifiesVolumeOrMuteChanges() {
+        // Initial <Report Audio Status> has volume=50 and mute=false
+        enableAbsoluteVolumeControl();
+
+        // New volume and mute status: sets both
+        receiveReportAudioStatus(20, true);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(5),
+                anyInt());
+        verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_MUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // New volume only: sets volume only
+        receiveReportAudioStatus(32, true);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_MUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // New mute status only: sets mute only
+        receiveReportAudioStatus(32, false);
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // Repeat of earlier message: sets neither volume nor mute
+        receiveReportAudioStatus(32, false);
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE), anyInt());
+        clearInvocations(mAudioManager);
+
+        // If AudioService causes us to send <Set Audio Volume Level>, the System Audio device's
+        // volume changes. Afterward, a duplicate of an earlier <Report Audio Status> should
+        // still cause us to call setStreamVolume()
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged(
+                getAudioOutputDevice(),
+                new VolumeInfo.Builder(ENABLE_AVC_VOLUME_INFO)
+                        .setVolumeIndex(20)
+                        .build()
+        );
+        mTestLooper.dispatchAll();
+        receiveReportAudioStatus(32, false);
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(8),
+                anyInt());
+        verify(mAudioManager, never()).adjustStreamVolume(eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE), anyInt());
+    }
+
+    @Test
+    public void avcEnabled_audioDeviceVolumeAdjusted_sendsUserControlPressedAndGiveAudioStatus() {
+        enableAbsoluteVolumeControl();
+        mNativeWrapper.clearResultMessages();
+
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted(
+                getAudioOutputDevice(),
+                ENABLE_AVC_VOLUME_INFO,
+                AudioManager.ADJUST_RAISE,
+                AudioDeviceVolumeManager.ADJUST_MODE_NORMAL
+        );
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildUserControlPressed(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress(), CEC_KEYCODE_VOLUME_UP));
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildUserControlReleased(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress()));
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress()));
+    }
+
+    @Test
+    public void avcEnabled_audioDeviceVolumeChanged_sendsSetAudioVolumeLevel() {
+        enableAbsoluteVolumeControl();
+        mNativeWrapper.clearResultMessages();
+
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeChanged(
+                getAudioOutputDevice(),
+                new VolumeInfo.Builder(ENABLE_AVC_VOLUME_INFO)
+                        .setVolumeIndex(20)
+                        .build()
+        );
+        mTestLooper.dispatchAll();
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                SetAudioVolumeLevelMessage.build(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress(), 20));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
index 5cec8ad..28ba4bb 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DetectTvSystemAudioModeSupportActionTest.java
@@ -56,7 +56,7 @@
         mDeviceInfoForTests = HdmiDeviceInfo.hardwarePort(1001, 1234);
         HdmiControlService hdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
 
                     @Override
                     void sendCecCommand(
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index 52a0b6c..545f318 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
@@ -78,7 +79,8 @@
 
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -110,6 +112,7 @@
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x2000;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index 35432ed..d7fef90 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -20,6 +20,7 @@
 import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY;
 import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_3;
@@ -100,7 +101,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -136,6 +137,7 @@
 
         mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x0000);
         mPowerManager = new FakePowerManagerWrapper(context);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index e77cd91..72d36b0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -20,6 +20,7 @@
 import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY;
 import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
 import static com.android.server.hdmi.Constants.ADDR_TV;
@@ -109,7 +110,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -145,6 +146,7 @@
                                  true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java
new file mode 100644
index 0000000..d33ef9b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeAudioDeviceVolumeManagerWrapper.java
@@ -0,0 +1,72 @@
+/*
+ * 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.hdmi;
+
+import static android.media.AudioDeviceVolumeManager.OnAudioDeviceVolumeChangedListener;
+import static android.media.AudioDeviceVolumeManager.OnDeviceVolumeBehaviorChangedListener;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceVolumeManager;
+import android.media.AudioManager;
+import android.media.VolumeInfo;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Wrapper for {@link AudioDeviceVolumeManager} that stubs its methods. Useful for testing.
+ */
+public class FakeAudioDeviceVolumeManagerWrapper implements
+        AudioDeviceVolumeManagerWrapperInterface {
+
+    private final Set<OnDeviceVolumeBehaviorChangedListener> mVolumeBehaviorListeners;
+
+    public FakeAudioDeviceVolumeManagerWrapper() {
+        mVolumeBehaviorListeners = new HashSet<>();
+    }
+
+    @Override
+    public void addOnDeviceVolumeBehaviorChangedListener(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnDeviceVolumeBehaviorChangedListener listener)
+            throws SecurityException {
+        mVolumeBehaviorListeners.add(listener);
+    }
+
+    @Override
+    public void removeOnDeviceVolumeBehaviorChangedListener(
+            @NonNull OnDeviceVolumeBehaviorChangedListener listener) {
+        mVolumeBehaviorListeners.remove(listener);
+    }
+
+    @Override
+    public void setDeviceAbsoluteVolumeBehavior(
+            @NonNull AudioDeviceAttributes device,
+            @NonNull VolumeInfo volume,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnAudioDeviceVolumeChangedListener vclistener,
+            boolean handlesVolumeAdjustment) {
+        // Notify all volume behavior listeners that the device adopted absolute volume behavior
+        for (OnDeviceVolumeBehaviorChangedListener listener : mVolumeBehaviorListeners) {
+            listener.onDeviceVolumeBehaviorChanged(device,
+                    AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
index 30bcc7e..9f744f9 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
@@ -89,7 +89,8 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList()));
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
         doReturn(mHdmiCecAtomWriterSpy).when(mHdmiControlServiceSpy).getAtomWriter();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
index 2dcc449..0cba106 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecControllerTest.java
@@ -101,7 +101,8 @@
         mMyLooper = mTestLooper.getLooper();
 
         mHdmiControlServiceSpy = spy(new HdmiControlService(
-                InstrumentationRegistry.getTargetContext(), Collections.emptyList()));
+                InstrumentationRegistry.getTargetContext(), Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()));
         doReturn(mMyLooper).when(mHdmiControlServiceSpy).getIoLooper();
         doReturn(mMyLooper).when(mHdmiControlServiceSpy).getServiceLooper();
         doAnswer(__ -> mCecVersion).when(mHdmiControlServiceSpy).getCecVersion();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 70bc460..91d265c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -88,7 +88,7 @@
 
         mHdmiControlService =
             new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                    Collections.emptyList()) {
+                    Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                 @Override
                 AudioManager getAudioManager() {
                     return new AudioManager() {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 86130da..484b5a8 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ABORT_UNRECOGNIZED_OPCODE;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
@@ -92,7 +93,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     void wakeUp() {
                         mWokenUp = true;
@@ -151,6 +152,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.setPowerManagerInternal(mPowerManagerInternal);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
index fb8baa3..48e70fe 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
@@ -17,6 +17,7 @@
 
 import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_TV;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_TV;
@@ -137,7 +138,8 @@
         Context context = InstrumentationRegistry.getTargetContext();
 
         mHdmiControlService =
-                new HdmiControlService(context, Collections.emptyList()) {
+                new HdmiControlService(context, Collections.emptyList(),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return isControlEnabled;
@@ -190,6 +192,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
         mNativeWrapper.setPhysicalAddress(0x2000);
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index df4aa5d..f27b8c2 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ABORT_UNRECOGNIZED_OPCODE;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
@@ -29,8 +30,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.Context;
 import android.hardware.hdmi.HdmiControlManager;
@@ -125,7 +128,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     void wakeUp() {
                         mWokenUp = true;
@@ -179,6 +182,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
@@ -768,7 +772,7 @@
         // When the device reports its physical address, the listener eventually is invoked.
         HdmiCecMessage reportPhysicalAddress =
                 HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(
-                ADDR_PLAYBACK_2, 0x1000, HdmiDeviceInfo.DEVICE_PLAYBACK);
+                        ADDR_PLAYBACK_2, 0x1000, HdmiDeviceInfo.DEVICE_PLAYBACK);
         mNativeWrapper.onCecMessage(reportPhysicalAddress);
         mTestLooper.dispatchAll();
 
@@ -777,6 +781,54 @@
         assertThat(mDeviceEventListeners.size()).isEqualTo(1);
         assertThat(mDeviceEventListeners.get(0).getStatus())
                 .isEqualTo(HdmiControlManager.DEVICE_EVENT_ADD_DEVICE);
+    }
 
+    @Test
+    public void receiveSetAudioVolumeLevel_samNotActivated_noFeatureAbort_volumeChanges() {
+        when(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(25);
+
+        // Max volume of STREAM_MUSIC is retrieved on boot
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.onCecMessage(SetAudioVolumeLevelMessage.build(
+                ADDR_PLAYBACK_1,
+                ADDR_TV,
+                20));
+        mTestLooper.dispatchAll();
+
+        // <Feature Abort>[Not in correct mode] not sent
+        HdmiCecMessage featureAbortMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                ADDR_TV,
+                ADDR_PLAYBACK_1,
+                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
+                Constants.ABORT_NOT_IN_CORRECT_MODE);
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(featureAbortMessage);
+
+        // <Set Audio Volume Level> uses volume range [0, 100]; STREAM_MUSIC uses range [0, 25]
+        verify(mAudioManager).setStreamVolume(eq(AudioManager.STREAM_MUSIC), eq(5), anyInt());
+    }
+
+    @Test
+    public void receiveSetAudioVolumeLevel_samActivated_respondsFeatureAbort_noVolumeChange() {
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildSetSystemAudioMode(
+                ADDR_AUDIO_SYSTEM, ADDR_TV, true));
+        mTestLooper.dispatchAll();
+
+        mNativeWrapper.onCecMessage(SetAudioVolumeLevelMessage.build(
+                ADDR_PLAYBACK_1, ADDR_TV, 50));
+        mTestLooper.dispatchAll();
+
+        // <Feature Abort>[Not in correct mode] sent
+        HdmiCecMessage featureAbortMessage = HdmiCecMessageBuilder.buildFeatureAbortCommand(
+                ADDR_TV,
+                ADDR_PLAYBACK_1,
+                Constants.MESSAGE_SET_AUDIO_VOLUME_LEVEL,
+                Constants.ABORT_NOT_IN_CORRECT_MODE);
+        assertThat(mNativeWrapper.getResultMessages()).contains(featureAbortMessage);
+
+        // AudioManager not notified of volume change
+        verify(mAudioManager, never()).setStreamVolume(eq(AudioManager.STREAM_MUSIC), anyInt(),
+                anyInt());
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
index 50c9f70..a446e10 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
@@ -51,7 +51,8 @@
     @Before
     public void setUp() throws Exception {
         HdmiControlService mHdmiControlService = new HdmiControlService(
-                InstrumentationRegistry.getTargetContext(), Collections.emptyList());
+                InstrumentationRegistry.getTargetContext(), Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper());
 
         mHdmiControlService.setIoLooper(mTestLooper.getLooper());
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
index 03532ae..b8a1ba3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
@@ -67,7 +67,8 @@
     @Before
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getTargetContext();
-        mHdmiControlService = new HdmiControlService(mContext, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContext, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             void invokeDeviceEventListeners(HdmiDeviceInfo device, int status) {
                 mDeviceEventListenerStatuses.add(status);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index 7a68285..b94deed 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -15,6 +15,7 @@
  */
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -65,7 +66,8 @@
         Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         Looper myLooper = mTestLooper.getLooper();
 
-        mHdmiControlService = new HdmiControlService(contextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(contextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             boolean isControlEnabled() {
                 return true;
@@ -105,6 +107,7 @@
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(contextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.getHdmiCecNetwork().initPortInfo();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 3987c32..6266571 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -91,7 +91,8 @@
 
         HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList()));
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 561e6a5..46a4e86 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_TV;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 import static com.android.server.hdmi.OneTouchPlayAction.STATE_WAITING_FOR_REPORT_POWER_STATUS;
@@ -87,7 +88,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         mHdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -120,6 +122,7 @@
         mHdmiControlService.setCecController(hdmiCecController);
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x2000;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java
new file mode 100644
index 0000000..6418602
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToAudioSystemAvcTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.hdmi;
+
+import android.hardware.hdmi.DeviceFeatures;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDeviceAttributes;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.android.collect.Lists;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+/**
+ * Tests for Absolute Volume Control where the local device is a Playback device and the
+ * System Audio device is an Audio System.
+ */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class PlaybackDeviceToAudioSystemAvcTest extends BaseAbsoluteVolumeControlTest {
+
+    @Override
+    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
+        return new HdmiCecLocalDevicePlayback(hdmiControlService);
+    }
+
+    @Override
+    protected int getPhysicalAddress() {
+        return 0x1100;
+    }
+
+    @Override
+    protected int getDeviceType() {
+        return HdmiDeviceInfo.DEVICE_PLAYBACK;
+    }
+
+    @Override
+    protected AudioDeviceAttributes getAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceLogicalAddress() {
+        return Constants.ADDR_AUDIO_SYSTEM;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceType() {
+        return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
+    }
+
+    /**
+     * AVC is disabled if the Audio System disables System Audio mode, and the TV has unknown
+     * support for <Set Audio Volume Level>. It is enabled once the TV confirms support for
+     * <Set Audio Volume Level> and sends <Report Audio Status>.
+     */
+    @Test
+    public void switchToTv_absoluteVolumeControlDisabledUntilAllConditionsMet() {
+        enableAbsoluteVolumeControl();
+
+        // Audio System disables System Audio Mode. AVC should be disabled.
+        receiveSetSystemAudioMode(false);
+        verifyAbsoluteVolumeDisabled();
+
+        // TV reports support for <Set Audio Volume Level>
+        mNativeWrapper.onCecMessage(ReportFeaturesMessage.build(
+                Constants.ADDR_TV, HdmiControlManager.HDMI_CEC_VERSION_2_0,
+                Arrays.asList(HdmiDeviceInfo.DEVICE_TV), Constants.RC_PROFILE_TV,
+                Lists.newArrayList(Constants.RC_PROFILE_TV_NONE),
+                DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
+                        .setSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED)
+                        .build()));
+        mTestLooper.dispatchAll();
+
+        // TV reports its initial audio status
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus(
+                Constants.ADDR_TV,
+                getLogicalAddress(),
+                30,
+                false));
+        mTestLooper.dispatchAll();
+
+        verifyAbsoluteVolumeEnabled();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java
new file mode 100644
index 0000000..504c3bc
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PlaybackDeviceToTvAvcTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.hdmi;
+
+import static org.mockito.Mockito.clearInvocations;
+
+import android.hardware.hdmi.DeviceFeatures;
+import android.hardware.hdmi.HdmiControlManager;
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDeviceAttributes;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Tests for Absolute Volume Control where the local device is a Playback device and the
+ * System Audio device is a TV.
+ */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class PlaybackDeviceToTvAvcTest extends BaseAbsoluteVolumeControlTest {
+
+    @Override
+    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
+        return new HdmiCecLocalDevicePlayback(hdmiControlService);
+    }
+
+    @Override
+    protected int getPhysicalAddress() {
+        return 0x1100;
+    }
+
+    @Override
+    protected int getDeviceType() {
+        return HdmiDeviceInfo.DEVICE_PLAYBACK;
+    }
+
+    @Override
+    protected AudioDeviceAttributes getAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceLogicalAddress() {
+        return Constants.ADDR_TV;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceType() {
+        return HdmiDeviceInfo.DEVICE_TV;
+    }
+
+    /**
+     * AVC is disabled when an Audio System with unknown support for <Set Audio Volume Level>
+     * becomes the System Audio device. It is enabled once the Audio System reports that it
+     * supports <Set Audio Volume Level> and sends <Report Audio Status>.
+     */
+    @Test
+    public void switchToAudioSystem_absoluteVolumeControlDisabledUntilAllConditionsMet() {
+        enableAbsoluteVolumeControl();
+
+        // Audio System enables System Audio Mode. AVC should be disabled.
+        receiveSetSystemAudioMode(true);
+        verifyAbsoluteVolumeDisabled();
+
+        clearInvocations(mAudioManager, mAudioDeviceVolumeManager);
+
+        // Audio System reports support for <Set Audio Volume Level>
+        mNativeWrapper.onCecMessage(ReportFeaturesMessage.build(
+                Constants.ADDR_AUDIO_SYSTEM, HdmiControlManager.HDMI_CEC_VERSION_2_0,
+                Arrays.asList(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM), Constants.RC_PROFILE_SOURCE,
+                Collections.emptyList(),
+                DeviceFeatures.NO_FEATURES_SUPPORTED.toBuilder()
+                        .setSetAudioVolumeLevelSupport(DeviceFeatures.FEATURE_SUPPORTED)
+                        .build()));
+        mTestLooper.dispatchAll();
+
+        // Audio system reports its initial audio status
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus(
+                Constants.ADDR_AUDIO_SYSTEM,
+                getLogicalAddress(),
+                30,
+                false));
+        mTestLooper.dispatchAll();
+
+        verifyAbsoluteVolumeEnabled();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index c878f99..e5058be 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
@@ -68,7 +69,8 @@
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
 
         mHdmiControlService = new HdmiControlService(mContextSpy,
-                Collections.singletonList(HdmiDeviceInfo.DEVICE_TV)) {
+                Collections.singletonList(HdmiDeviceInfo.DEVICE_TV),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -110,6 +112,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfo);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mPhysicalAddress = 0x0000;
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index 6184c21..f7983ca 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
@@ -96,8 +97,8 @@
         mMyLooper = mTestLooper.getLooper();
 
         mHdmiControlService =
-                new HdmiControlService(context,
-                        Collections.emptyList()) {
+                new HdmiControlService(context, Collections.emptyList(),
+                        new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -125,6 +126,7 @@
         mHdmiControlService.setHdmiMhlController(HdmiMhlControllerStub.create(mHdmiControlService));
         mLocalDevices.add(mHdmiCecLocalDeviceTv);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index 0587864..566a7e0 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.hdmi;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
 import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
@@ -149,7 +150,7 @@
 
         mHdmiControlService =
                 new HdmiControlService(InstrumentationRegistry.getTargetContext(),
-                        Collections.emptyList()) {
+                        Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     boolean isControlEnabled() {
                         return true;
@@ -186,6 +187,7 @@
                         true, false, false);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
index a34b55c..087e407 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
@@ -20,6 +20,7 @@
 import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED;
 import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORT_UNKNOWN;
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -81,7 +82,8 @@
         mContextSpy = spy(new ContextWrapper(
                 InstrumentationRegistry.getInstrumentation().getTargetContext()));
 
-        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList()));
+        mHdmiControlServiceSpy = spy(new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()));
         doNothing().when(mHdmiControlServiceSpy)
                 .writeStringSystemProperty(anyString(), anyString());
 
@@ -98,6 +100,7 @@
         mHdmiControlServiceSpy.setHdmiMhlController(
                 HdmiMhlControllerStub.create(mHdmiControlServiceSpy));
         mHdmiControlServiceSpy.initService();
+        mHdmiControlServiceSpy.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlServiceSpy.setPowerManager(mPowerManager);
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index 9d14341..1644252 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -17,6 +17,7 @@
 package com.android.server.hdmi;
 
 
+import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
 import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
 import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
 import static com.android.server.hdmi.SystemAudioAutoInitiationAction.RETRIES_ON_TIMEOUT;
@@ -69,7 +70,8 @@
 
         Looper myLooper = mTestLooper.getLooper();
 
-        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList()) {
+        mHdmiControlService = new HdmiControlService(mContextSpy, Collections.emptyList(),
+                new FakeAudioDeviceVolumeManagerWrapper()) {
             @Override
             AudioManager getAudioManager() {
                 return new AudioManager() {
@@ -108,6 +110,7 @@
                 new HdmiPortInfo(2, HdmiPortInfo.PORT_INPUT, 0x2000, true, false, true);
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mHdmiControlService.initService();
+        mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
index 095c69c..c2f706a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioInitiationActionFromAvrTest.java
@@ -69,7 +69,7 @@
         Context context = InstrumentationRegistry.getTargetContext();
 
         HdmiControlService hdmiControlService = new HdmiControlService(context,
-                Collections.emptyList()) {
+                Collections.emptyList(), new FakeAudioDeviceVolumeManagerWrapper()) {
                     @Override
                     void sendCecCommand(
                             HdmiCecMessage command, @Nullable SendMessageCallback callback) {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java
new file mode 100644
index 0000000..41c0e0d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvcTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hdmi;
+
+import android.hardware.hdmi.HdmiDeviceInfo;
+import android.media.AudioDeviceAttributes;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for Absolute Volume Control where the local device is a TV and the System Audio device
+ * is an Audio System. Assumes that the TV uses ARC (rather than eARC).
+ */
+@SmallTest
+@Presubmit
+@RunWith(JUnit4.class)
+public class TvToAudioSystemAvcTest extends BaseAbsoluteVolumeControlTest {
+
+    @Override
+    protected HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService) {
+        return new HdmiCecLocalDeviceTv(hdmiControlService);
+    }
+
+    @Override
+    protected int getPhysicalAddress() {
+        return 0x0000;
+    }
+
+    @Override
+    protected int getDeviceType() {
+        return HdmiDeviceInfo.DEVICE_TV;
+    }
+
+    @Override
+    protected AudioDeviceAttributes getAudioOutputDevice() {
+        return HdmiControlService.AUDIO_OUTPUT_DEVICE_HDMI_ARC;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceLogicalAddress() {
+        return Constants.ADDR_AUDIO_SYSTEM;
+    }
+
+    @Override
+    protected int getSystemAudioDeviceType() {
+        return HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index bd35be4..c735d18 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -22,7 +22,6 @@
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
@@ -39,7 +38,6 @@
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
 import android.os.HandlerThread;
 import android.os.LocaleList;
@@ -102,8 +100,6 @@
     @Mock
     private Context mMockContext;
     @Mock
-    private PackageManagerInternal mMockPackageManagerInternal;
-    @Mock
     private PackageManager mMockPackageManager;
     @Mock
     private LocaleManagerService mMockLocaleManagerService;
@@ -129,7 +125,6 @@
     @Before
     public void setUp() throws Exception {
         mMockContext = mock(Context.class);
-        mMockPackageManagerInternal = mock(PackageManagerInternal.class);
         mMockPackageManager = mock(PackageManager.class);
         mMockLocaleManagerService = mock(LocaleManagerService.class);
         SystemAppUpdateTracker systemAppUpdateTracker = mock(SystemAppUpdateTracker.class);
@@ -141,7 +136,7 @@
         broadcastHandlerThread.start();
 
         mBackupHelper = spy(new ShadowLocaleManagerBackupHelper(mMockContext,
-                mMockLocaleManagerService, mMockPackageManagerInternal, mClock, STAGE_DATA,
+                mMockLocaleManagerService, mMockPackageManager, mClock, STAGE_DATA,
                 broadcastHandlerThread));
         doNothing().when(mBackupHelper).notifyBackupManager();
 
@@ -158,8 +153,8 @@
 
     @Test
     public void testBackupPayload_noAppsInstalled_returnsNull() throws Exception {
-        doReturn(List.of()).when(mMockPackageManagerInternal)
-                .getInstalledApplications(anyLong(), anyInt(), anyInt());
+        doReturn(List.of()).when(mMockPackageManager)
+                .getInstalledApplicationsAsUser(any(), anyInt());
 
         assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
     }
@@ -198,8 +193,8 @@
         ApplicationInfo anotherAppInfo = new ApplicationInfo();
         defaultAppInfo.packageName = DEFAULT_PACKAGE_NAME;
         anotherAppInfo.packageName = "com.android.anotherapp";
-        doReturn(List.of(defaultAppInfo, anotherAppInfo)).when(mMockPackageManagerInternal)
-                .getInstalledApplications(anyLong(), anyInt(), anyInt());
+        doReturn(List.of(defaultAppInfo, anotherAppInfo)).when(mMockPackageManager)
+                .getInstalledApplicationsAsUser(any(), anyInt());
 
         setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, DEFAULT_LOCALES);
         // Exception when getting locales for anotherApp.
@@ -447,8 +442,8 @@
         // Retention period has not elapsed.
         setCurrentTimeMillis(
                 DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.minusHours(1).toMillis());
-        doReturn(List.of()).when(mMockPackageManagerInternal)
-                .getInstalledApplications(anyLong(), anyInt(), anyInt());
+        doReturn(List.of()).when(mMockPackageManager)
+                .getInstalledApplicationsAsUser(any(), anyInt());
         assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
 
         checkStageDataExists(DEFAULT_USER_ID);
@@ -456,8 +451,8 @@
         // Exactly RETENTION_PERIOD amount of time has passed so stage data should still not be
         // removed.
         setCurrentTimeMillis(DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.toMillis());
-        doReturn(List.of()).when(mMockPackageManagerInternal)
-                .getInstalledApplications(anyLong(), anyInt(), anyInt());
+        doReturn(List.of()).when(mMockPackageManager)
+                .getInstalledApplicationsAsUser(any(), anyInt());
         assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
 
         checkStageDataExists(DEFAULT_USER_ID);
@@ -465,8 +460,8 @@
         // Retention period has now expired, stage data should be deleted.
         setCurrentTimeMillis(
                 DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.plusSeconds(1).toMillis());
-        doReturn(List.of()).when(mMockPackageManagerInternal)
-                .getInstalledApplications(anyLong(), anyInt(), anyInt());
+        doReturn(List.of()).when(mMockPackageManager)
+                .getInstalledApplicationsAsUser(any(), anyInt());
         assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
 
         checkStageDataDoesNotExist(DEFAULT_USER_ID);
@@ -577,8 +572,8 @@
     private void setUpDummyAppForPackageManager(String packageName) {
         ApplicationInfo dummyApp = new ApplicationInfo();
         dummyApp.packageName = packageName;
-        doReturn(List.of(dummyApp)).when(mMockPackageManagerInternal)
-                .getInstalledApplications(anyLong(), anyInt(), anyInt());
+        doReturn(List.of(dummyApp)).when(mMockPackageManager)
+                .getInstalledApplicationsAsUser(any(), anyInt());
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
index 0b3ef45..1dcdbac 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java
@@ -23,7 +23,6 @@
 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.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
@@ -40,7 +39,6 @@
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
 import android.os.LocaleList;
 
@@ -80,7 +78,7 @@
     @Mock
     private Context mMockContext;
     @Mock
-    private PackageManagerInternal mMockPackageManagerInternal;
+    private PackageManager mMockPackageManager;
     @Mock
     private FakePackageConfigurationUpdater mFakePackageConfigurationUpdater;
     @Mock
@@ -95,14 +93,13 @@
         mMockContext = mock(Context.class);
         mMockActivityTaskManager = mock(ActivityTaskManagerInternal.class);
         mMockActivityManager = mock(ActivityManagerInternal.class);
-        mMockPackageManagerInternal = mock(PackageManagerInternal.class);
+        mMockPackageManager = mock(PackageManager.class);
         mMockPackageMonitor = mock(PackageMonitor.class);
 
         // For unit tests, set the default installer info
-        PackageManager mockPackageManager = mock(PackageManager.class);
-        doReturn(DEFAULT_INSTALL_SOURCE_INFO).when(mockPackageManager)
+        doReturn(DEFAULT_INSTALL_SOURCE_INFO).when(mMockPackageManager)
                 .getInstallSourceInfo(anyString());
-        doReturn(mockPackageManager).when(mMockContext).getPackageManager();
+        doReturn(mMockPackageManager).when(mMockContext).getPackageManager();
 
         mFakePackageConfigurationUpdater = new FakePackageConfigurationUpdater();
         doReturn(mFakePackageConfigurationUpdater)
@@ -117,14 +114,14 @@
 
         mMockBackupHelper = mock(ShadowLocaleManagerBackupHelper.class);
         mLocaleManagerService = new LocaleManagerService(mMockContext, mMockActivityTaskManager,
-                mMockActivityManager, mMockPackageManagerInternal,
+                mMockActivityManager, mMockPackageManager,
                 mMockBackupHelper, mMockPackageMonitor);
     }
 
     @Test(expected = SecurityException.class)
     public void testSetApplicationLocales_arbitraryAppWithoutPermissions_fails() throws Exception {
         doReturn(DEFAULT_UID)
-                .when(mMockPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt());
+                .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt());
         setUpFailingPermissionCheckFor(Manifest.permission.CHANGE_CONFIGURATION);
 
         try {
@@ -170,7 +167,7 @@
     @Test
     public void testSetApplicationLocales_arbitraryAppWithPermission_succeeds() throws Exception {
         doReturn(DEFAULT_UID)
-                .when(mMockPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt());
+                .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt());
         // if package is not owned by the caller, the calling app should have the following
         //   permission. We will mock this to succeed to imitate that.
         setUpPassingPermissionCheckFor(Manifest.permission.CHANGE_CONFIGURATION);
@@ -186,7 +183,7 @@
     @Test
     public void testSetApplicationLocales_callerOwnsPackage_succeeds() throws Exception {
         doReturn(Binder.getCallingUid())
-                .when(mMockPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt());
+                .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt());
 
         mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
                 DEFAULT_LOCALES);
@@ -197,8 +194,8 @@
 
     @Test(expected = IllegalArgumentException.class)
     public void testSetApplicationLocales_invalidPackageOrUserId_fails() throws Exception {
-        doReturn(INVALID_UID)
-                .when(mMockPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt());
+        doThrow(new PackageManager.NameNotFoundException("Mock"))
+                .when(mMockPackageManager).getPackageUidAsUser(anyString(), any(), anyInt());
         try {
             mLocaleManagerService.setApplicationLocales(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID,
                     LocaleList.getEmptyLocaleList());
@@ -211,8 +208,8 @@
 
     @Test(expected = SecurityException.class)
     public void testGetApplicationLocales_arbitraryAppWithoutPermission_fails() throws Exception {
-        doReturn(DEFAULT_UID).when(mMockPackageManagerInternal)
-                .getPackageUid(anyString(), anyLong(), anyInt());
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUidAsUser(anyString(), any(), anyInt());
         setUpFailingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
 
         try {
@@ -229,8 +226,8 @@
     public void testGetApplicationLocales_appSpecificConfigAbsent_returnsEmptyList()
             throws Exception {
         // any valid app calling for its own package or having appropriate permission
-        doReturn(DEFAULT_UID).when(mMockPackageManagerInternal)
-                .getPackageUid(anyString(), anyLong(), anyInt());
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUidAsUser(anyString(), any(), anyInt());
         setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
         doReturn(null)
                 .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
@@ -244,8 +241,8 @@
     @Test
     public void testGetApplicationLocales_appSpecificLocalesAbsent_returnsEmptyList()
             throws Exception {
-        doReturn(DEFAULT_UID).when(mMockPackageManagerInternal)
-                .getPackageUid(anyString(), anyLong(), anyInt());
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUidAsUser(anyString(), any(), anyInt());
         setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
         doReturn(new PackageConfig(/* nightMode = */ 0, /* locales = */ null))
                 .when(mMockActivityTaskManager).getApplicationConfig(any(), anyInt());
@@ -259,8 +256,8 @@
     @Test
     public void testGetApplicationLocales_callerOwnsAppAndConfigPresent_returnsLocales()
             throws Exception {
-        doReturn(Binder.getCallingUid()).when(mMockPackageManagerInternal)
-                .getPackageUid(anyString(), anyLong(), anyInt());
+        doReturn(Binder.getCallingUid()).when(mMockPackageManager)
+                .getPackageUidAsUser(anyString(), any(), anyInt());
         doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
                 .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
 
@@ -273,8 +270,8 @@
     @Test
     public void testGetApplicationLocales_arbitraryCallerWithPermissions_returnsLocales()
             throws Exception {
-        doReturn(DEFAULT_UID).when(mMockPackageManagerInternal)
-                .getPackageUid(anyString(), anyLong(), anyInt());
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUidAsUser(anyString(), any(), anyInt());
         setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES);
         doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
                 .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
@@ -288,10 +285,10 @@
     @Test
     public void testGetApplicationLocales_callerIsInstaller_returnsLocales()
             throws Exception {
-        doReturn(DEFAULT_UID).when(mMockPackageManagerInternal)
-                .getPackageUid(eq(DEFAULT_PACKAGE_NAME), anyLong(), anyInt());
-        doReturn(Binder.getCallingUid()).when(mMockPackageManagerInternal)
-                .getPackageUid(eq(DEFAULT_INSTALLER_PACKAGE_NAME), anyLong(), anyInt());
+        doReturn(DEFAULT_UID).when(mMockPackageManager)
+                .getPackageUidAsUser(eq(DEFAULT_PACKAGE_NAME), any(), anyInt());
+        doReturn(Binder.getCallingUid()).when(mMockPackageManager)
+                .getPackageUidAsUser(eq(DEFAULT_INSTALLER_PACKAGE_NAME), any(), anyInt());
         doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES))
                 .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt());
 
diff --git a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
index ad9be0d..e403c87 100644
--- a/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
+++ b/services/tests/servicestests/src/com/android/server/locales/ShadowLocaleManagerBackupHelper.java
@@ -17,7 +17,7 @@
 package com.android.server.locales;
 
 import android.content.Context;
-import android.content.pm.PackageManagerInternal;
+import android.content.pm.PackageManager;
 import android.os.HandlerThread;
 import android.util.SparseArray;
 
@@ -31,9 +31,10 @@
 public class ShadowLocaleManagerBackupHelper extends LocaleManagerBackupHelper {
     ShadowLocaleManagerBackupHelper(Context context,
             LocaleManagerService localeManagerService,
-            PackageManagerInternal pmInternal, Clock clock,
+            PackageManager packageManager, Clock clock,
             SparseArray<LocaleManagerBackupHelper.StagedData> stagedData,
             HandlerThread broadcastHandlerThread) {
-        super(context, localeManagerService, pmInternal, clock, stagedData, broadcastHandlerThread);
+        super(context, localeManagerService, packageManager, clock, stagedData,
+                broadcastHandlerThread);
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
index db602ca..808b74e 100644
--- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java
@@ -38,7 +38,6 @@
 import android.content.pm.InstallSourceInfo;
 import android.content.pm.PackageInstaller;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
 import android.os.Environment;
 import android.os.LocaleList;
@@ -93,8 +92,6 @@
     @Mock
     PackageManager mMockPackageManager;
     @Mock
-    private PackageManagerInternal mMockPackageManagerInternal;
-    @Mock
     private ActivityTaskManagerInternal mMockActivityTaskManager;
     @Mock
     private ActivityManagerInternal mMockActivityManager;
@@ -110,21 +107,20 @@
         mMockContext = mock(Context.class);
         mMockActivityTaskManager = mock(ActivityTaskManagerInternal.class);
         mMockActivityManager = mock(ActivityManagerInternal.class);
-        mMockPackageManagerInternal = mock(PackageManagerInternal.class);
+        mMockPackageManager = mock(PackageManager.class);
         LocaleManagerBackupHelper mockLocaleManagerBackupHelper =
                 mock(ShadowLocaleManagerBackupHelper.class);
         // PackageMonitor is not needed in LocaleManagerService for these tests hence it is
         // passed as null.
         mLocaleManagerService = new LocaleManagerService(mMockContext,
                 mMockActivityTaskManager, mMockActivityManager,
-                mMockPackageManagerInternal, mockLocaleManagerBackupHelper,
+                mMockPackageManager, mockLocaleManagerBackupHelper,
                 /* mPackageMonitor= */ null);
 
         doReturn(DEFAULT_USER_ID).when(mMockActivityManager)
                 .handleIncomingUser(anyInt(), anyInt(), eq(DEFAULT_USER_ID), anyBoolean(), anyInt(),
                         anyString(), anyString());
 
-        mMockPackageManager = mock(PackageManager.class);
         doReturn(DEFAULT_INSTALL_SOURCE_INFO).when(mMockPackageManager)
                 .getInstallSourceInfo(anyString());
         doReturn(mMockPackageManager).when(mMockContext).getPackageManager();
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java b/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
index 004d7bc..07cca0c 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
@@ -84,15 +84,15 @@
 @RunWith(AndroidJUnit4.class)
 public class PackageParserLegacyCoreTest {
     private static final String RELEASED = null;
-    private static final String OLDER_PRE_RELEASE = "A";
-    private static final String PRE_RELEASE = "B";
-    private static final String NEWER_PRE_RELEASE = "C";
+    private static final String OLDER_PRE_RELEASE = "Q";
+    private static final String PRE_RELEASE = "R";
+    private static final String NEWER_PRE_RELEASE = "Z";
 
     // Codenames with a fingerprint attached to them. These may only be present in the apps
     // declared min SDK and not as platform codenames.
-    private static final String OLDER_PRE_RELEASE_WITH_FINGERPRINT = "A.fingerprint";
-    private static final String PRE_RELEASE_WITH_FINGERPRINT = "B.fingerprint";
-    private static final String NEWER_PRE_RELEASE_WITH_FINGERPRINT = "C.fingerprint";
+    private static final String OLDER_PRE_RELEASE_WITH_FINGERPRINT = "Q.fingerprint";
+    private static final String PRE_RELEASE_WITH_FINGERPRINT = "R.fingerprint";
+    private static final String NEWER_PRE_RELEASE_WITH_FINGERPRINT = "Z.fingerprint";
 
     private static final String[] CODENAMES_RELEASED = { /* empty */};
     private static final String[] CODENAMES_PRE_RELEASE = {PRE_RELEASE};
@@ -199,13 +199,14 @@
     }
 
     private void verifyComputeTargetSdkVersion(int targetSdkVersion, String targetSdkCodename,
-            boolean isPlatformReleased, int expectedTargetSdk) {
+            boolean isPlatformReleased, boolean allowUnknownCodenames, int expectedTargetSdk) {
         final ParseTypeImpl input = ParseTypeImpl.forParsingWithoutPlatformCompat();
         final ParseResult<Integer> result = FrameworkParsingPackageUtils.computeTargetSdkVersion(
                 targetSdkVersion,
                 targetSdkCodename,
                 isPlatformReleased ? CODENAMES_RELEASED : CODENAMES_PRE_RELEASE,
-                input);
+                input,
+                allowUnknownCodenames);
 
         if (expectedTargetSdk == -1) {
             assertTrue(result.isError());
@@ -220,40 +221,61 @@
         // Do allow older release targetSdkVersion on pre-release platform.
         // APP: Released API 10
         // DEV: Pre-release API 20
-        verifyComputeTargetSdkVersion(OLDER_VERSION, RELEASED, false, OLDER_VERSION);
+        verifyComputeTargetSdkVersion(OLDER_VERSION, RELEASED, false, false, OLDER_VERSION);
 
         // Do allow same release targetSdkVersion on pre-release platform.
         // APP: Released API 20
         // DEV: Pre-release API 20
-        verifyComputeTargetSdkVersion(PLATFORM_VERSION, RELEASED, false, PLATFORM_VERSION);
+        verifyComputeTargetSdkVersion(PLATFORM_VERSION, RELEASED, false, false, PLATFORM_VERSION);
 
         // Do allow newer release targetSdkVersion on pre-release platform.
         // APP: Released API 30
         // DEV: Pre-release API 20
-        verifyComputeTargetSdkVersion(NEWER_VERSION, RELEASED, false, NEWER_VERSION);
+        verifyComputeTargetSdkVersion(NEWER_VERSION, RELEASED, false, false, NEWER_VERSION);
 
         // Don't allow older pre-release targetSdkVersion on pre-release platform.
         // APP: Pre-release API 10
         // DEV: Pre-release API 20
-        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE, false, -1);
-        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE_WITH_FINGERPRINT, false, -1);
+        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE, false, false, -1);
+        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE_WITH_FINGERPRINT, false,
+                false, -1
+        );
 
+        // Don't allow older pre-release targetSdkVersion on pre-release platform when
+        // allowUnknownCodenames is true.
+        // APP: Pre-release API 10
+        // DEV: Pre-release API 20
+        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE, false,
+                true, -1);
+        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE_WITH_FINGERPRINT, false,
+                true, -1);
 
         // Do allow same pre-release targetSdkVersion on pre-release platform,
         // but overwrite the specified version with CUR_DEVELOPMENT.
         // APP: Pre-release API 20
         // DEV: Pre-release API 20
         verifyComputeTargetSdkVersion(PLATFORM_VERSION, PRE_RELEASE, false,
-                Build.VERSION_CODES.CUR_DEVELOPMENT);
+                false, Build.VERSION_CODES.CUR_DEVELOPMENT);
         verifyComputeTargetSdkVersion(PLATFORM_VERSION, PRE_RELEASE_WITH_FINGERPRINT, false,
-                Build.VERSION_CODES.CUR_DEVELOPMENT);
-
+                false, Build.VERSION_CODES.CUR_DEVELOPMENT);
 
         // Don't allow newer pre-release targetSdkVersion on pre-release platform.
         // APP: Pre-release API 30
         // DEV: Pre-release API 20
-        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE, false, -1);
-        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE_WITH_FINGERPRINT, false, -1);
+        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE, false, false, -1);
+        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE_WITH_FINGERPRINT, false,
+                false, -1
+        );
+
+        // Do allow newer pre-release targetSdkVersion on pre-release platform when
+        // allowUnknownCodenames is true.
+        // APP: Pre-release API 30
+        // DEV: Pre-release API 20
+        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE, false,
+                true, Build.VERSION_CODES.CUR_DEVELOPMENT);
+        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE_WITH_FINGERPRINT, false,
+                true, Build.VERSION_CODES.CUR_DEVELOPMENT);
+
     }
 
     @Test
@@ -261,36 +283,58 @@
         // Do allow older release targetSdkVersion on released platform.
         // APP: Released API 10
         // DEV: Released API 20
-        verifyComputeTargetSdkVersion(OLDER_VERSION, RELEASED, true, OLDER_VERSION);
+        verifyComputeTargetSdkVersion(OLDER_VERSION, RELEASED, true, false, OLDER_VERSION);
 
         // Do allow same release targetSdkVersion on released platform.
         // APP: Released API 20
         // DEV: Released API 20
-        verifyComputeTargetSdkVersion(PLATFORM_VERSION, RELEASED, true, PLATFORM_VERSION);
+        verifyComputeTargetSdkVersion(PLATFORM_VERSION, RELEASED, true, false, PLATFORM_VERSION);
 
         // Do allow newer release targetSdkVersion on released platform.
         // APP: Released API 30
         // DEV: Released API 20
-        verifyComputeTargetSdkVersion(NEWER_VERSION, RELEASED, true, NEWER_VERSION);
+        verifyComputeTargetSdkVersion(NEWER_VERSION, RELEASED, true, false, NEWER_VERSION);
 
         // Don't allow older pre-release targetSdkVersion on released platform.
         // APP: Pre-release API 10
         // DEV: Released API 20
-        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE, true, -1);
-        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE_WITH_FINGERPRINT, true, -1);
+        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE, true, false, -1);
+        verifyComputeTargetSdkVersion(OLDER_VERSION, OLDER_PRE_RELEASE_WITH_FINGERPRINT, true,
+                false, -1
+        );
 
         // Don't allow same pre-release targetSdkVersion on released platform.
         // APP: Pre-release API 20
         // DEV: Released API 20
-        verifyComputeTargetSdkVersion(PLATFORM_VERSION, PRE_RELEASE, true, -1);
-        verifyComputeTargetSdkVersion(PLATFORM_VERSION, PRE_RELEASE_WITH_FINGERPRINT, true, -1);
+        verifyComputeTargetSdkVersion(PLATFORM_VERSION, PRE_RELEASE, true, false, -1);
+        verifyComputeTargetSdkVersion(PLATFORM_VERSION, PRE_RELEASE_WITH_FINGERPRINT, true, false,
+                -1
+        );
 
+        // Don't allow same pre-release targetSdkVersion on released platform when
+        // allowUnknownCodenames is true.
+        // APP: Pre-release API 20
+        // DEV: Released API 20
+        verifyComputeTargetSdkVersion(PLATFORM_VERSION, PRE_RELEASE, true, true,
+                -1);
+        verifyComputeTargetSdkVersion(PLATFORM_VERSION, PRE_RELEASE_WITH_FINGERPRINT, true, true,
+                -1);
 
         // Don't allow newer pre-release targetSdkVersion on released platform.
         // APP: Pre-release API 30
         // DEV: Released API 20
-        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE, true, -1);
-        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE_WITH_FINGERPRINT, true, -1);
+        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE, true, false, -1);
+        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE_WITH_FINGERPRINT, true,
+                false, -1
+        );
+        // Do allow newer pre-release targetSdkVersion on released platform when
+        // allowUnknownCodenames is true.
+        // APP: Pre-release API 30
+        // DEV: Released API 20
+        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE, true, true,
+                Build.VERSION_CODES.CUR_DEVELOPMENT);
+        verifyComputeTargetSdkVersion(NEWER_VERSION, NEWER_PRE_RELEASE_WITH_FINGERPRINT, true,
+                true, Build.VERSION_CODES.CUR_DEVELOPMENT);
     }
 
     /**
diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundHw2CompatTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundHw2CompatTest.java
index 6bdd88c..2d0755d 100644
--- a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundHw2CompatTest.java
+++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundHw2CompatTest.java
@@ -20,18 +20,15 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atMost;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -56,32 +53,20 @@
 import org.junit.runners.Parameterized;
 import org.mockito.ArgumentCaptor;
 
-import java.util.LinkedList;
-import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
 @RunWith(Parameterized.class)
 public class SoundHw2CompatTest {
-    @Parameterized.Parameter(0) public String mVersion;
-    @Parameterized.Parameter(1) public boolean mSupportConcurrentCapture;
+    @Parameterized.Parameter public String mVersion;
 
     private final Runnable mRebootRunnable = mock(Runnable.class);
     private ISoundTriggerHal mCanonical;
-    private CaptureStateNotifier mCaptureStateNotifier;
     private android.hardware.soundtrigger.V2_0.ISoundTriggerHw mHalDriver;
 
     // We run the test once for every version of the underlying driver.
-    @Parameterized.Parameters(name = "{0}, concurrent={1}")
-    public static Iterable<Object[]> data() {
-        List<Object[]> result = new LinkedList<>();
-
-        for (String version : new String[]{"V2_0", "V2_1", "V2_2", "V2_3",}) {
-            for (boolean concurrentCapture : new boolean[]{false, true}) {
-                result.add(new Object[]{version, concurrentCapture});
-            }
-        }
-
-        return result;
+    @Parameterized.Parameters
+    public static Object[] data() {
+        return new String[]{"V2_0", "V2_1", "V2_2", "V2_3"};
     }
 
     @Before
@@ -139,7 +124,7 @@
         when(mHalDriver.asBinder()).thenReturn(binder);
 
         android.hardware.soundtrigger.V2_3.Properties halProperties =
-                TestUtil.createDefaultProperties_2_3(mSupportConcurrentCapture);
+                TestUtil.createDefaultProperties_2_3();
         doAnswer(invocation -> {
             ((android.hardware.soundtrigger.V2_0.ISoundTriggerHw.getPropertiesCallback) invocation.getArgument(
                     0)).onValues(0, halProperties.base);
@@ -156,10 +141,7 @@
             }).when(driver).getProperties_2_3(any());
         }
 
-        mCaptureStateNotifier = spy(new CaptureStateNotifier());
-
-        mCanonical = SoundTriggerHw2Compat.create(mHalDriver, mRebootRunnable,
-                mCaptureStateNotifier);
+        mCanonical = SoundTriggerHw2Compat.create(mHalDriver, mRebootRunnable, null);
 
         // During initialization any method can be called, but after we're starting to enforce that
         // no additional methods are called.
@@ -171,7 +153,6 @@
         mCanonical.detach();
         verifyNoMoreInteractions(mHalDriver);
         verifyNoMoreInteractions(mRebootRunnable);
-        mCaptureStateNotifier.verifyNoMoreListeners();
     }
 
     @Test
@@ -194,12 +175,12 @@
             // It is OK for the SUT to cache the properties, so the underlying method doesn't
             // need to be called every single time.
             verify(driver, atMost(1)).getProperties_2_3(any());
-            TestUtil.validateDefaultProperties(properties, mSupportConcurrentCapture);
+            TestUtil.validateDefaultProperties(properties);
         } else {
             // It is OK for the SUT to cache the properties, so the underlying method doesn't
             // need to be called every single time.
             verify(mHalDriver, atMost(1)).getProperties(any());
-            TestUtil.validateDefaultProperties(properties, mSupportConcurrentCapture, 0, "");
+            TestUtil.validateDefaultProperties(properties, 0, "");
         }
     }
 
@@ -291,7 +272,7 @@
 
         ISoundTriggerHal.ModelCallback canonicalCallback = mock(
                 ISoundTriggerHal.ModelCallback.class);
-        final int maxModels = TestUtil.createDefaultProperties_2_0(false).maxSoundModels;
+        final int maxModels = TestUtil.createDefaultProperties_2_0().maxSoundModels;
         int[] modelHandles = new int[maxModels];
 
         // Load as many models as we're allowed.
@@ -318,7 +299,7 @@
         verify(globalCallback).onResourcesAvailable();
     }
 
-    private int loadPhraseModel_2_0(ISoundTriggerHal.ModelCallback canonicalCallback)
+    private void loadPhraseModel_2_0(ISoundTriggerHal.ModelCallback canonicalCallback)
             throws Exception {
         final int handle = 29;
         ArgumentCaptor<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel>
@@ -345,10 +326,9 @@
 
         TestUtil.validatePhraseSoundModel_2_0(modelCaptor.getValue());
         validateCallback_2_0(callbackCaptor.getValue(), canonicalCallback);
-        return handle;
     }
 
-    private int loadPhraseModel_2_1(ISoundTriggerHal.ModelCallback canonicalCallback)
+    private void loadPhraseModel_2_1(ISoundTriggerHal.ModelCallback canonicalCallback)
             throws Exception {
         final android.hardware.soundtrigger.V2_1.ISoundTriggerHw driver_2_1 =
                 (android.hardware.soundtrigger.V2_1.ISoundTriggerHw) mHalDriver;
@@ -380,14 +360,13 @@
 
         TestUtil.validatePhraseSoundModel_2_1(model.get());
         validateCallback_2_1(callbackCaptor.getValue(), canonicalCallback);
-        return handle;
     }
 
-    public int loadPhraseModel(ISoundTriggerHal.ModelCallback canonicalCallback) throws Exception {
+    public void loadPhraseModel(ISoundTriggerHal.ModelCallback canonicalCallback) throws Exception {
         if (mHalDriver instanceof android.hardware.soundtrigger.V2_1.ISoundTriggerHw) {
-            return loadPhraseModel_2_1(canonicalCallback);
+            loadPhraseModel_2_1(canonicalCallback);
         } else {
-            return loadPhraseModel_2_0(canonicalCallback);
+            loadPhraseModel_2_0(canonicalCallback);
         }
     }
 
@@ -484,80 +463,6 @@
     }
 
     @Test
-    public void testConcurrentCaptureAbort() throws Exception {
-        assumeFalse(mSupportConcurrentCapture);
-        verify(mCaptureStateNotifier, atLeast(1)).registerListener(any());
-
-        // Register global callback.
-        ISoundTriggerHal.GlobalCallback globalCallback = mock(
-                ISoundTriggerHal.GlobalCallback.class);
-        mCanonical.registerCallback(globalCallback);
-
-        // Load.
-        ISoundTriggerHal.ModelCallback canonicalCallback = mock(
-                ISoundTriggerHal.ModelCallback.class);
-        final int handle = loadGenericModel(canonicalCallback);
-
-        // Then start.
-        startRecognition(handle, canonicalCallback);
-
-        // Now activate external capture.
-        mCaptureStateNotifier.setState(true);
-
-        // Expect hardware to have been stopped.
-        verify(mHalDriver).stopRecognition(handle);
-
-        // Expect an abort event (async).
-        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
-                RecognitionEvent.class);
-        mCanonical.flushCallbacks();
-        verify(canonicalCallback).recognitionCallback(eq(handle), eventCaptor.capture());
-        assertEquals(RecognitionStatus.ABORTED, eventCaptor.getValue().status);
-
-        // Deactivate external capture.
-        mCaptureStateNotifier.setState(false);
-
-        // Expect a onResourcesAvailable().
-        mCanonical.flushCallbacks();
-        verify(globalCallback).onResourcesAvailable();
-    }
-
-    @Test
-    public void testConcurrentCaptureReject() throws Exception {
-        assumeFalse(mSupportConcurrentCapture);
-        verify(mCaptureStateNotifier, atLeast(1)).registerListener(any());
-
-        // Register global callback.
-        ISoundTriggerHal.GlobalCallback globalCallback = mock(
-                ISoundTriggerHal.GlobalCallback.class);
-        mCanonical.registerCallback(globalCallback);
-
-        // Load (this registers the callback).
-        ISoundTriggerHal.ModelCallback canonicalCallback = mock(
-                ISoundTriggerHal.ModelCallback.class);
-        final int handle = loadGenericModel(canonicalCallback);
-
-        // Report external capture active.
-        mCaptureStateNotifier.setState(true);
-
-        // Then start.
-        RecognitionConfig config = TestUtil.createRecognitionConfig();
-        try {
-            mCanonical.startRecognition(handle, 203, 204, config);
-            fail("Expected an exception");
-        } catch (RecoverableException e) {
-            assertEquals(Status.RESOURCE_CONTENTION, e.errorCode);
-        }
-
-        // Deactivate external capture.
-        mCaptureStateNotifier.setState(false);
-
-        // Expect a onResourcesAvailable().
-        mCanonical.flushCallbacks();
-        verify(globalCallback).onResourcesAvailable();
-    }
-
-    @Test
     public void testStopRecognition() throws Exception {
         mCanonical.stopRecognition(17);
         verify(mHalDriver).stopRecognition(17);
@@ -675,7 +580,7 @@
     }
 
     @Test
-    public void testGlobalCallback() throws Exception {
+    public void testGlobalCallback() {
         testGlobalCallback_2_0();
     }
 
@@ -803,29 +708,4 @@
         verifyNoMoreInteractions(canonicalCallback);
         clearInvocations(canonicalCallback);
     }
-
-    public static class CaptureStateNotifier implements ICaptureStateNotifier {
-        private final List<Listener> mListeners = new LinkedList<>();
-
-        @Override
-        public boolean registerListener(Listener listener) {
-            mListeners.add(listener);
-            return false;
-        }
-
-        @Override
-        public void unregisterListener(Listener listener) {
-            mListeners.remove(listener);
-        }
-
-        public void setState(boolean state) {
-            for (Listener listener : mListeners) {
-                listener.onCaptureStateChange(state);
-            }
-        }
-
-        public void verifyNoMoreListeners() {
-            assertEquals(0, mListeners.size());
-        }
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandlerTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandlerTest.java
new file mode 100644
index 0000000..6198925
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandlerTest.java
@@ -0,0 +1,313 @@
+/*
+ * 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.soundtrigger_middleware;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+import android.media.soundtrigger.RecognitionEvent;
+import android.media.soundtrigger.RecognitionStatus;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+
+@RunWith(JUnit4.class)
+public class SoundTriggerHalConcurrentCaptureHandlerTest {
+    private ISoundTriggerHal mUnderlying;
+    private CaptureStateNotifier mNotifier;
+    private ISoundTriggerHal.GlobalCallback mGlobalCallback;
+    private SoundTriggerHalConcurrentCaptureHandler mHandler;
+
+    @Before
+    public void setUp() {
+        mNotifier = new CaptureStateNotifier();
+        mUnderlying = mock(ISoundTriggerHal.class);
+        mGlobalCallback = mock(ISoundTriggerHal.GlobalCallback.class);
+        mHandler = new SoundTriggerHalConcurrentCaptureHandler(mUnderlying, mNotifier);
+        mHandler.registerCallback(mGlobalCallback);
+    }
+
+    @Test
+    public void testBasic() throws Exception {
+        ISoundTriggerHal.ModelCallback callback = mock(ISoundTriggerHal.ModelCallback.class);
+        int handle = mHandler.loadSoundModel(TestUtil.createGenericSoundModel(), callback);
+        verify(mUnderlying).loadSoundModel(any(), any());
+
+        mHandler.startRecognition(handle, 101, 102, TestUtil.createRecognitionConfig());
+        verify(mUnderlying).startRecognition(eq(handle), eq(101), eq(102), any());
+
+        mNotifier.setActive(true);
+        verify(mUnderlying).stopRecognition(handle);
+        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                RecognitionEvent.class);
+        Thread.sleep(50);
+        verify(callback).recognitionCallback(eq(handle), eventCaptor.capture());
+        RecognitionEvent event = eventCaptor.getValue();
+        assertEquals(event.status, RecognitionStatus.ABORTED);
+        assertFalse(event.recognitionStillActive);
+        verifyZeroInteractions(mGlobalCallback);
+        clearInvocations(callback, mUnderlying);
+
+        mNotifier.setActive(false);
+        Thread.sleep(50);
+        verify(mGlobalCallback).onResourcesAvailable();
+        verifyNoMoreInteractions(callback, mUnderlying);
+
+        mNotifier.setActive(true);
+        verifyNoMoreInteractions(callback, mUnderlying);
+    }
+
+    @Test
+    public void testStopBeforeActive() throws Exception {
+        ISoundTriggerHal.ModelCallback callback = mock(ISoundTriggerHal.ModelCallback.class);
+        int handle = mHandler.loadSoundModel(TestUtil.createGenericSoundModel(), callback);
+        verify(mUnderlying).loadSoundModel(any(), any());
+
+        mHandler.startRecognition(handle, 101, 102, TestUtil.createRecognitionConfig());
+        verify(mUnderlying).startRecognition(eq(handle), eq(101), eq(102), any());
+        mHandler.stopRecognition(handle);
+        verify(mUnderlying).stopRecognition(handle);
+        clearInvocations(mUnderlying);
+
+        mNotifier.setActive(true);
+        Thread.sleep(50);
+        verifyNoMoreInteractions(mUnderlying);
+        verifyNoMoreInteractions(callback);
+    }
+
+    @Test
+    public void testStopAfterActive() {
+        ISoundTriggerHal.ModelCallback callback = mock(ISoundTriggerHal.ModelCallback.class);
+        int handle = mHandler.loadSoundModel(TestUtil.createGenericSoundModel(), callback);
+        verify(mUnderlying).loadSoundModel(any(), any());
+
+        mHandler.startRecognition(handle, 101, 102, TestUtil.createRecognitionConfig());
+        verify(mUnderlying).startRecognition(eq(handle), eq(101), eq(102), any());
+
+        mNotifier.setActive(true);
+        verify(mUnderlying, times(1)).stopRecognition(handle);
+        mHandler.stopRecognition(handle);
+        verify(callback, times(1)).recognitionCallback(eq(handle), any());
+    }
+
+    @Test(timeout = 200)
+    public void testAbortWhileStop() {
+        ISoundTriggerHal.ModelCallback callback = mock(ISoundTriggerHal.ModelCallback.class);
+        int handle = mHandler.loadSoundModel(TestUtil.createGenericSoundModel(), callback);
+        ArgumentCaptor<ISoundTriggerHal.ModelCallback> modelCallbackCaptor =
+                ArgumentCaptor.forClass(ISoundTriggerHal.ModelCallback.class);
+        verify(mUnderlying).loadSoundModel(any(), modelCallbackCaptor.capture());
+        ISoundTriggerHal.ModelCallback modelCallback = modelCallbackCaptor.getValue();
+
+        mHandler.startRecognition(handle, 101, 102, TestUtil.createRecognitionConfig());
+        verify(mUnderlying).startRecognition(eq(handle), eq(101), eq(102), any());
+
+        doAnswer(invocation -> {
+            RecognitionEvent event = TestUtil.createRecognitionEvent(RecognitionStatus.ABORTED,
+                    false);
+            // Call the callback from a different thread to detect deadlocks by preventing recursive
+            // locking from working.
+            runOnSeparateThread(() -> modelCallback.recognitionCallback(handle, event));
+            return null;
+        }).when(mUnderlying).stopRecognition(handle);
+        mHandler.stopRecognition(handle);
+        verify(mUnderlying, times(1)).stopRecognition(handle);
+
+        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                RecognitionEvent.class);
+        verify(callback, atMost(1)).recognitionCallback(eq(handle), eventCaptor.capture());
+    }
+
+    @Test(timeout = 200)
+    public void testActiveWhileStop() {
+        ISoundTriggerHal.ModelCallback callback = mock(ISoundTriggerHal.ModelCallback.class);
+        int handle = mHandler.loadSoundModel(TestUtil.createGenericSoundModel(), callback);
+        ArgumentCaptor<ISoundTriggerHal.ModelCallback> modelCallbackCaptor =
+                ArgumentCaptor.forClass(ISoundTriggerHal.ModelCallback.class);
+        verify(mUnderlying).loadSoundModel(any(), modelCallbackCaptor.capture());
+        ISoundTriggerHal.ModelCallback modelCallback = modelCallbackCaptor.getValue();
+
+        mHandler.startRecognition(handle, 101, 102, TestUtil.createRecognitionConfig());
+        verify(mUnderlying).startRecognition(eq(handle), eq(101), eq(102), any());
+
+        doAnswer(invocation -> {
+            // The stop request causes a callback to be flushed.
+            RecognitionEvent event = TestUtil.createRecognitionEvent(RecognitionStatus.FORCED,
+                    true);
+            // Call the callback from a different thread to detect deadlocks by preventing recursive
+            // locking from working.
+            runOnSeparateThread(() -> modelCallback.recognitionCallback(handle, event));
+            // While the HAL is processing the stop request, capture state becomes active.
+            new Thread(() -> mNotifier.setActive(true)).start();
+            Thread.sleep(50);
+            return null;
+        }).when(mUnderlying).stopRecognition(handle);
+        mHandler.stopRecognition(handle);
+        // We only expect one underlying invocation of stop().
+        verify(mUnderlying, times(1)).stopRecognition(handle);
+
+        // The callback shouldn't be invoked in this case.
+        verify(callback, never()).recognitionCallback(eq(handle), any());
+    }
+
+    @Test(timeout = 200)
+    public void testStopWhileActive() {
+        ISoundTriggerHal.ModelCallback callback = mock(ISoundTriggerHal.ModelCallback.class);
+        int handle = mHandler.loadSoundModel(TestUtil.createGenericSoundModel(), callback);
+        ArgumentCaptor<ISoundTriggerHal.ModelCallback> modelCallbackCaptor =
+                ArgumentCaptor.forClass(ISoundTriggerHal.ModelCallback.class);
+        verify(mUnderlying).loadSoundModel(any(), modelCallbackCaptor.capture());
+        ISoundTriggerHal.ModelCallback modelCallback = modelCallbackCaptor.getValue();
+
+        mHandler.startRecognition(handle, 101, 102, TestUtil.createRecognitionConfig());
+        verify(mUnderlying).startRecognition(eq(handle), eq(101), eq(102), any());
+
+        doAnswer(invocation -> {
+            // The stop request causes a callback to be flushed.
+            RecognitionEvent event = TestUtil.createRecognitionEvent(RecognitionStatus.FORCED,
+                    true);
+            // Call the callback from a different thread to detect deadlocks by preventing recursive
+            // locking from working.
+            runOnSeparateThread(() -> modelCallback.recognitionCallback(handle, event));
+            // While the HAL is processing the stop request, client requests stop.
+            new Thread(() -> mHandler.stopRecognition(handle)).start();
+            Thread.sleep(50);
+            return null;
+        }).when(mUnderlying).stopRecognition(handle);
+        mNotifier.setActive(true);
+        // We only expect one underlying invocation of stop().
+        verify(mUnderlying, times(1)).stopRecognition(handle);
+        verify(callback, atMost(1)).recognitionCallback(eq(handle), any());
+    }
+
+    @Test(timeout = 200)
+    public void testEventWhileActive() throws Exception {
+        ISoundTriggerHal.ModelCallback callback = mock(ISoundTriggerHal.ModelCallback.class);
+        int handle = mHandler.loadSoundModel(TestUtil.createGenericSoundModel(), callback);
+        ArgumentCaptor<ISoundTriggerHal.ModelCallback> modelCallbackCaptor =
+                ArgumentCaptor.forClass(ISoundTriggerHal.ModelCallback.class);
+        verify(mUnderlying).loadSoundModel(any(), modelCallbackCaptor.capture());
+        ISoundTriggerHal.ModelCallback modelCallback = modelCallbackCaptor.getValue();
+
+        mHandler.startRecognition(handle, 101, 102, TestUtil.createRecognitionConfig());
+        verify(mUnderlying).startRecognition(eq(handle), eq(101), eq(102), any());
+
+        doAnswer(invocation -> {
+            RecognitionEvent event = TestUtil.createRecognitionEvent(RecognitionStatus.SUCCESS,
+                    false);
+            // Call the callback from a different thread to detect deadlocks by preventing recursive
+            // locking from working.
+            runOnSeparateThread(() -> modelCallback.recognitionCallback(handle, event));
+            return null;
+        }).when(mUnderlying).stopRecognition(handle);
+        mNotifier.setActive(true);
+        verify(mUnderlying, times(1)).stopRecognition(handle);
+        Thread.sleep(50);
+
+        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                RecognitionEvent.class);
+        verify(callback, atMost(2)).recognitionCallback(eq(handle), eventCaptor.capture());
+        RecognitionEvent lastEvent = eventCaptor.getValue();
+        assertEquals(lastEvent.status, RecognitionStatus.ABORTED);
+        assertFalse(lastEvent.recognitionStillActive);
+    }
+
+
+    @Test(timeout = 200)
+    public void testNonFinalEventWhileActive() throws Exception {
+        ISoundTriggerHal.ModelCallback callback = mock(ISoundTriggerHal.ModelCallback.class);
+        int handle = mHandler.loadSoundModel(TestUtil.createGenericSoundModel(), callback);
+        ArgumentCaptor<ISoundTriggerHal.ModelCallback> modelCallbackCaptor =
+                ArgumentCaptor.forClass(ISoundTriggerHal.ModelCallback.class);
+        verify(mUnderlying).loadSoundModel(any(), modelCallbackCaptor.capture());
+        ISoundTriggerHal.ModelCallback modelCallback = modelCallbackCaptor.getValue();
+
+        mHandler.startRecognition(handle, 101, 102, TestUtil.createRecognitionConfig());
+        verify(mUnderlying).startRecognition(eq(handle), eq(101), eq(102), any());
+
+        doAnswer(invocation -> {
+            RecognitionEvent event = TestUtil.createRecognitionEvent(RecognitionStatus.FORCED,
+                    true);
+            // Call the callback from a different thread to detect deadlocks by preventing recursive
+            // locking from working.
+            runOnSeparateThread(() -> modelCallback.recognitionCallback(handle, event));
+
+            return null;
+        }).when(mUnderlying).stopRecognition(handle);
+        mNotifier.setActive(true);
+        verify(mUnderlying, times(1)).stopRecognition(handle);
+
+        Thread.sleep(50);
+        ArgumentCaptor<RecognitionEvent> eventCaptor = ArgumentCaptor.forClass(
+                RecognitionEvent.class);
+        verify(callback, atMost(2)).recognitionCallback(eq(handle), eventCaptor.capture());
+        RecognitionEvent lastEvent = eventCaptor.getValue();
+        assertEquals(lastEvent.status, RecognitionStatus.ABORTED);
+        assertFalse(lastEvent.recognitionStillActive);
+    }
+
+    private static void runOnSeparateThread(Runnable runnable) {
+        Thread thread = new Thread(runnable);
+        thread.start();
+        try {
+            thread.join();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static class CaptureStateNotifier implements ICaptureStateNotifier {
+        boolean mActive = false;
+        Listener mListener;
+
+        @Override
+        public boolean registerListener(@NonNull Listener listener) {
+            mListener = listener;
+            return mActive;
+        }
+
+        @Override
+        public void unregisterListener(@NonNull Listener listener) {
+            mListener = null;
+        }
+
+        public void setActive(boolean active) {
+            mActive = active;
+            if (mListener != null) {
+                // Call the callback from a different thread to detect deadlocks by preventing
+                // recursive locking from working.
+                runOnSeparateThread(() -> mListener.onCaptureStateChange(mActive));
+            }
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java
index 0187e34..3bebc94 100644
--- a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareImplTest.java
@@ -134,7 +134,7 @@
     public void setUp() throws Exception {
         clearInvocations(mHalDriver);
         clearInvocations(mAudioSessionProvider);
-        when(mHalDriver.getProperties()).thenReturn(TestUtil.createDefaultProperties(false));
+        when(mHalDriver.getProperties()).thenReturn(TestUtil.createDefaultProperties());
         mService = new SoundTriggerMiddlewareImpl(() -> mHalDriver, mAudioSessionProvider);
     }
 
@@ -156,7 +156,7 @@
         assertEquals(1, allDescriptors.length);
 
         Properties properties = allDescriptors[0].properties;
-        assertEquals(TestUtil.createDefaultProperties(false), properties);
+        assertEquals(TestUtil.createDefaultProperties(), properties);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/TestUtil.java b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/TestUtil.java
index 30b4a59..39561f7 100644
--- a/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/TestUtil.java
+++ b/services/tests/servicestests/src/com/android/server/soundtrigger_middleware/TestUtil.java
@@ -162,8 +162,8 @@
                 phrases.get(0).recognitionModes);
     }
 
-    static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties createDefaultProperties_2_0(
-            boolean supportConcurrentCapture) {
+    static android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties
+            createDefaultProperties_2_0() {
         android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties properties =
                 new android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties();
         properties.implementor = "implementor";
@@ -185,17 +185,16 @@
                         | android.hardware.soundtrigger.V2_0.RecognitionMode.GENERIC_TRIGGER;
         properties.captureTransition = true;
         properties.maxBufferMs = 321;
-        properties.concurrentCapture = supportConcurrentCapture;
+        properties.concurrentCapture = true;
         properties.triggerInEvent = true;
         properties.powerConsumptionMw = 432;
         return properties;
     }
 
-    static android.hardware.soundtrigger.V2_3.Properties createDefaultProperties_2_3(
-            boolean supportConcurrentCapture) {
+    static android.hardware.soundtrigger.V2_3.Properties createDefaultProperties_2_3() {
         android.hardware.soundtrigger.V2_3.Properties properties =
                 new android.hardware.soundtrigger.V2_3.Properties();
-        properties.base = createDefaultProperties_2_0(supportConcurrentCapture);
+        properties.base = createDefaultProperties_2_0();
         properties.supportedModelArch = "supportedModelArch";
         properties.audioCapabilities =
                 android.hardware.soundtrigger.V2_3.AudioCapabilities.ECHO_CANCELLATION
@@ -203,7 +202,7 @@
         return properties;
     }
 
-    static Properties createDefaultProperties(boolean supportConcurrentCapture) {
+    static Properties createDefaultProperties() {
         Properties properties = new Properties();
         properties.implementor = "implementor";
         properties.description = "description";
@@ -217,7 +216,7 @@
                         | RecognitionMode.USER_AUTHENTICATION | RecognitionMode.GENERIC_TRIGGER;
         properties.captureTransition = true;
         properties.maxBufferMs = 321;
-        properties.concurrentCapture = supportConcurrentCapture;
+        properties.concurrentCapture = true;
         properties.triggerInEvent = true;
         properties.powerConsumptionMw = 432;
         properties.supportedModelArch = "supportedModelArch";
@@ -226,13 +225,13 @@
         return properties;
     }
 
-    static void validateDefaultProperties(Properties properties, boolean supportConcurrentCapture) {
-        validateDefaultProperties(properties, supportConcurrentCapture,
+    static void validateDefaultProperties(Properties properties) {
+        validateDefaultProperties(properties,
                 AudioCapabilities.ECHO_CANCELLATION | AudioCapabilities.NOISE_SUPPRESSION,
                 "supportedModelArch");
     }
 
-    static void validateDefaultProperties(Properties properties, boolean supportConcurrentCapture,
+    static void validateDefaultProperties(Properties properties,
             @AudioCapabilities int audioCapabilities, @NonNull String supportedModelArch) {
         assertEquals("implementor", properties.implementor);
         assertEquals("description", properties.description);
@@ -246,7 +245,7 @@
                 properties.recognitionModes);
         assertTrue(properties.captureTransition);
         assertEquals(321, properties.maxBufferMs);
-        assertEquals(supportConcurrentCapture, properties.concurrentCapture);
+        assertEquals(true, properties.concurrentCapture);
         assertTrue(properties.triggerInEvent);
         assertEquals(432, properties.powerConsumptionMw);
         assertEquals(supportedModelArch, properties.supportedModelArch);
diff --git a/tests/FlickerTests/OWNERS b/tests/FlickerTests/OWNERS
index c1221e3..d40ff56 100644
--- a/tests/FlickerTests/OWNERS
+++ b/tests/FlickerTests/OWNERS
@@ -1,4 +1,4 @@
-# Bug component: 909476
+# Bug component: 1157642
 include /services/core/java/com/android/server/wm/OWNERS
 natanieljr@google.com
 pablogamito@google.com
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt
index db2e645..e2e1ae8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt
@@ -114,7 +114,7 @@
         }
     }
 
-    @Presubmit
+    @FlakyTest(bugId = 228011606)
     @Test
     fun imeLayerIsVisibleAndAssociatedWithAppWidow() {
         testSpec.assertLayersStart {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
index 7e3ed82..4b268a8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest.kt
@@ -32,12 +32,15 @@
 import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.statusBarWindowIsVisible
 import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.common.WindowManagerConditionsFactory
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
+import org.junit.Assume
+import org.junit.Before
 
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -55,11 +58,16 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group4
 @Presubmit
-class SwitchImeWindowsFromGestureNavTest(private val testSpec: FlickerTestParameter) {
+open class SwitchImeWindowsFromGestureNavTest(private val testSpec: FlickerTestParameter) {
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val testApp = SimpleAppHelper(instrumentation)
     private val imeTestApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
 
+    @Before
+    open fun before() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
+    }
+
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTestShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
similarity index 76%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTestShellTransit.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
index 2252a94..edd52b7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTestShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2021 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.flicker.pip
+package com.android.server.wm.flicker.ime
 
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
@@ -24,22 +24,26 @@
 import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import org.junit.Assume
 import org.junit.Before
+
 import org.junit.FixMethodOrder
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.junit.runners.Parameterized
 
+/**
+ * Test IME windows switching with 2-Buttons or gestural navigation.
+ * To run this test: `atest FlickerTests:SwitchImeWindowsFromGestureNavTest`
+ */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group4
-class PipRotationTestShellTransit(testSpec: FlickerTestParameter) : PipRotationTest(testSpec) {
+@FlakyTest(bugId = 228012334)
+class SwitchImeWindowsFromGestureNavTest_ShellTransit(testSpec: FlickerTestParameter)
+    : SwitchImeWindowsFromGestureNavTest(testSpec) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
     }
-
-    @FlakyTest(bugId = 227214914)
-    override fun pipLayerRotates_StartingBounds() = super.pipLayerRotates_StartingBounds()
 }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
index ee0f3d8..6e33f66 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt
@@ -26,12 +26,9 @@
 import com.android.server.wm.flicker.LAUNCHER_COMPONENT
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.helpers.reopenAppFromOverview
 import com.android.server.wm.flicker.helpers.setRotation
 import com.android.server.wm.traces.common.WindowManagerConditionsFactory
-import org.junit.Assume.assumeFalse
-import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -64,10 +61,6 @@
 @Group1
 open class OpenAppFromOverviewTest(testSpec: FlickerTestParameter)
     : OpenAppFromLauncherTransition(testSpec) {
-    @Before
-    open fun before() {
-        assumeFalse(isShellTransitionsEnabled)
-    }
 
     /**
      * Defines the transition used to run the test
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest_ShellTransit.kt
deleted file mode 100644
index 55e1e9b..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest_ShellTransit.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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.server.wm.flicker.launch
-
-import androidx.test.filters.FlakyTest
-import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group1
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test launching an app from the recents app view (the overview)
- *
- * To run this test: `atest FlickerTests:OpenAppFromOverviewTest`
- *
- * Actions:
- *     Launch [testApp]
- *     Press recents
- *     Relaunch an app [testApp] by selecting it in the overview screen, and wait animation to
- *     complete (only this action is traced)
- *
- * Notes:
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [OpenAppTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group1
-class OpenAppFromOverviewTest_ShellTransit(testSpec: FlickerTestParameter)
-    : OpenAppFromOverviewTest(testSpec) {
-    @Before
-    override fun before() {
-        assumeTrue(isShellTransitionsEnabled)
-    }
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 216266712)
-    @Test
-    override fun appWindowBecomesTopWindow() = super.appWindowBecomesTopWindow()
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 216266712)
-    @Test
-    override fun appWindowReplacesLauncherAsTopWindow() =
-            super.appWindowReplacesLauncherAsTopWindow()
-
-    /** {@inheritDoc} */
-    @FlakyTest(bugId = 218470989)
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
index fbd611a..f357177 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
@@ -27,10 +27,7 @@
 import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.traces.common.FlickerComponentName
-import org.junit.Assume
-import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -64,11 +61,6 @@
     override val testApp = NonResizeableAppHelper(instrumentation)
     private val colorFadComponent = FlickerComponentName("", "ColorFade BLAST#")
 
-    @Before
-    open fun before() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-    }
-
     /**
      * Checks that the nav bar layer starts invisible, becomes visible during unlocking animation
      * and remains visible at the end
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
index 4313b8d..02c1a10 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppTransition.kt
@@ -17,7 +17,6 @@
 package com.android.server.wm.flicker.launch
 
 import android.app.Instrumentation
-import androidx.test.filters.FlakyTest
 import android.platform.test.annotations.Presubmit
 import androidx.test.platform.app.InstrumentationRegistry
 import com.android.server.wm.flicker.FlickerBuilderProvider
@@ -216,7 +215,7 @@
      * Checks that [testApp] window is not on top at the start of the transition, and then becomes
      * the top visible window until the end of the transition.
      */
-    @FlakyTest(bugId = 203538234)
+    @Presubmit
     @Test
     open fun appWindowBecomesTopWindow() {
         testSpec.assertWm {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
index 1eb3d8d..c89e6a4 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt
@@ -33,12 +33,15 @@
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
 import com.android.server.wm.flicker.helpers.WindowUtils
+import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
 import com.android.server.wm.flicker.navBarWindowIsVisible
 import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarWindowIsVisible
 import com.android.server.wm.traces.common.FlickerComponentName
+import org.junit.Assume
+import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -70,6 +73,11 @@
 
     private val startDisplayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
 
+    @Before
+    open fun before() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
+    }
+
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
similarity index 64%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest_ShellTransit.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
index 8a08d07..b9fef08 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.server.wm.flicker.launch
+package com.android.server.wm.flicker.quickswitch
 
-import androidx.test.filters.FlakyTest
 import android.platform.test.annotations.RequiresDevice
+import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.annotation.Group1
@@ -30,30 +30,24 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test launching an app while the device is locked
+ * Test quick switching back to previous app from last opened app
  *
- * To run this test: `atest FlickerTests:OpenAppNonResizeableTest`
+ * To run this test: `atest FlickerTests:QuickSwitchBetweenTwoAppsBackTest`
  *
  * Actions:
- *     Lock the device.
- *     Launch an app on top of the lock screen [testApp] and wait animation to complete
+ *     Launch an app [testApp1]
+ *     Launch another app [testApp2]
+ *     Swipe right from the bottom of the screen to quick switch back to the first app [testApp1]
  *
- * Notes:
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [OpenAppTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group1
-@FlakyTest(bugId = 219688533)
-class OpenAppNonResizeableTest_ShellTransit(testSpec: FlickerTestParameter)
-    : OpenAppNonResizeableTest(testSpec) {
+@FlakyTest(bugId = 228009808)
+open class QuickSwitchBetweenTwoAppsBackTest_ShellTransit(testSpec: FlickerTestParameter)
+    : QuickSwitchBetweenTwoAppsBackTest(testSpec) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
index 5474a42..725d2c3 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt
@@ -32,6 +32,7 @@
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.navBarLayerIsVisible
 import com.android.server.wm.flicker.navBarLayerRotatesAndScales
 import com.android.server.wm.flicker.navBarWindowIsVisible
@@ -39,6 +40,8 @@
 import com.android.server.wm.flicker.statusBarWindowIsVisible
 import com.android.server.wm.traces.common.FlickerComponentName
 import com.android.server.wm.traces.common.Rect
+import org.junit.Assume
+import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -68,6 +71,11 @@
     private val testApp1 = SimpleAppHelper(instrumentation)
     private val testApp2 = NonResizeableAppHelper(instrumentation)
 
+    @Before
+    open fun before() {
+        Assume.assumeFalse(isShellTransitionsEnabled)
+    }
+
     @FlickerBuilderProvider
     fun buildFlicker(): FlickerBuilder {
         return FlickerBuilder(instrumentation).apply {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
similarity index 62%
copy from tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest_ShellTransit.kt
copy to tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
index 8a08d07..9c9dedc2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.server.wm.flicker.launch
+package com.android.server.wm.flicker.quickswitch
 
-import androidx.test.filters.FlakyTest
 import android.platform.test.annotations.RequiresDevice
 import com.android.server.wm.flicker.FlickerParametersRunnerFactory
 import com.android.server.wm.flicker.FlickerTestParameter
@@ -30,30 +29,23 @@
 import org.junit.runners.Parameterized
 
 /**
- * Test launching an app while the device is locked
+ * Test quick switching back to previous app from last opened app
  *
- * To run this test: `atest FlickerTests:OpenAppNonResizeableTest`
+ * To run this test: `atest FlickerTests:QuickSwitchBetweenTwoAppsForwardTest`
  *
  * Actions:
- *     Lock the device.
- *     Launch an app on top of the lock screen [testApp] and wait animation to complete
- *
- * Notes:
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [OpenAppTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
+ *     Launch an app [testApp1]
+ *     Launch another app [testApp2]
+ *     Swipe right from the bottom of the screen to quick switch back to the first app [testApp1]
+ *     Swipe left from the bottom of the screen to quick switch forward to the second app [testApp2]
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @Group1
-@FlakyTest(bugId = 219688533)
-class OpenAppNonResizeableTest_ShellTransit(testSpec: FlickerTestParameter)
-    : OpenAppNonResizeableTest(testSpec) {
+open class QuickSwitchBetweenTwoAppsForwardTest_ShellTransit(testSpec: FlickerTestParameter)
+    : QuickSwitchBetweenTwoAppsForwardTest(testSpec) {
     @Before
     override fun before() {
         Assume.assumeTrue(isShellTransitionsEnabled)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
index f83ae87..cc4a4b2 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
@@ -28,7 +28,7 @@
 import com.android.server.wm.flicker.FlickerTestParameter
 import com.android.server.wm.flicker.FlickerTestParameterFactory
 import com.android.server.wm.flicker.LAUNCHER_COMPONENT
-import com.android.server.wm.flicker.annotation.Group4
+import com.android.server.wm.flicker.annotation.Group1
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.entireScreenCovered
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
@@ -60,7 +60,7 @@
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group4
+@Group1
 class QuickSwitchFromLauncherTest(private val testSpec: FlickerTestParameter) {
     private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
     private val taplInstrumentation = LauncherInstrumentation()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
index 2b944c6..8b851e5 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
@@ -25,13 +25,11 @@
 import com.android.server.wm.flicker.annotation.Group3
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.rules.WMFlickerServiceRuleForTestSpec
 import com.android.server.wm.flicker.statusBarLayerIsVisible
 import com.android.server.wm.flicker.statusBarLayerRotatesScales
 import com.android.server.wm.flicker.statusBarWindowIsVisible
 import com.android.server.wm.traces.common.FlickerComponentName
-import org.junit.Assume
 import org.junit.FixMethodOrder
 import org.junit.Rule
 import org.junit.Test
@@ -100,7 +98,7 @@
      * Windows maybe recreated when rotated. Checks that the focus does not change or if it does,
      * focus returns to [testApp]
      */
-    @FlakyTest(bugId = 190185577)
+    @Presubmit
     @Test
     fun focusChanges() {
         testSpec.assertEventLog {
@@ -130,18 +128,6 @@
     @Presubmit
     @Test
     fun rotationLayerAppearsAndVanishes() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-        rotationLayerAppearsAndVanishesAssertion()
-    }
-
-    /**
-     * Checks that the [FlickerComponentName.ROTATION] layer appears during the transition,
-     * doesn't flicker, and disappears before the transition is complete
-     */
-    @FlakyTest(bugId = 218484127)
-    @Test
-    fun rotationLayerAppearsAndVanishes_shellTransit() {
-        Assume.assumeTrue(isShellTransitionsEnabled)
         rotationLayerAppearsAndVanishesAssertion()
     }
 
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
index 15fd5e1..fac5baf 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
@@ -26,11 +26,8 @@
 import com.android.server.wm.flicker.annotation.Group3
 import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.android.server.wm.flicker.helpers.SeamlessRotationAppHelper
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
 import com.android.server.wm.flicker.testapp.ActivityOptions
 import com.android.server.wm.traces.common.FlickerComponentName
-import org.junit.Assume
-import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -84,11 +81,6 @@
 ) : RotationTransition(testSpec) {
     override val testApp = SeamlessRotationAppHelper(instrumentation)
 
-    @Before
-    open fun before() {
-        Assume.assumeFalse(isShellTransitionsEnabled)
-    }
-
     override val transition: FlickerBuilder.() -> Unit
         get() = {
             super.transition(this)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest_ShellTransit.kt
deleted file mode 100644
index d397d59..0000000
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest_ShellTransit.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.server.wm.flicker.rotation
-
-import androidx.test.filters.FlakyTest
-import android.platform.test.annotations.RequiresDevice
-import com.android.server.wm.flicker.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.FlickerTestParameter
-import com.android.server.wm.flicker.annotation.Group3
-import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
-import org.junit.Assume
-import org.junit.Before
-import org.junit.FixMethodOrder
-import org.junit.runner.RunWith
-import org.junit.runners.MethodSorters
-import org.junit.runners.Parameterized
-
-/**
- * Test opening an app and cycling through app rotations using seamless rotations
- *
- * Currently runs:
- *      0 -> 90 degrees
- *      0 -> 90 degrees (with starved UI thread)
- *      90 -> 0 degrees
- *      90 -> 0 degrees (with starved UI thread)
- *
- * Actions:
- *     Launch an app in fullscreen and supporting seamless rotation (via intent)
- *     Set initial device orientation
- *     Start tracing
- *     Change device orientation
- *     Stop tracing
- *
- * To run this test: `atest FlickerTests:SeamlessAppRotationTest`
- *
- * To run only the presubmit assertions add: `--
- *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
- *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Presubmit`
- *
- * To run only the postsubmit assertions add: `--
- *      --module-arg FlickerTests:exclude-annotation:androidx.test.filters.FlakyTest
- *      --module-arg FlickerTests:include-annotation:android.platform.test.annotations.Postsubmit`
- *
- * To run only the flaky assertions add: `--
- *      --module-arg FlickerTests:include-annotation:androidx.test.filters.FlakyTest`
- *
- * Notes:
- *     1. Some default assertions (e.g., nav bar, status bar and screen covered)
- *        are inherited [RotationTransition]
- *     2. Part of the test setup occurs automatically via
- *        [com.android.server.wm.flicker.TransitionRunnerWithRules],
- *        including configuring navigation mode, initial orientation and ensuring no
- *        apps are running before setup
- */
-@RequiresDevice
-@RunWith(Parameterized::class)
-@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Group3
-@FlakyTest(bugId = 219689723)
-class SeamlessAppRotationTest_ShellTransit(
-    testSpec: FlickerTestParameter
-) : SeamlessAppRotationTest(testSpec) {
-    @Before
-    override fun before() {
-        Assume.assumeTrue(isShellTransitionsEnabled)
-    }
-}