Merge "Override equals & hash for VirtualCameraStreamConfig" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 900c902..903248f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -159,6 +159,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -301,6 +302,8 @@
     private final ConnectivityController mConnectivityController;
     /** Need directly for sending uid state changes */
     private final DeviceIdleJobsController mDeviceIdleJobsController;
+    /** Need directly for sending exempted bucket changes */
+    private final FlexibilityController mFlexibilityController;
     /** Needed to get next estimated launch time. */
     private final PrefetchController mPrefetchController;
     /** Needed to get remaining quota time. */
@@ -513,6 +516,10 @@
                     if (name == null) {
                         continue;
                     }
+                    if (DEBUG) {
+                        Slog.d(TAG, "DeviceConfig " + name
+                                + " changed to " + properties.getString(name, null));
+                    }
                     switch (name) {
                         case Constants.KEY_ENABLE_API_QUOTAS:
                         case Constants.KEY_ENABLE_EXECUTION_SAFEGUARDS_UDC:
@@ -2532,17 +2539,17 @@
         mControllers = new ArrayList<StateController>();
         mPrefetchController = new PrefetchController(this);
         mControllers.add(mPrefetchController);
-        final FlexibilityController flexibilityController =
+        mFlexibilityController =
                 new FlexibilityController(this, mPrefetchController);
-        mControllers.add(flexibilityController);
+        mControllers.add(mFlexibilityController);
         mConnectivityController =
-                new ConnectivityController(this, flexibilityController);
+                new ConnectivityController(this, mFlexibilityController);
         mControllers.add(mConnectivityController);
         mControllers.add(new TimeController(this));
-        final IdleController idleController = new IdleController(this, flexibilityController);
+        final IdleController idleController = new IdleController(this, mFlexibilityController);
         mControllers.add(idleController);
         final BatteryController batteryController =
-                new BatteryController(this, flexibilityController);
+                new BatteryController(this, mFlexibilityController);
         mControllers.add(batteryController);
         mStorageController = new StorageController(this);
         mControllers.add(mStorageController);
@@ -3191,6 +3198,13 @@
     }
 
     @Override
+    public void onExemptedBucketChanged(@NonNull ArraySet<JobStatus> changedJobs) {
+        if (changedJobs.size() > 0) {
+            mFlexibilityController.onExemptedBucketChanged(changedJobs);
+        }
+    }
+
+    @Override
     public void onRestrictionStateChanged(@NonNull JobRestriction restriction,
             boolean stopOvertimeJobs) {
         mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
@@ -3497,7 +3511,10 @@
                 }
 
                 final boolean shouldForceBatchJob;
-                if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) {
+                if (job.overrideState > JobStatus.OVERRIDE_NONE) {
+                    // The job should run for some test. Don't force batch it.
+                    shouldForceBatchJob = false;
+                } else if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiatedJob()) {
                     // Never batch expedited or user-initiated jobs, even for RESTRICTED apps.
                     shouldForceBatchJob = false;
                 } else if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) {
@@ -4950,6 +4967,8 @@
         Slog.d(TAG, "executeRunCommand(): " + pkgName + "/" + namespace + "/" + userId
                 + " " + jobId + " s=" + satisfied + " f=" + force);
 
+        final CountDownLatch delayLatch = new CountDownLatch(1);
+        final JobStatus js;
         try {
             final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0,
                     userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM);
@@ -4958,7 +4977,7 @@
             }
 
             synchronized (mLock) {
-                final JobStatus js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
+                js = mJobs.getJobByUidAndJobId(uid, namespace, jobId);
                 if (js == null) {
                     return JobSchedulerShellCommand.CMD_ERR_NO_JOB;
                 }
@@ -4969,23 +4988,71 @@
                 // Re-evaluate constraints after the override is set in case one of the overridden
                 // constraints was preventing another constraint from thinking it needed to update.
                 for (int c = mControllers.size() - 1; c >= 0; --c) {
-                    mControllers.get(c).reevaluateStateLocked(uid);
+                    mControllers.get(c).evaluateStateLocked(js);
                 }
 
                 if (!js.isConstraintsSatisfied()) {
-                    js.overrideState = JobStatus.OVERRIDE_NONE;
-                    return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS;
+                    if (js.hasConnectivityConstraint()
+                            && !js.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)
+                            && js.wouldBeReadyWithConstraint(JobStatus.CONSTRAINT_CONNECTIVITY)) {
+                        // Because of how asynchronous the connectivity signals are, JobScheduler
+                        // may not get the connectivity satisfaction signal immediately. In this
+                        // case, wait a few seconds to see if it comes in before saying the
+                        // connectivity constraint isn't satisfied.
+                        mHandler.postDelayed(
+                                checkConstraintRunnableForTesting(
+                                        mHandler, js, delayLatch, 5, 1000),
+                                1000);
+                    } else {
+                        // There's no asynchronous signal to wait for. We can immediately say the
+                        // job's constraints aren't satisfied and return.
+                        js.overrideState = JobStatus.OVERRIDE_NONE;
+                        return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS;
+                    }
+                } else {
+                    delayLatch.countDown();
                 }
-
-                queueReadyJobsForExecutionLocked();
-                maybeRunPendingJobsLocked();
             }
         } catch (RemoteException e) {
             // can't happen
+            return 0;
+        }
+
+        // Choose to block the return until we're sure about the state of the connectivity job
+        // so that tests can expect a reliable state after calling the run command.
+        try {
+            delayLatch.await(7L, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Slog.e(TAG, "Couldn't wait for asynchronous constraint change", e);
+        }
+
+        synchronized (mLock) {
+            if (!js.isConstraintsSatisfied()) {
+                js.overrideState = JobStatus.OVERRIDE_NONE;
+                return JobSchedulerShellCommand.CMD_ERR_CONSTRAINTS;
+            }
+
+            queueReadyJobsForExecutionLocked();
+            maybeRunPendingJobsLocked();
         }
         return 0;
     }
 
+    private static Runnable checkConstraintRunnableForTesting(@NonNull final Handler handler,
+            @NonNull final JobStatus js, @NonNull final CountDownLatch latch,
+            final int remainingAttempts, final long delayMs) {
+        return () -> {
+            if (remainingAttempts <= 0 || js.isConstraintsSatisfied()) {
+                latch.countDown();
+                return;
+            }
+            handler.postDelayed(
+                    checkConstraintRunnableForTesting(
+                            handler, js, latch, remainingAttempts - 1, delayMs),
+                    delayMs);
+        };
+    }
+
     // Shell command infrastructure: immediately timeout currently executing jobs
     int executeStopCommand(PrintWriter pw, String pkgName, int userId,
             @Nullable String namespace, boolean hasJobId, int jobId,
diff --git a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
index 50064bd..411a24d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/StateChangedListener.java
@@ -62,6 +62,12 @@
 
     /**
      * Called when these jobs are added or removed from the
+     * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_EXEMPTED} bucket.
+     */
+    void onExemptedBucketChanged(@NonNull ArraySet<JobStatus> jobs);
+
+    /**
+     * Called when these jobs are added or removed from the
      * {@link android.app.usage.UsageStatsManager#STANDBY_BUCKET_RESTRICTED} bucket.
      */
     void onRestrictedBucketChanged(@NonNull List<JobStatus> jobs);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 0e67b9a..e4cb569 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -20,6 +20,7 @@
 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
 
+import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
 import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
 import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
@@ -180,8 +181,12 @@
                 }
             };
 
-    private static final int MSG_UPDATE_JOBS = 0;
-    private static final int MSG_UPDATE_JOB = 1;
+    private static final int MSG_CHECK_ALL_JOBS = 0;
+    /** Check the jobs in {@link #mJobsToCheck} */
+    private static final int MSG_CHECK_JOBS = 1;
+
+    @GuardedBy("mLock")
+    private final ArraySet<JobStatus> mJobsToCheck = new ArraySet<>();
 
     public FlexibilityController(
             JobSchedulerService service, PrefetchController prefetchController) {
@@ -266,7 +271,14 @@
     @GuardedBy("mLock")
     boolean isFlexibilitySatisfiedLocked(JobStatus js) {
         return !mFlexibilityEnabled
+                // Exclude all jobs of the TOP app
                 || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP
+                // Only exclude DEFAULT+ priority jobs for BFGS+ apps
+                || (mService.getUidBias(js.getSourceUid()) >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+                        && js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT)
+                // Only exclude DEFAULT+ priority jobs for EXEMPTED apps
+                || (js.getStandbyBucket() == EXEMPTED_INDEX
+                        && js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT)
                 || hasEnoughSatisfiedConstraintsLocked(js)
                 || mService.isCurrentlyRunningLocked(js);
     }
@@ -371,11 +383,19 @@
 
                 // Push the job update to the handler to avoid blocking other controllers and
                 // potentially batch back-to-back controller state updates together.
-                mHandler.obtainMessage(MSG_UPDATE_JOBS).sendToTarget();
+                mHandler.obtainMessage(MSG_CHECK_ALL_JOBS).sendToTarget();
             }
         }
     }
 
+    /** Called with a set of apps who have been added to or removed from the exempted bucket. */
+    public void onExemptedBucketChanged(@NonNull ArraySet<JobStatus> changedJobs) {
+        synchronized (mLock) {
+            mJobsToCheck.addAll(changedJobs);
+            mHandler.sendEmptyMessage(MSG_CHECK_JOBS);
+        }
+    }
+
     /** Checks if the given constraint is satisfied in the flexibility controller. */
     @VisibleForTesting
     boolean isConstraintSatisfied(int constraint) {
@@ -485,7 +505,9 @@
     @Override
     @GuardedBy("mLock")
     public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) {
-        if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) {
+        if (prevBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+                && newBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) {
+            // All changes are below BFGS. There's no significant change to care about.
             return;
         }
         final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -710,7 +732,8 @@
                     }
                     mFlexibilityTracker.setNumDroppedFlexibleConstraints(js,
                             js.getNumAppliedFlexibleConstraints());
-                    mHandler.obtainMessage(MSG_UPDATE_JOB, js).sendToTarget();
+                    mJobsToCheck.add(js);
+                    mHandler.sendEmptyMessage(MSG_CHECK_JOBS);
                     return;
                 }
                 if (nextTimeElapsed == NO_LIFECYCLE_END) {
@@ -761,10 +784,11 @@
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
-                case MSG_UPDATE_JOBS:
-                    removeMessages(MSG_UPDATE_JOBS);
+                case MSG_CHECK_ALL_JOBS:
+                    removeMessages(MSG_CHECK_ALL_JOBS);
 
                     synchronized (mLock) {
+                        mJobsToCheck.clear();
                         final long nowElapsed = sElapsedRealtimeClock.millis();
                         final ArraySet<JobStatus> changedJobs = new ArraySet<>();
 
@@ -790,19 +814,25 @@
                     }
                     break;
 
-                case MSG_UPDATE_JOB:
+                case MSG_CHECK_JOBS:
                     synchronized (mLock) {
-                        final JobStatus js = (JobStatus) msg.obj;
-                        if (DEBUG) {
-                            Slog.d("blah", "Checking on " + js.toShortString());
-                        }
                         final long nowElapsed = sElapsedRealtimeClock.millis();
-                        if (js.setFlexibilityConstraintSatisfied(
-                                nowElapsed, isFlexibilitySatisfiedLocked(js))) {
-                            // TODO(141645789): add method that will take a single job
-                            ArraySet<JobStatus> changedJob = new ArraySet<>();
-                            changedJob.add(js);
-                            mStateChangedListener.onControllerStateChanged(changedJob);
+                        ArraySet<JobStatus> changedJobs = new ArraySet<>();
+
+                        for (int i = mJobsToCheck.size() - 1; i >= 0; --i) {
+                            final JobStatus js = mJobsToCheck.valueAt(i);
+                            if (DEBUG) {
+                                Slog.d(TAG, "Checking on " + js.toShortString());
+                            }
+                            if (js.setFlexibilityConstraintSatisfied(
+                                    nowElapsed, isFlexibilitySatisfiedLocked(js))) {
+                                changedJobs.add(js);
+                            }
+                        }
+
+                        mJobsToCheck.clear();
+                        if (changedJobs.size() > 0) {
+                            mStateChangedListener.onControllerStateChanged(changedJobs);
                         }
                     }
                     break;
@@ -985,7 +1015,10 @@
             pw.println(":");
             pw.increaseIndent();
 
-            pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS).println();
+            pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS);
+            pw.print("(");
+            JobStatus.dumpConstraints(pw, APPLIED_CONSTRAINTS);
+            pw.println(")");
             pw.print(KEY_DEADLINE_PROXIMITY_LIMIT, DEADLINE_PROXIMITY_LIMIT_MS).println();
             pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE, FALLBACK_FLEXIBILITY_DEADLINE_MS).println();
             pw.print(KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS,
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 0d5d11e..a095a16 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -106,11 +106,8 @@
     public static final long NO_LATEST_RUNTIME = Long.MAX_VALUE;
     public static final long NO_EARLIEST_RUNTIME = 0L;
 
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 < 0
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE;  // 1 << 2
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public static final int CONSTRAINT_BATTERY_NOT_LOW =
             JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@@ -2194,7 +2191,7 @@
      * @return Whether or not this job would be ready to run if it had the specified constraint
      * granted, based on its requirements.
      */
-    boolean wouldBeReadyWithConstraint(int constraint) {
+    public boolean wouldBeReadyWithConstraint(int constraint) {
         return readinessStatusWithConstraint(constraint, true);
     }
 
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 8ddbf69..04da781 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
@@ -2511,6 +2511,7 @@
                     + " to bucketIndex " + bucketIndex);
         }
         List<JobStatus> restrictedChanges = new ArrayList<>();
+        ArraySet<JobStatus> exemptedChanges = new ArraySet<>();
         synchronized (mLock) {
             ShrinkableDebits debits = mEJStats.get(userId, packageName);
             if (debits != null) {
@@ -2530,6 +2531,10 @@
                         && bucketIndex != js.getStandbyBucket()) {
                     restrictedChanges.add(js);
                 }
+                if ((bucketIndex == EXEMPTED_INDEX || js.getStandbyBucket() == EXEMPTED_INDEX)
+                        && bucketIndex != js.getStandbyBucket()) {
+                    exemptedChanges.add(js);
+                }
                 js.setStandbyBucket(bucketIndex);
             }
             Timer timer = mPkgTimers.get(userId, packageName);
@@ -2544,6 +2549,9 @@
                     maybeUpdateConstraintForPkgLocked(
                             sElapsedRealtimeClock.millis(), userId, packageName));
         }
+        if (exemptedChanges.size() > 0) {
+            mStateChangedListener.onExemptedBucketChanged(exemptedChanges);
+        }
         if (restrictedChanges.size() > 0) {
             mStateChangedListener.onRestrictedBucketChanged(restrictedChanges);
         }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 69a6e6d..c6f920f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -518,6 +518,8 @@
         <permission name="android.permission.ACCESS_BROADCAST_RADIO"/>
         <!-- Permission required for CTS test - CtsAmbientContextServiceTestCases -->
         <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/>
+        <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases -->
+        <permission name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE"/>
         <!-- Permission required for CTS test - CtsTelephonyProviderTestCases -->
         <permission name="android.permission.WRITE_APN_SETTINGS"/>
         <!-- Permission required for GTS test - GtsStatsdHostTestCases -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index 5b0239f..02af2d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -563,7 +563,7 @@
                         ? p.x - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR
                         : p.x + mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
                 animationForChild(child)
-                        .translationX(fromX, p.y)
+                        .translationX(fromX, p.x)
                         .start();
             } else {
                 float fromY = p.y - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR;
@@ -634,4 +634,9 @@
                     .start();
         }
     }
+
+    /** Returns true if we're in the middle of a collapse or expand animation. */
+    boolean isAnimating() {
+        return mAnimatingCollapse || mAnimatingExpand;
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
index c1ff260..60f1d271 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -16,52 +16,51 @@
 
 package com.android.wm.shell.bubbles.animation;
 
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.annotation.SuppressLint;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Insets;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.testing.AndroidTestingRunner;
 import android.view.View;
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 
 import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleStackView;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
 @SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
 public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestCase {
 
-    private int mDisplayWidth = 500;
-    private int mDisplayHeight = 1000;
-
-    private Runnable mOnBubbleAnimatedOutAction = mock(Runnable.class);
+    private final Semaphore mBubbleRemovedSemaphore = new Semaphore(0);
+    private final Runnable mOnBubbleAnimatedOutAction = mBubbleRemovedSemaphore::release;
     ExpandedAnimationController mExpandedController;
 
     private int mStackOffset;
     private PointF mExpansionPoint;
     private BubblePositioner mPositioner;
-    private BubbleStackView.StackViewState mStackViewState = new BubbleStackView.StackViewState();
+    private final BubbleStackView.StackViewState mStackViewState =
+            new BubbleStackView.StackViewState();
 
-    @SuppressLint("VisibleForTests")
     @Before
     public void setUp() throws Exception {
         super.setUp();
@@ -70,15 +69,13 @@
                 getContext().getSystemService(WindowManager.class));
         mPositioner.updateInternal(Configuration.ORIENTATION_PORTRAIT,
                 Insets.of(0, 0, 0, 0),
-                new Rect(0, 0, mDisplayWidth, mDisplayHeight));
+                new Rect(0, 0, 500, 1000));
 
         BubbleStackView stackView = mock(BubbleStackView.class);
-        when(stackView.getState()).thenReturn(getStackViewState());
 
         mExpandedController = new ExpandedAnimationController(mPositioner,
                 mOnBubbleAnimatedOutAction,
                 stackView);
-        spyOn(mExpandedController);
 
         addOneMoreThanBubbleLimitBubbles();
         mLayout.setActiveController(mExpandedController);
@@ -86,9 +83,18 @@
         Resources res = mLayout.getResources();
         mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
         mExpansionPoint = new PointF(100, 100);
+
+        getStackViewState();
+        when(stackView.getState()).thenAnswer(i -> getStackViewState());
+        waitForMainThread();
     }
 
-    public BubbleStackView.StackViewState getStackViewState() {
+    @After
+    public void tearDown() {
+        waitForMainThread();
+    }
+
+    private BubbleStackView.StackViewState getStackViewState() {
         mStackViewState.numberOfBubbles = mLayout.getChildCount();
         mStackViewState.selectedIndex = 0;
         mStackViewState.onLeft = mPositioner.isStackOnLeft(mExpansionPoint);
@@ -96,68 +102,71 @@
     }
 
     @Test
-    @Ignore
-    public void testExpansionAndCollapse() throws InterruptedException {
-        Runnable afterExpand = mock(Runnable.class);
-        mExpandedController.expandFromStack(afterExpand);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
+    public void testExpansionAndCollapse() throws Exception {
+        expand();
         testBubblesInCorrectExpandedPositions();
-        verify(afterExpand).run();
+        waitForMainThread();
 
-        Runnable afterCollapse = mock(Runnable.class);
+        final Semaphore semaphore = new Semaphore(0);
+        Runnable afterCollapse = semaphore::release;
         mExpandedController.collapseBackToStack(mExpansionPoint, false, afterCollapse);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
-        testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1);
-        verify(afterExpand).run();
+        assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue();
+        waitForAnimation();
+        testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y);
     }
 
     @Test
-    @Ignore
-    public void testOnChildAdded() throws InterruptedException {
+    public void testOnChildAdded() throws Exception {
         expand();
+        waitForMainThread();
 
         // Add another new view and wait for its animation.
         final View newView = new FrameLayout(getContext());
         mLayout.addView(newView, 0);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
 
+        waitForAnimation();
         testBubblesInCorrectExpandedPositions();
     }
 
     @Test
-    @Ignore
-    public void testOnChildRemoved() throws InterruptedException {
+    public void testOnChildRemoved() throws Exception {
         expand();
+        waitForMainThread();
 
-        // Remove some views and see if the remaining child views still pass the expansion test.
+        // Remove some views and verify the remaining child views still pass the expansion test.
         mLayout.removeView(mViews.get(0));
         mLayout.removeView(mViews.get(3));
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+
+        // Removing a view will invoke onBubbleAnimatedOutAction. Block until it gets called twice.
+        assertThat(mBubbleRemovedSemaphore.tryAcquire(2, 2, TimeUnit.SECONDS)).isTrue();
+
+        waitForAnimation();
         testBubblesInCorrectExpandedPositions();
     }
 
     @Test
-    public void testDragBubbleOutDoesntNPE() throws InterruptedException {
+    public void testDragBubbleOutDoesntNPE() {
         mExpandedController.onGestureFinished();
         mExpandedController.dragBubbleOut(mViews.get(0), 1, 1);
     }
 
     /** Expand the stack and wait for animations to finish. */
     private void expand() throws InterruptedException {
-        mExpandedController.expandFromStack(mock(Runnable.class));
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+        final Semaphore semaphore = new Semaphore(0);
+        Runnable afterExpand = semaphore::release;
+
+        mExpandedController.expandFromStack(afterExpand);
+        assertThat(semaphore.tryAcquire(1, TimeUnit.SECONDS)).isTrue();
     }
 
     /** Check that children are in the correct positions for being stacked. */
-    private void testStackedAtPosition(float x, float y, int offsetMultiplier) {
+    private void testStackedAtPosition(float x, float y) {
         // Make sure the rest of the stack moved again, including the first bubble not moving, and
         // is stacked to the right now that we're on the right side of the screen.
         for (int i = 0; i < mLayout.getChildCount(); i++) {
-            assertEquals(x + i * offsetMultiplier * mStackOffset,
-                    mLayout.getChildAt(i).getTranslationX(), 2f);
-            assertEquals(y, mLayout.getChildAt(i).getTranslationY(), 2f);
+            assertEquals(x, mLayout.getChildAt(i).getTranslationX(), 2f);
+            assertEquals(y + Math.min(i, 1) * mStackOffset, mLayout.getChildAt(i).getTranslationY(),
+                    2f);
             assertEquals(1f, mLayout.getChildAt(i).getAlpha(), .01f);
         }
     }
@@ -175,4 +184,22 @@
                     mLayout.getChildAt(i).getTranslationY(), 2f);
         }
     }
+
+    private void waitForAnimation() throws Exception {
+        final Semaphore semaphore = new Semaphore(0);
+        boolean[] animating = new boolean[]{ true };
+        for (int i = 0; i < 4; i++) {
+            if (animating[0]) {
+                mMainThreadHandler.post(() -> {
+                    if (!mExpandedController.isAnimating()) {
+                        animating[0] = false;
+                        semaphore.release();
+                    }
+                });
+                Thread.sleep(500);
+            }
+        }
+        assertThat(semaphore.tryAcquire(1, 2, TimeUnit.SECONDS)).isTrue();
+        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
+    }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
index 48ae296..2ed5add 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTestCase.java
@@ -164,11 +164,17 @@
 
         @Override
         public void cancelAllAnimations() {
+            if (mLayout.getChildCount() == 0) {
+                return;
+            }
             mMainThreadHandler.post(super::cancelAllAnimations);
         }
 
         @Override
         public void cancelAnimationsOnView(View view) {
+            if (mLayout.getChildCount() == 0) {
+                return;
+            }
             mMainThreadHandler.post(() -> super.cancelAnimationsOnView(view));
         }
 
@@ -221,6 +227,9 @@
 
             @Override
             protected void startPathAnimation() {
+                if (mLayout.getChildCount() == 0) {
+                    return;
+                }
                 mMainThreadHandler.post(super::startPathAnimation);
             }
         }
@@ -322,4 +331,9 @@
             e.printStackTrace();
         }
     }
+
+    /** Waits for the main thread to finish processing all pending runnables. */
+    public void waitForMainThread() {
+        runOnMainThreadAndBlock(() -> {});
+    }
 }
diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java
index 5a72b0b..1a3d7b7 100644
--- a/media/java/android/media/AudioDeviceInfo.java
+++ b/media/java/android/media/AudioDeviceInfo.java
@@ -23,6 +23,8 @@
 import android.annotation.TestApi;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.List;
@@ -402,7 +404,9 @@
 
     private final AudioDevicePort mPort;
 
-    AudioDeviceInfo(AudioDevicePort port) {
+    /** @hide */
+    @VisibleForTesting
+    public AudioDeviceInfo(AudioDevicePort port) {
        mPort = port;
     }
 
diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java
index 73bc6f9..2de8eef 100644
--- a/media/java/android/media/AudioDevicePort.java
+++ b/media/java/android/media/AudioDevicePort.java
@@ -20,6 +20,8 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.os.Build;
 
+import com.android.aconfig.annotations.VisibleForTesting;
+
 import java.util.Arrays;
 import java.util.List;
 
@@ -38,6 +40,26 @@
 
 public class AudioDevicePort extends AudioPort {
 
+    /** @hide */
+    // TODO: b/316864909 - Remove this method once there's a way to fake audio device ports further
+    // down the stack.
+    @VisibleForTesting
+    public static AudioDevicePort createForTesting(
+            int type, @NonNull String name, @NonNull String address) {
+        return new AudioDevicePort(
+                new AudioHandle(/* id= */ 0),
+                name,
+                /* samplingRates= */ null,
+                /* channelMasks= */ null,
+                /* channelIndexMasks= */ null,
+                /* formats= */ null,
+                /* gains= */ null,
+                type,
+                address,
+                /* encapsulationModes= */ null,
+                /* encapsulationMetadataTypes= */ null);
+    }
+
     private final int mType;
     private final String mAddress;
     private final int[] mEncapsulationModes;
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 6e65c16..477c42e 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -688,6 +688,9 @@
     <!-- Permission required for CTS test - CtsAmbientContextDetectionServiceDeviceTest -->
     <uses-permission android:name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" />
 
+    <!-- Permission required for CTS test - CtsWearableSensingServiceTestCases -->
+    <uses-permission android:name="android.permission.MANAGE_WEARABLE_SENSING_SERVICE" />
+
     <!-- Permission required for CTS test - CallAudioInterceptionTest -->
     <uses-permission android:name="android.permission.CALL_AUDIO_INTERCEPTION" />
 
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index ab4fe76..eaa6d07 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -241,10 +241,16 @@
 }
 
 flag {
+   name: "switch_user_on_bg"
+   namespace: "systemui"
+   description: "Does user switching on a background thread"
+   bug: "284095720"
+}
+
+flag {
     name: "status_bar_static_inout_indicators"
     namespace: "systemui"
     description: "(Upstream request) Always show the network activity inout indicators and "
         "prefer using alpha to distinguish network activity."
     bug: "310715220"
 }
-
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
index 0ab596c..1f8854f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt
@@ -616,4 +616,28 @@
                 .onDozeAmountChanged(eq(.3f), eq(.3f), eq(ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN))
             job.cancel()
         }
+
+    @Test
+    fun bouncerToAod_dozeAmountChanged() =
+        testScope.runTest {
+            // GIVEN view is attached
+            mController.onViewAttached()
+            Mockito.reset(mView)
+
+            val job = mController.listenForPrimaryBouncerToAodTransitions(this)
+            // WHEN alternate bouncer to aod transition in progress
+            transitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.PRIMARY_BOUNCER,
+                    to = KeyguardState.AOD,
+                    value = .3f,
+                    transitionState = TransitionState.RUNNING
+                )
+            )
+            runCurrent()
+
+            // THEN doze amount is updated to
+            verify(mView).onDozeAmountChanged(eq(.3f), eq(.3f), eq(ANIMATE_APPEAR_ON_SCREEN_OFF))
+            job.cancel()
+        }
 }
diff --git a/packages/SystemUI/res/drawable/ic_memory.xml b/packages/SystemUI/res/drawable/ic_memory.xml
deleted file mode 100644
index ada36c5..0000000
--- a/packages/SystemUI/res/drawable/ic_memory.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-Copyright (C) 2018 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:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
-    <path
-        android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
-        android:fillAlpha="0.5"
-        android:fillColor="#000000"/>
-    <path
-        android:pathData="M6,9 L6,7 L4,7 L4,5 L6,5 C6,3.9 6.9,3 8,3 L16,3 C17.1,3 18,3.9 18,5 L20,5 L20,7 L18,7 L18,9 L20,9 L20,11 L18,11 L18,13 L20,13 L20,15 L18,15 L18,17 L20,17 L20,19 L18,19 C18,20.1 17.1,21 16,21 L8,21 C6.9,21 6,20.1 6,19 L4,19 L4,17 L6,17 L6,15 L4,15 L4,13 L6,13 L6,11 L4,11 L4,9 L6,9 Z M16,19 L16,5 L8,5 L8,19 L16,19 Z"
-        android:fillColor="#000000"/>
-</vector>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 90cc1fb..e01a2aa 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -480,9 +480,6 @@
      This name is in the ComponentName flattened format (package/class)  -->
     <string name="config_remoteCopyPackage" translatable="false"></string>
 
-    <!-- On debuggable builds, alert the user if SystemUI PSS goes over this number (in kb) -->
-    <integer name="watch_heap_limit">256000</integer>
-
     <!-- SystemUI Plugins that can be loaded on user builds. -->
     <string-array name="config_pluginAllowlist" translatable="false">
         <item>com.android.systemui</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e10925d..f4b25a7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2397,10 +2397,6 @@
     <!-- URl of the webpage that explains battery saver. -->
     <string name="help_uri_battery_saver_learn_more_link_target" translatable="false"></string>
 
-    <!-- Name for a quick settings tile, used only by platform developers, to extract the SystemUI process memory and send it to another
-         app for debugging. Will not be seen by users. [CHAR LIMIT=20] -->
-    <string name="heap_dump_tile_name">Dump SysUI Heap</string>
-
     <!-- Title for the privacy indicators dialog, only appears as part of a11y descriptions [CHAR LIMIT=NONE] -->
     <string name="ongoing_privacy_dialog_a11y_title">In use</string>
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index cdd7b80..74b975c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -39,7 +39,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.systemui.Dumpable;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
@@ -48,9 +47,7 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
 import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl;
-import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder;
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.core.LogLevel;
 import com.android.systemui.log.dagger.KeyguardClockLog;
@@ -62,17 +59,11 @@
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
-import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.settings.SecureSettings;
@@ -102,14 +93,7 @@
     private final DumpManager mDumpManager;
     private final ClockEventController mClockEventController;
     private final LogBuffer mLogBuffer;
-    private final NotificationIconContainerAlwaysOnDisplayViewModel mAodIconsViewModel;
-    private final KeyguardRootViewModel mKeyguardRootViewModel;
-    private final ConfigurationState mConfigurationState;
-    private final SystemBarUtilsState mSystemBarUtilsState;
-    private final DozeParameters mDozeParameters;
-    private final ScreenOffAnimationController mScreenOffAnimationController;
-    private final AlwaysOnDisplayNotificationIconViewStore mAodIconViewStore;
-    private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker;
+    private final NotificationIconContainerAlwaysOnDisplayViewBinder mNicViewBinder;
     private FrameLayout mSmallClockFrame; // top aligned clock
     private FrameLayout mLargeClockFrame; // centered clock
 
@@ -183,9 +167,7 @@
             KeyguardSliceViewController keyguardSliceViewController,
             NotificationIconAreaController notificationIconAreaController,
             LockscreenSmartspaceController smartspaceController,
-            SystemBarUtilsState systemBarUtilsState,
-            ScreenOffAnimationController screenOffAnimationController,
-            StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker,
+            NotificationIconContainerAlwaysOnDisplayViewBinder nicViewBinder,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             SecureSettings secureSettings,
             @Main DelayableExecutor uiExecutor,
@@ -193,11 +175,6 @@
             DumpManager dumpManager,
             ClockEventController clockEventController,
             @KeyguardClockLog LogBuffer logBuffer,
-            NotificationIconContainerAlwaysOnDisplayViewModel aodIconsViewModel,
-            KeyguardRootViewModel keyguardRootViewModel,
-            ConfigurationState configurationState,
-            DozeParameters dozeParameters,
-            AlwaysOnDisplayNotificationIconViewStore aodIconViewStore,
             KeyguardInteractor keyguardInteractor,
             KeyguardClockInteractor keyguardClockInteractor,
             FeatureFlagsClassic featureFlags,
@@ -208,9 +185,7 @@
         mKeyguardSliceViewController = keyguardSliceViewController;
         mNotificationIconAreaController = notificationIconAreaController;
         mSmartspaceController = smartspaceController;
-        mSystemBarUtilsState = systemBarUtilsState;
-        mScreenOffAnimationController = screenOffAnimationController;
-        mIconViewBindingFailureTracker = iconViewBindingFailureTracker;
+        mNicViewBinder = nicViewBinder;
         mSecureSettings = secureSettings;
         mUiExecutor = uiExecutor;
         mBgExecutor = bgExecutor;
@@ -218,11 +193,6 @@
         mDumpManager = dumpManager;
         mClockEventController = clockEventController;
         mLogBuffer = logBuffer;
-        mAodIconsViewModel = aodIconsViewModel;
-        mKeyguardRootViewModel = keyguardRootViewModel;
-        mConfigurationState = configurationState;
-        mDozeParameters = dozeParameters;
-        mAodIconViewStore = aodIconViewStore;
         mView.setLogBuffer(mLogBuffer);
         mFeatureFlags = featureFlags;
         mKeyguardInteractor = keyguardInteractor;
@@ -619,28 +589,7 @@
                     mAodIconsBindHandle.dispose();
                 }
                 if (nic != null) {
-                    final DisposableHandle viewHandle =
-                            NotificationIconContainerViewBinder.bindWhileAttached(
-                                    nic,
-                                    mAodIconsViewModel,
-                                    mConfigurationState,
-                                    mSystemBarUtilsState,
-                                    mIconViewBindingFailureTracker,
-                                    mAodIconViewStore);
-                    final DisposableHandle visHandle = KeyguardRootViewBinder.bindAodIconVisibility(
-                            nic,
-                            mKeyguardRootViewModel.isNotifIconContainerVisible(),
-                            mConfigurationState,
-                            mFeatureFlags,
-                            mScreenOffAnimationController);
-                    if (visHandle == null) {
-                        mAodIconsBindHandle = viewHandle;
-                    } else {
-                        mAodIconsBindHandle = () -> {
-                            viewHandle.dispose();
-                            visHandle.dispose();
-                        };
-                    }
+                    mAodIconsBindHandle = mNicViewBinder.bindWhileAttached(nic);
                     mAodIconContainer = nic;
                 }
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
index 63fe26a..109741c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt
@@ -199,11 +199,27 @@
                 listenForAodToOccludedTransitions(this)
                 listenForAlternateBouncerToAodTransitions(this)
                 listenForDreamingToAodTransitions(this)
+                listenForPrimaryBouncerToAodTransitions(this)
             }
         }
     }
 
     @VisibleForTesting
+    suspend fun listenForPrimaryBouncerToAodTransitions(scope: CoroutineScope): Job {
+        return scope.launch {
+            transitionInteractor
+                .transition(KeyguardState.PRIMARY_BOUNCER, KeyguardState.AOD)
+                .collect { transitionStep ->
+                    view.onDozeAmountChanged(
+                        transitionStep.value,
+                        transitionStep.value,
+                        ANIMATE_APPEAR_ON_SCREEN_OFF,
+                    )
+                }
+        }
+    }
+
+    @VisibleForTesting
     suspend fun listenForDreamingToAodTransitions(scope: CoroutineScope): Job {
         return scope.launch {
             transitionInteractor.transition(KeyguardState.DREAMING, KeyguardState.AOD).collect {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index b43f54d..5b9509d 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -483,9 +483,6 @@
     // TODO(b/264916608): Tracking Bug
     @JvmField val SCREENSHOT_METADATA = unreleasedFlag("screenshot_metadata")
 
-    // TODO(b/266955521): Tracking bug
-    @JvmField val SCREENSHOT_DETECTION = releasedFlag("screenshot_detection")
-
     // TODO(b/251205791): Tracking Bug
     @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips")
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
index de15fd6..c98f637 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
 import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
 import com.android.systemui.res.R
 import javax.inject.Inject
@@ -48,7 +49,9 @@
 ) {
 
     init {
-        alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG)
+        if (!DeviceEntryUdfpsRefactor.isEnabled) {
+            alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, TAG)
+        }
     }
 
     private val showIndicatorForPrimaryBouncer: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 08e2a8f..362e7e6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -29,7 +29,6 @@
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
 import com.android.internal.jank.InteractionJankMonitor
@@ -67,6 +66,7 @@
 import javax.inject.Provider
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -205,7 +205,6 @@
                                     childViews[aodNotificationIconContainerId]
                                         ?.setAodNotifIconContainerIsVisible(
                                             isVisible,
-                                            featureFlags,
                                             iconsAppearTranslationPx.value,
                                             screenOffAnimationController,
                                         )
@@ -359,37 +358,29 @@
         }
     }
 
-    @JvmStatic
-    fun bindAodIconVisibility(
+    suspend fun bindAodNotifIconVisibility(
         view: View,
         isVisible: Flow<AnimatedValue<Boolean>>,
         configuration: ConfigurationState,
-        featureFlags: FeatureFlagsClassic,
         screenOffAnimationController: ScreenOffAnimationController,
-    ): DisposableHandle? {
+    ) {
         KeyguardShadeMigrationNssl.assertInLegacyMode()
-        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return null
-        return view.repeatWhenAttached {
-            lifecycleScope.launch {
-                val iconAppearTranslationPx =
-                    configuration
-                        .getDimensionPixelSize(R.dimen.shelf_appear_translation)
-                        .stateIn(this)
-                isVisible.collect { isVisible ->
-                    view.setAodNotifIconContainerIsVisible(
-                        isVisible,
-                        featureFlags,
-                        iconAppearTranslationPx.value,
-                        screenOffAnimationController,
-                    )
-                }
+        if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return
+        coroutineScope {
+            val iconAppearTranslationPx =
+                configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this)
+            isVisible.collect { isVisible ->
+                view.setAodNotifIconContainerIsVisible(
+                    isVisible = isVisible,
+                    iconsAppearTranslationPx = iconAppearTranslationPx.value,
+                    screenOffAnimationController = screenOffAnimationController,
+                )
             }
         }
     }
 
     private fun View.setAodNotifIconContainerIsVisible(
         isVisible: AnimatedValue<Boolean>,
-        featureFlags: FeatureFlagsClassic,
         iconsAppearTranslationPx: Int,
         screenOffAnimationController: ScreenOffAnimationController,
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
index 1ab64b7..ba3357c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java
@@ -17,12 +17,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.res.Resources;
-import android.os.Build;
 import android.provider.Settings;
 
-import com.android.systemui.res.R;
 import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.util.leak.GarbageMonitor;
+import com.android.systemui.res.R;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -44,10 +42,6 @@
         final String defaultTileList = res.getString(R.string.quick_settings_tiles_default);
 
         tiles.addAll(Arrays.asList(defaultTileList.split(",")));
-        if (Build.IS_DEBUGGABLE
-                && GarbageMonitor.ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS) {
-            tiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
-        }
         return tiles;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 2af7ae0..47b0624 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -23,7 +23,6 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
-import android.os.Build;
 import android.provider.Settings;
 import android.service.quicksettings.Tile;
 import android.service.quicksettings.TileService;
@@ -33,7 +32,6 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.systemui.res.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.qs.QSTile;
@@ -42,8 +40,8 @@
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
-import com.android.systemui.util.leak.GarbageMonitor;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -114,9 +112,6 @@
                 possibleTiles.add(spec);
             }
         }
-        if (Build.IS_DEBUGGABLE && !current.contains(GarbageMonitor.MemoryTile.TILE_SPEC)) {
-            possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
-        }
 
         final ArrayList<QSTile> tilesToAdd = new ArrayList<>();
         possibleTiles.remove("cell");
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index 17e6375..bdcbac0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -14,7 +14,6 @@
 
 package com.android.systemui.qs.tileimpl;
 
-import android.os.Build;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
@@ -25,15 +24,14 @@
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.external.CustomTile;
-import com.android.systemui.util.leak.GarbageMonitor;
-
-import dagger.Lazy;
 
 import java.util.Map;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
 
+import dagger.Lazy;
+
 /**
  * A factory that creates Quick Settings tiles based on a tileSpec
  *
@@ -79,9 +77,7 @@
     @Nullable
     protected QSTileImpl createTileInternal(String tileSpec) {
         // Stock tiles.
-        if (mTileMap.containsKey(tileSpec)
-                // We should not return a Garbage Monitory Tile if the build is not Debuggable
-                && (!tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC) || Build.IS_DEBUGGABLE)) {
+        if (mTileMap.containsKey(tileSpec)) {
             return mTileMap.get(tileSpec).get();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
index 5cbea90..7130fa1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/MessageContainerController.kt
@@ -11,8 +11,6 @@
 import android.view.animation.AccelerateDecelerateInterpolator
 import androidx.constraintlayout.widget.Guideline
 import com.android.systemui.res.R
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import javax.inject.Inject
 
 /**
@@ -23,7 +21,6 @@
 constructor(
     private val workProfileMessageController: WorkProfileMessageController,
     private val screenshotDetectionController: ScreenshotDetectionController,
-    private val featureFlags: FeatureFlags,
 ) {
     private lateinit var container: ViewGroup
     private lateinit var guideline: Guideline
@@ -63,10 +60,8 @@
 
     fun onScreenshotTaken(screenshot: ScreenshotData) {
         val workProfileData = workProfileMessageController.onScreenshotTaken(screenshot.userHandle)
-        var notifiedApps: List<CharSequence> = listOf()
-        if (featureFlags.isEnabled(Flags.SCREENSHOT_DETECTION)) {
-            notifiedApps = screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
-        }
+        var notifiedApps: List<CharSequence> =
+            screenshotDetectionController.maybeNotifyOfScreenshot(screenshot)
 
         // If work profile first run needs to show, bias towards that, otherwise show screenshot
         // detection notification if needed.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt
new file mode 100644
index 0000000..d7c29f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerAlwaysOnDisplayViewBinder.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.statusbar.notification.icon.ui.viewbinder
+
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.launch
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerAlwaysOnDisplayViewModel]. */
+class NotificationIconContainerAlwaysOnDisplayViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerAlwaysOnDisplayViewModel,
+    private val keyguardRootViewModel: KeyguardRootViewModel,
+    private val configuration: ConfigurationState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val screenOffAnimationController: ScreenOffAnimationController,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val viewStore: AlwaysOnDisplayNotificationIconViewStore,
+) {
+    fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle {
+        return view.repeatWhenAttached {
+            lifecycleScope.launch {
+                launch {
+                    NotificationIconContainerViewBinder.bind(
+                        view = view,
+                        viewModel = viewModel,
+                        configuration = configuration,
+                        systemBarUtilsState = systemBarUtilsState,
+                        failureTracker = failureTracker,
+                        viewStore = viewStore,
+                    )
+                }
+                launch {
+                    KeyguardRootViewBinder.bindAodNotifIconVisibility(
+                        view = view,
+                        isVisible = keyguardRootViewModel.isNotifIconContainerVisible,
+                        configuration = configuration,
+                        screenOffAnimationController = screenOffAnimationController,
+                    )
+                }
+            }
+        }
+    }
+}
+
+/** [IconViewStore] for the always-on display. */
+class AlwaysOnDisplayNotificationIconViewStore
+@Inject
+constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
new file mode 100644
index 0000000..783488af
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerShelfViewBinder.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.statusbar.notification.icon.ui.viewbinder
+
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.bindIcons
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerShelfViewModel]. */
+class NotificationIconContainerShelfViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerShelfViewModel,
+    private val configuration: ConfigurationState,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val viewStore: ShelfNotificationIconViewStore,
+) {
+    suspend fun bind(view: NotificationIconContainer) {
+        viewModel.icons.bindIcons(
+            view,
+            configuration,
+            systemBarUtilsState,
+            notifyBindingFailures = { failureTracker.shelfFailures = it },
+            viewStore,
+        )
+    }
+}
+
+/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
+class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
new file mode 100644
index 0000000..8e089b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerStatusBarViewBinder.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.statusbar.notification.icon.ui.viewbinder
+
+import androidx.lifecycle.lifecycleScope
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.notification.collection.NotifCollection
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
+import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import javax.inject.Inject
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.launch
+
+/** Binds a [NotificationIconContainer] to a [NotificationIconContainerStatusBarViewModel]. */
+class NotificationIconContainerStatusBarViewBinder
+@Inject
+constructor(
+    private val viewModel: NotificationIconContainerStatusBarViewModel,
+    private val configuration: ConfigurationState,
+    private val systemBarUtilsState: SystemBarUtilsState,
+    private val failureTracker: StatusBarIconViewBindingFailureTracker,
+    private val viewStore: StatusBarNotificationIconViewStore,
+) {
+    fun bindWhileAttached(view: NotificationIconContainer): DisposableHandle {
+        return view.repeatWhenAttached {
+            lifecycleScope.launch {
+                NotificationIconContainerViewBinder.bind(
+                    view = view,
+                    viewModel = viewModel,
+                    configuration = configuration,
+                    systemBarUtilsState = systemBarUtilsState,
+                    failureTracker = failureTracker,
+                    viewStore = viewStore,
+                )
+            }
+        }
+    }
+}
+
+/** [IconViewStore] for the status bar. */
+class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
+    IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon })
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index e1e30e1..8fe0022 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder.IconViewStore
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconColors
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData
 import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData.LimitType
@@ -45,7 +44,6 @@
 import com.android.systemui.util.ui.isAnimating
 import com.android.systemui.util.ui.stopAnimating
 import com.android.systemui.util.ui.value
-import javax.inject.Inject
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.coroutineScope
@@ -56,42 +54,6 @@
 
 /** Binds a view-model to a [NotificationIconContainer]. */
 object NotificationIconContainerViewBinder {
-    @JvmStatic
-    fun bindWhileAttached(
-        view: NotificationIconContainer,
-        viewModel: NotificationIconContainerShelfViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
-        failureTracker: StatusBarIconViewBindingFailureTracker,
-        viewStore: IconViewStore,
-    ): DisposableHandle {
-        return view.repeatWhenAttached {
-            lifecycleScope.launch {
-                viewModel.icons.bindIcons(
-                    view,
-                    configuration,
-                    systemBarUtilsState,
-                    notifyBindingFailures = { failureTracker.shelfFailures = it },
-                    viewStore,
-                )
-            }
-        }
-    }
-
-    @JvmStatic
-    fun bindWhileAttached(
-        view: NotificationIconContainer,
-        viewModel: NotificationIconContainerStatusBarViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
-        failureTracker: StatusBarIconViewBindingFailureTracker,
-        viewStore: IconViewStore,
-    ): DisposableHandle =
-        view.repeatWhenAttached {
-            lifecycleScope.launch {
-                bind(view, viewModel, configuration, systemBarUtilsState, failureTracker, viewStore)
-            }
-        }
 
     suspend fun bind(
         view: NotificationIconContainer,
@@ -215,7 +177,7 @@
      * given `iconKey`. The parent [Job] of this coroutine will be cancelled automatically when the
      * view is to be unbound.
      */
-    private suspend fun Flow<NotificationIconsViewData>.bindIcons(
+    suspend fun Flow<NotificationIconsViewData>.bindIcons(
         view: NotificationIconContainer,
         configuration: ConfigurationState,
         systemBarUtilsState: SystemBarUtilsState,
@@ -377,24 +339,14 @@
     }
 
     @ColorInt private const val DEFAULT_AOD_ICON_COLOR = Color.WHITE
-    private const val TAG =  "NotifIconContainerViewBinder"
+    private const val TAG = "NotifIconContainerViewBinder"
 }
 
-/** [IconViewStore] for the [com.android.systemui.statusbar.NotificationShelf] */
-class ShelfNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.shelfIcon })
-
-/** [IconViewStore] for the always-on display. */
-class AlwaysOnDisplayNotificationIconViewStore
-@Inject
-constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.aodIcon })
-
-/** [IconViewStore] for the status bar. */
-class StatusBarNotificationIconViewStore @Inject constructor(notifCollection: NotifCollection) :
-    IconViewStore by (notifCollection.iconViewStoreBy { it.statusBarIcon })
-
-private fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) =
+/**
+ * Convenience builder for [IconViewStore] that uses [block] to extract the relevant
+ * [StatusBarIconView] from an [IconPack] stored inside of the [NotifCollection].
+ */
+fun NotifCollection.iconViewStoreBy(block: (IconPack) -> StatusBarIconView?) =
     IconViewStore { key ->
         getEntry(key)?.icons?.let(block)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
index 699e140..5ab4d4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewbinder/NotificationShelfViewBinder.kt
@@ -16,60 +16,38 @@
 
 package com.android.systemui.statusbar.notification.shelf.ui.viewbinder
 
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.common.ui.ConfigurationState
-import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.statusbar.NotificationShelf
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.row.ui.viewbinder.ActivatableNotificationViewBinder
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
 import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
-import com.android.systemui.statusbar.ui.SystemBarUtilsState
 import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
 /** Binds a [NotificationShelf] to its [view model][NotificationShelfViewModel]. */
 object NotificationShelfViewBinder {
-    fun bind(
+    suspend fun bind(
         shelf: NotificationShelf,
         viewModel: NotificationShelfViewModel,
-        configuration: ConfigurationState,
-        systemBarUtilsState: SystemBarUtilsState,
         falsingManager: FalsingManager,
-        iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
+        nicBinder: NotificationIconContainerShelfViewBinder,
         notificationIconAreaController: NotificationIconAreaController,
-        shelfIconViewStore: ShelfNotificationIconViewStore,
-    ) {
+    ): Unit = coroutineScope {
         ActivatableNotificationViewBinder.bind(viewModel, shelf, falsingManager)
         shelf.apply {
             if (NotificationIconContainerRefactor.isEnabled) {
-                NotificationIconContainerViewBinder.bindWhileAttached(
-                    shelfIcons,
-                    viewModel.icons,
-                    configuration,
-                    systemBarUtilsState,
-                    iconViewBindingFailureTracker,
-                    shelfIconViewStore,
-                )
+                launch { nicBinder.bind(shelfIcons) }
             } else {
                 notificationIconAreaController.setShelfIcons(shelfIcons)
             }
-            repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.STARTED) {
-                    launch {
-                        viewModel.canModifyColorOfNotifications.collect(
-                            ::setCanModifyColorOfNotifications
-                        )
-                    }
-                    launch { viewModel.isClickable.collect(::setCanInteract) }
-                    registerViewListenersWhileAttached(shelf, viewModel)
-                }
+            launch {
+                viewModel.canModifyColorOfNotifications.collect(::setCanModifyColorOfNotifications)
             }
+            launch { viewModel.isClickable.collect(::setCanInteract) }
+            registerViewListenersWhileAttached(shelf, viewModel)
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
index 64b5b62c..5ca8b53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModel.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.NotificationShelf
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModel
 import com.android.systemui.statusbar.notification.shelf.domain.interactor.NotificationShelfInteractor
 import javax.inject.Inject
@@ -32,7 +31,6 @@
 constructor(
     private val interactor: NotificationShelfInteractor,
     activatableViewModel: ActivatableNotificationViewModel,
-    val icons: NotificationIconContainerShelfViewModel,
 ) : ActivatableNotificationViewModel by activatableViewModel {
     /** Is the shelf allowed to be clickable when it has content? */
     val isClickable: Flow<Boolean>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index d2fca8f..7c7d943 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -859,7 +859,7 @@
         mGroupExpansionManager.registerGroupExpansionChangeListener(
                 (changedRow, expanded) -> mView.onGroupExpandChanged(changedRow, expanded));
 
-        mViewBinder.bind(mView, this);
+        mViewBinder.bindWhileAttached(mView, this);
 
         if (!FooterViewRefactor.isEnabled()) {
             collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
index 274bf94..910b40f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt
@@ -16,29 +16,22 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewbinder
 
 import androidx.core.view.doOnDetach
-import androidx.lifecycle.lifecycleScope
-import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
-import kotlinx.coroutines.launch
 
 /**
  * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel].
  */
 object HideNotificationsBinder {
-    fun bindHideList(
+    suspend fun bindHideList(
         viewController: NotificationStackScrollLayoutController,
         viewModel: NotificationListViewModel
     ) {
-        viewController.view.repeatWhenAttached {
-            lifecycleScope.launch {
-                viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
-                    viewController.bindHideState(shouldHide)
-                }
-            }
-        }
-
         viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) }
+
+        viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide ->
+            viewController.bindHideState(shouldHide)
+        }
     }
 
     private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 9373d49..1b36660 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -32,15 +32,14 @@
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel
 import com.android.systemui.statusbar.phone.NotificationIconAreaController
-import com.android.systemui.statusbar.ui.SystemBarUtilsState
+import com.android.systemui.util.kotlin.getOrNull
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.flow.combine
@@ -55,25 +54,27 @@
     private val configuration: ConfigurationState,
     private val falsingManager: FalsingManager,
     private val iconAreaController: NotificationIconAreaController,
-    private val iconViewBindingFailureTracker: StatusBarIconViewBindingFailureTracker,
     private val metricsLogger: MetricsLogger,
-    private val shelfIconViewStore: ShelfNotificationIconViewStore,
-    private val systemBarUtilsState: SystemBarUtilsState,
+    private val nicBinder: NotificationIconContainerShelfViewBinder,
 ) {
 
-    fun bind(
+    fun bindWhileAttached(
         view: NotificationStackScrollLayout,
         viewController: NotificationStackScrollLayoutController
     ) {
-        bindShelf(view)
-        bindHideList(viewController, viewModel)
+        val shelf =
+            LayoutInflater.from(view.context)
+                .inflate(R.layout.status_bar_notification_shelf, view, false) as NotificationShelf
+        view.setShelf(shelf)
 
-        if (FooterViewRefactor.isEnabled) {
-            bindFooter(view)
-            bindEmptyShade(view)
+        view.repeatWhenAttached {
+            lifecycleScope.launch {
+                launch { bindShelf(shelf) }
+                launch { bindHideList(viewController, viewModel) }
 
-            view.repeatWhenAttached {
-                lifecycleScope.launch {
+                if (FooterViewRefactor.isEnabled) {
+                    launch { bindFooter(view) }
+                    launch { bindEmptyShade(view) }
                     viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
                         view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
                     }
@@ -82,73 +83,57 @@
         }
     }
 
-    private fun bindShelf(parentView: NotificationStackScrollLayout) {
-        val shelf =
-            LayoutInflater.from(parentView.context)
-                .inflate(R.layout.status_bar_notification_shelf, parentView, false)
-                as NotificationShelf
+    private suspend fun bindShelf(shelf: NotificationShelf) {
         NotificationShelfViewBinder.bind(
             shelf,
             viewModel.shelf,
-            configuration,
-            systemBarUtilsState,
             falsingManager,
-            iconViewBindingFailureTracker,
+            nicBinder,
             iconAreaController,
-            shelfIconViewStore,
         )
-        parentView.setShelf(shelf)
     }
 
-    private fun bindFooter(parentView: NotificationStackScrollLayout) {
-        viewModel.footer.ifPresent { footerViewModel ->
+    private suspend fun bindFooter(parentView: NotificationStackScrollLayout) {
+        viewModel.footer.getOrNull()?.let { footerViewModel ->
             // The footer needs to be re-inflated every time the theme or the font size changes.
-            parentView.repeatWhenAttached {
-                configuration.reinflateAndBindLatest(
-                    R.layout.status_bar_notification_footer,
-                    parentView,
-                    attachToRoot = false,
-                    backgroundDispatcher,
-                ) { footerView: FooterView ->
-                    traceSection("bind FooterView") {
-                        val disposableHandle =
-                            FooterViewBinder.bind(
-                                footerView,
-                                footerViewModel,
-                                clearAllNotifications = {
-                                    metricsLogger.action(
-                                        MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
-                                    )
-                                    parentView.clearAllNotifications()
-                                },
-                            )
-                        parentView.setFooterView(footerView)
-                        return@reinflateAndBindLatest disposableHandle
-                    }
+            configuration.reinflateAndBindLatest(
+                R.layout.status_bar_notification_footer,
+                parentView,
+                attachToRoot = false,
+                backgroundDispatcher,
+            ) { footerView: FooterView ->
+                traceSection("bind FooterView") {
+                    val disposableHandle =
+                        FooterViewBinder.bind(
+                            footerView,
+                            footerViewModel,
+                            clearAllNotifications = {
+                                metricsLogger.action(
+                                    MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
+                                )
+                                parentView.clearAllNotifications()
+                            },
+                        )
+                    parentView.setFooterView(footerView)
+                    return@reinflateAndBindLatest disposableHandle
                 }
             }
         }
     }
 
-    private fun bindEmptyShade(
-        parentView: NotificationStackScrollLayout,
-    ) {
-        parentView.repeatWhenAttached {
-            lifecycleScope.launch {
-                combine(
-                        viewModel.shouldShowEmptyShadeView,
-                        viewModel.areNotificationsHiddenInShade,
-                        viewModel.hasFilteredOutSeenNotifications,
-                        ::Triple
-                    )
-                    .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
-                        parentView.updateEmptyShadeView(
-                            shouldShow,
-                            areNotifsHidden,
-                            hasFilteredNotifs,
-                        )
-                    }
+    private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
+        combine(
+                viewModel.shouldShowEmptyShadeView,
+                viewModel.areNotificationsHiddenInShade,
+                viewModel.hasFilteredOutSeenNotifications,
+                ::Triple
+            )
+            .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
+                parentView.updateEmptyShadeView(
+                    shouldShow,
+                    areNotifsHidden,
+                    hasFilteredNotifs,
+                )
             }
-        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index cd99934..2740cc6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -38,7 +38,6 @@
 import com.android.app.animation.InterpolatorsAndroidX;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dumpable;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
@@ -54,10 +53,7 @@
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState;
 import com.android.systemui.statusbar.events.SystemStatusAnimationCallback;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder;
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
@@ -75,7 +71,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarVisibilityChangeListener;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
@@ -95,6 +90,8 @@
 
 import kotlin.Unit;
 
+import kotlinx.coroutines.DisposableHandle;
+
 /**
  * Contains the collapsed status bar and handles hiding/showing based on disable flags
  * and keyguard state. Also manages lifecycle to make sure the views it contains are being
@@ -151,10 +148,7 @@
     private final DumpManager mDumpManager;
     private final StatusBarWindowStateController mStatusBarWindowStateController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final NotificationIconContainerStatusBarViewModel mStatusBarIconsViewModel;
-    private final ConfigurationState mConfigurationState;
-    private final SystemBarUtilsState mSystemBarUtilsState;
-    private final StatusBarNotificationIconViewStore mStatusBarIconViewStore;
+    private final NotificationIconContainerStatusBarViewBinder mNicViewBinder;
     private final DemoModeController mDemoModeController;
 
     private List<String> mBlockedIcons = new ArrayList<>();
@@ -216,7 +210,7 @@
         mWaitingForWindowStateChangeAfterCameraLaunch = false;
         mTransitionFromLockscreenToDreamStarted = false;
     };
-    private final StatusBarIconViewBindingFailureTracker mIconViewBindingFailureTracker;
+    private DisposableHandle mNicBindingDisposable;
 
     @Inject
     public CollapsedStatusBarFragment(
@@ -234,7 +228,7 @@
             KeyguardStateController keyguardStateController,
             ShadeViewController shadeViewController,
             StatusBarStateController statusBarStateController,
-            StatusBarIconViewBindingFailureTracker iconViewBindingFailureTracker,
+            NotificationIconContainerStatusBarViewBinder nicViewBinder,
             CommandQueue commandQueue,
             CarrierConfigTracker carrierConfigTracker,
             CollapsedStatusBarFragmentLogger collapsedStatusBarFragmentLogger,
@@ -244,10 +238,6 @@
             DumpManager dumpManager,
             StatusBarWindowStateController statusBarWindowStateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            NotificationIconContainerStatusBarViewModel statusBarIconsViewModel,
-            ConfigurationState configurationState,
-            SystemBarUtilsState systemBarUtilsState,
-            StatusBarNotificationIconViewStore statusBarIconViewStore,
             DemoModeController demoModeController) {
         mStatusBarFragmentComponentFactory = statusBarFragmentComponentFactory;
         mOngoingCallController = ongoingCallController;
@@ -263,7 +253,7 @@
         mKeyguardStateController = keyguardStateController;
         mShadeViewController = shadeViewController;
         mStatusBarStateController = statusBarStateController;
-        mIconViewBindingFailureTracker = iconViewBindingFailureTracker;
+        mNicViewBinder = nicViewBinder;
         mCommandQueue = commandQueue;
         mCarrierConfigTracker = carrierConfigTracker;
         mCollapsedStatusBarFragmentLogger = collapsedStatusBarFragmentLogger;
@@ -273,10 +263,6 @@
         mDumpManager = dumpManager;
         mStatusBarWindowStateController = statusBarWindowStateController;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mStatusBarIconsViewModel = statusBarIconsViewModel;
-        mConfigurationState = configurationState;
-        mSystemBarUtilsState = systemBarUtilsState;
-        mStatusBarIconViewStore = statusBarIconViewStore;
         mDemoModeController = demoModeController;
     }
 
@@ -455,6 +441,12 @@
             mStartableStates.put(startable, Startable.State.STOPPED);
         }
         mDumpManager.unregisterDumpable(getClass().getSimpleName());
+        if (NotificationIconContainerRefactor.isEnabled()) {
+            if (mNicBindingDisposable != null) {
+                mNicBindingDisposable.dispose();
+                mNicBindingDisposable = null;
+            }
+        }
     }
 
     /** Initializes views related to the notification icon area. */
@@ -466,13 +458,7 @@
                         .inflate(R.layout.notification_icon_area, notificationIconArea, true);
             NotificationIconContainer notificationIcons =
                     notificationIconArea.requireViewById(R.id.notificationIcons);
-            NotificationIconContainerViewBinder.bindWhileAttached(
-                    notificationIcons,
-                    mStatusBarIconsViewModel,
-                    mConfigurationState,
-                    mSystemBarUtilsState,
-                    mIconViewBindingFailureTracker,
-                    mStatusBarIconViewStore);
+            mNicBindingDisposable = mNicViewBinder.bindWhileAttached(notificationIcons);
         } else {
             mNotificationIconAreaInner =
                     mNotificationIconAreaController.getNotificationInnerAreaView();
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
index e0d205f..c170eb5 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
@@ -37,6 +37,7 @@
 import com.android.internal.util.UserIcons
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.Flags.switchUserOnBg
 import com.android.systemui.SystemUISecondaryUserService
 import com.android.systemui.animation.Expandable
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -44,6 +45,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -100,6 +102,7 @@
     broadcastDispatcher: BroadcastDispatcher,
     keyguardUpdateMonitor: KeyguardUpdateMonitor,
     @Background private val backgroundDispatcher: CoroutineDispatcher,
+    @Main private val mainDispatcher: CoroutineDispatcher,
     private val activityManager: ActivityManager,
     private val refreshUsersScheduler: RefreshUsersScheduler,
     private val guestUserInteractor: GuestUserInteractor,
@@ -339,7 +342,11 @@
             }
             .launchIn(applicationScope)
         restartSecondaryService(repository.getSelectedUserInfo().id)
-        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        applicationScope.launch {
+            withContext(mainDispatcher) {
+                keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+            }
+        }
     }
 
     fun addCallback(callback: UserCallback) {
@@ -593,10 +600,18 @@
     private fun switchUser(userId: Int) {
         // TODO(b/246631653): track jank and latency like in the old impl.
         refreshUsersScheduler.pause()
-        try {
-            activityManager.switchUser(userId)
-        } catch (e: RemoteException) {
-            Log.e(TAG, "Couldn't switch user.", e)
+        val runnable = Runnable {
+            try {
+                activityManager.switchUser(userId)
+            } catch (e: RemoteException) {
+                Log.e(TAG, "Couldn't switch user.", e)
+            }
+        }
+
+        if (switchUserOnBg()) {
+            applicationScope.launch { withContext(backgroundDispatcher) { runnable.run() } }
+        } else {
+            runnable.run()
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
deleted file mode 100644
index 8215360..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright (C) 2017 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.leak;
-
-import android.content.ClipData;
-import android.content.ClipDescription;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.util.Log;
-
-import androidx.core.content.FileProvider;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-/**
- * Utility class for dumping, compressing, sending, and serving heap dump files.
- *
- * <p>Unlike the Internet, this IS a big truck you can dump something on.
- */
-public class DumpTruck {
-    private static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
-    private static final String FILEPROVIDER_PATH = "leak";
-
-    private static final String TAG = "DumpTruck";
-    private static final int BUFSIZ = 1024 * 1024; // 1MB
-
-    private final Context context;
-    private final GarbageMonitor mGarbageMonitor;
-    private Uri hprofUri;
-    private long rss;
-    final StringBuilder body = new StringBuilder();
-
-    public DumpTruck(Context context, GarbageMonitor garbageMonitor) {
-        this.context = context;
-        mGarbageMonitor = garbageMonitor;
-    }
-
-    /**
-     * Capture memory for the given processes and zip them up for sharing.
-     *
-     * @param pids
-     * @return this, for chaining
-     */
-    public DumpTruck captureHeaps(List<Long> pids) {
-        final File dumpDir = new File(context.getCacheDir(), FILEPROVIDER_PATH);
-        dumpDir.mkdirs();
-        hprofUri = null;
-
-        body.setLength(0);
-        body.append("Build: ").append(Build.DISPLAY).append("\n\nProcesses:\n");
-
-        final ArrayList<String> paths = new ArrayList<String>();
-        final int myPid = android.os.Process.myPid();
-
-        for (Long pidL : pids) {
-            final int pid = pidL.intValue();
-            body.append("  pid ").append(pid);
-            GarbageMonitor.ProcessMemInfo info = mGarbageMonitor.getMemInfo(pid);
-            if (info != null) {
-                body.append(":")
-                        .append(" up=")
-                        .append(info.getUptime())
-                        .append(" rss=")
-                        .append(info.currentRss);
-                rss = info.currentRss;
-            }
-            if (pid == myPid) {
-                final String path =
-                        new File(dumpDir, String.format("heap-%d.ahprof", pid)).getPath();
-                Log.v(TAG, "Dumping memory info for process " + pid + " to " + path);
-                try {
-                    android.os.Debug.dumpHprofData(path); // will block
-                    paths.add(path);
-                    body.append(" (hprof attached)");
-                } catch (IOException e) {
-                    Log.e(TAG, "error dumping memory:", e);
-                    body.append("\n** Could not dump heap: \n").append(e.toString()).append("\n");
-                }
-            }
-            body.append("\n");
-        }
-
-        try {
-            final String zipfile =
-                    new File(dumpDir, String.format("hprof-%d.zip", System.currentTimeMillis()))
-                            .getCanonicalPath();
-            if (DumpTruck.zipUp(zipfile, paths)) {
-                final File pathFile = new File(zipfile);
-                hprofUri = FileProvider.getUriForFile(context, FILEPROVIDER_AUTHORITY, pathFile);
-                Log.v(TAG, "Heap dump accessible at URI: " + hprofUri);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "unable to zip up heapdumps", e);
-            body.append("\n** Could not zip up files: \n").append(e.toString()).append("\n");
-        }
-
-        return this;
-    }
-
-    /**
-     * Get the Uri of the current heap dump. Be sure to call captureHeaps first.
-     *
-     * @return Uri to the dump served by the SystemUI file provider
-     */
-    public Uri getDumpUri() {
-        return hprofUri;
-    }
-
-    /**
-     * Get an ACTION_SEND intent suitable for startActivity() or attaching to a Notification.
-     *
-     * @return share intent
-     */
-    public Intent createShareIntent() {
-        Intent shareIntent = new Intent(Intent.ACTION_SEND_MULTIPLE);
-        shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-        shareIntent.putExtra(Intent.EXTRA_SUBJECT,
-                String.format("SystemUI memory dump (rss=%dM)", rss / 1024));
-
-        shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString());
-
-        if (hprofUri != null) {
-            final ArrayList<Uri> uriList = new ArrayList<>();
-            uriList.add(hprofUri);
-            shareIntent.setType("application/zip");
-            shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);
-
-            // Include URI in ClipData also, so that grantPermission picks it up.
-            // We don't use setData here because some apps interpret this as "to:".
-            ClipData clipdata = new ClipData(new ClipDescription("content",
-                    new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
-                    new ClipData.Item(hprofUri));
-            shareIntent.setClipData(clipdata);
-            shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-        }
-        return shareIntent;
-    }
-
-    private static boolean zipUp(String zipfilePath, ArrayList<String> paths) {
-        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfilePath))) {
-            final byte[] buf = new byte[BUFSIZ];
-
-            for (String filename : paths) {
-                try (InputStream is = new BufferedInputStream(new FileInputStream(filename))) {
-                    ZipEntry entry = new ZipEntry(filename);
-                    zos.putNextEntry(entry);
-                    int len;
-                    while (0 < (len = is.read(buf, 0, BUFSIZ))) {
-                        zos.write(buf, 0, len);
-                    }
-                    zos.closeEntry();
-                }
-            }
-            return true;
-        } catch (IOException e) {
-            Log.e(TAG, "error zipping up profile data", e);
-        }
-        return false;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
deleted file mode 100644
index de392d3..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
+++ /dev/null
@@ -1,617 +0,0 @@
-/*
- * Copyright (C) 2017 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.leak;
-
-import static android.service.quicksettings.Tile.STATE_ACTIVE;
-import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE;
-
-import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Process;
-import android.os.SystemProperties;
-import android.provider.Settings;
-import android.text.format.DateUtils;
-import android.util.Log;
-import android.util.LongSparseArray;
-import android.view.View;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.systemui.CoreStartable;
-import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.qs.QSTile;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.qs.QSHost;
-import com.android.systemui.qs.QsEventLogger;
-import com.android.systemui.qs.logging.QSLogger;
-import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
-import com.android.systemui.qs.tileimpl.QSTileImpl;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.concurrency.MessageRouter;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.inject.Inject;
-
-/**
- * Suite of tools to periodically inspect the System UI heap and possibly prompt the user to
- * capture heap dumps and report them. Includes the implementation of the "Dump SysUI Heap"
- * quick settings tile.
- */
-@SysUISingleton
-public class GarbageMonitor implements Dumpable {
-    // Feature switches
-    // ================
-
-    // Whether to use TrackedGarbage to trigger LeakReporter. Off by default unless you set the
-    // appropriate sysprop on a userdebug device.
-    public static final boolean LEAK_REPORTING_ENABLED = Build.IS_DEBUGGABLE
-            && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
-    public static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting";
-
-    // Heap tracking: watch the current memory levels and update the MemoryTile if available.
-    // On for all userdebug devices.
-    public static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE;
-
-    // Tell QSTileHost.java to toss this into the default tileset?
-    public static final boolean ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS = true;
-
-    // whether to use ActivityManager.setHeapLimit (and post a notification to the user asking
-    // to dump the heap). Off by default unless you set the appropriate sysprop on userdebug
-    private static final boolean ENABLE_AM_HEAP_LIMIT = Build.IS_DEBUGGABLE
-            && SystemProperties.getBoolean("debug.enable_sysui_heap_limit", false);
-
-    // Tuning params
-    // =============
-
-    // threshold for setHeapLimit(), in KB (overrides R.integer.watch_heap_limit)
-    private static final String SETTINGS_KEY_AM_HEAP_LIMIT = "systemui_am_heap_limit";
-
-    private static final long GARBAGE_INSPECTION_INTERVAL =
-            15 * DateUtils.MINUTE_IN_MILLIS; // 15 min
-    private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min
-    private static final int HEAP_TRACK_HISTORY_LEN = 720; // 12 hours
-
-    private static final int DO_GARBAGE_INSPECTION = 1000;
-    private static final int DO_HEAP_TRACK = 3000;
-
-    static final int GARBAGE_ALLOWANCE = 5;
-
-    private static final String TAG = "GarbageMonitor";
-    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
-
-    private final MessageRouter mMessageRouter;
-    private final TrackedGarbage mTrackedGarbage;
-    private final LeakReporter mLeakReporter;
-    private final Context mContext;
-    private final DelayableExecutor mDelayableExecutor;
-    private MemoryTile mQSTile;
-    private final DumpTruck mDumpTruck;
-
-    private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
-    private final ArrayList<Long> mPids = new ArrayList<>();
-
-    private long mHeapLimit;
-
-    /**
-     */
-    @Inject
-    public GarbageMonitor(
-            Context context,
-            @Background DelayableExecutor delayableExecutor,
-            @Background MessageRouter messageRouter,
-            LeakDetector leakDetector,
-            LeakReporter leakReporter,
-            DumpManager dumpManager) {
-        mContext = context.getApplicationContext();
-
-        mDelayableExecutor = delayableExecutor;
-        mMessageRouter = messageRouter;
-        mMessageRouter.subscribeTo(DO_GARBAGE_INSPECTION, this::doGarbageInspection);
-        mMessageRouter.subscribeTo(DO_HEAP_TRACK, this::doHeapTrack);
-
-        mTrackedGarbage = leakDetector.getTrackedGarbage();
-        mLeakReporter = leakReporter;
-
-        mDumpTruck = new DumpTruck(mContext, this);
-
-        dumpManager.registerDumpable(getClass().getSimpleName(), this);
-
-        if (ENABLE_AM_HEAP_LIMIT) {
-            mHeapLimit = Settings.Global.getInt(context.getContentResolver(),
-                    SETTINGS_KEY_AM_HEAP_LIMIT,
-                    mContext.getResources().getInteger(R.integer.watch_heap_limit));
-        }
-    }
-
-    public void startLeakMonitor() {
-        if (mTrackedGarbage == null) {
-            return;
-        }
-
-        mMessageRouter.sendMessage(DO_GARBAGE_INSPECTION);
-    }
-
-    public void startHeapTracking() {
-        startTrackingProcess(
-                android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
-        mMessageRouter.sendMessage(DO_HEAP_TRACK);
-    }
-
-    private boolean gcAndCheckGarbage() {
-        if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
-            Runtime.getRuntime().gc();
-            return true;
-        }
-        return false;
-    }
-
-    void reinspectGarbageAfterGc() {
-        int count = mTrackedGarbage.countOldGarbage();
-        if (count > GARBAGE_ALLOWANCE) {
-            mLeakReporter.dumpLeak(count);
-        }
-    }
-
-    public ProcessMemInfo getMemInfo(int pid) {
-        return mData.get(pid);
-    }
-
-    public List<Long> getTrackedProcesses() {
-        return mPids;
-    }
-
-    public void startTrackingProcess(long pid, String name, long start) {
-        synchronized (mPids) {
-            if (mPids.contains(pid)) return;
-
-            mPids.add(pid);
-            logPids();
-
-            mData.put(pid, new ProcessMemInfo(pid, name, start));
-        }
-    }
-
-    private void logPids() {
-        if (DEBUG) {
-            StringBuffer sb = new StringBuffer("Now tracking processes: ");
-            for (int i = 0; i < mPids.size(); i++) {
-                final int p = mPids.get(i).intValue();
-                sb.append(" ");
-            }
-            Log.v(TAG, sb.toString());
-        }
-    }
-
-    private void update() {
-        synchronized (mPids) {
-            for (int i = 0; i < mPids.size(); i++) {
-                final int pid = mPids.get(i).intValue();
-                // rssValues contains [VmRSS, RssFile, RssAnon, VmSwap].
-                long[] rssValues = Process.getRss(pid);
-                if (rssValues == null && rssValues.length == 0) {
-                    if (DEBUG) Log.e(TAG, "update: Process.getRss() didn't provide any values.");
-                    break;
-                }
-                long rss = rssValues[0];
-                final ProcessMemInfo info = mData.get(pid);
-                info.rss[info.head] = info.currentRss = rss;
-                info.head = (info.head + 1) % info.rss.length;
-                if (info.currentRss > info.max) info.max = info.currentRss;
-                if (info.currentRss == 0) {
-                    if (DEBUG) Log.v(TAG, "update: pid " + pid + " has rss=0, it probably died");
-                    mData.remove(pid);
-                }
-            }
-            for (int i = mPids.size() - 1; i >= 0; i--) {
-                final long pid = mPids.get(i).intValue();
-                if (mData.get(pid) == null) {
-                    mPids.remove(i);
-                    logPids();
-                }
-            }
-        }
-        if (mQSTile != null) mQSTile.update();
-    }
-
-    private void setTile(MemoryTile tile) {
-        mQSTile = tile;
-        if (tile != null) tile.update();
-    }
-
-    private static String formatBytes(long b) {
-        String[] SUFFIXES = {"B", "K", "M", "G", "T"};
-        int i;
-        for (i = 0; i < SUFFIXES.length; i++) {
-            if (b < 1024) break;
-            b /= 1024;
-        }
-        return b + SUFFIXES[i];
-    }
-
-    private Intent dumpHprofAndGetShareIntent() {
-        return mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent();
-    }
-
-    @Override
-    public void dump(PrintWriter pw, @Nullable String[] args) {
-        pw.println("GarbageMonitor params:");
-        pw.println(String.format("   mHeapLimit=%d KB", mHeapLimit));
-        pw.println(String.format("   GARBAGE_INSPECTION_INTERVAL=%d (%.1f mins)",
-                GARBAGE_INSPECTION_INTERVAL,
-                (float) GARBAGE_INSPECTION_INTERVAL / DateUtils.MINUTE_IN_MILLIS));
-        final float htiMins = HEAP_TRACK_INTERVAL / DateUtils.MINUTE_IN_MILLIS;
-        pw.println(String.format("   HEAP_TRACK_INTERVAL=%d (%.1f mins)",
-                HEAP_TRACK_INTERVAL,
-                htiMins));
-        pw.println(String.format("   HEAP_TRACK_HISTORY_LEN=%d (%.1f hr total)",
-                HEAP_TRACK_HISTORY_LEN,
-                (float) HEAP_TRACK_HISTORY_LEN * htiMins / 60f));
-
-        pw.println("GarbageMonitor tracked processes:");
-
-        for (long pid : mPids) {
-            final ProcessMemInfo pmi = mData.get(pid);
-            if (pmi != null) {
-                pmi.dump(pw, args);
-            }
-        }
-    }
-
-
-    private static class MemoryIconDrawable extends Drawable {
-        long rss, limit;
-        final Drawable baseIcon;
-        final Paint paint = new Paint();
-        final float dp;
-
-        MemoryIconDrawable(Context context) {
-            baseIcon = context.getDrawable(R.drawable.ic_memory).mutate();
-            dp = context.getResources().getDisplayMetrics().density;
-            paint.setColor(Color.WHITE);
-        }
-
-        public void setRss(long rss) {
-            if (rss != this.rss) {
-                this.rss = rss;
-                invalidateSelf();
-            }
-        }
-
-        public void setLimit(long limit) {
-            if (limit != this.limit) {
-                this.limit = limit;
-                invalidateSelf();
-            }
-        }
-
-        @Override
-        public void draw(Canvas canvas) {
-            baseIcon.draw(canvas);
-
-            if (limit > 0 && rss > 0) {
-                float frac = Math.min(1f, (float) rss / limit);
-
-                final Rect bounds = getBounds();
-                canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp);
-                //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
-                canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint);
-            }
-        }
-
-        @Override
-        public void setBounds(int left, int top, int right, int bottom) {
-            super.setBounds(left, top, right, bottom);
-            baseIcon.setBounds(left, top, right, bottom);
-        }
-
-        @Override
-        public int getIntrinsicHeight() {
-            return baseIcon.getIntrinsicHeight();
-        }
-
-        @Override
-        public int getIntrinsicWidth() {
-            return baseIcon.getIntrinsicWidth();
-        }
-
-        @Override
-        public void setAlpha(int i) {
-            baseIcon.setAlpha(i);
-        }
-
-        @Override
-        public void setColorFilter(ColorFilter colorFilter) {
-            baseIcon.setColorFilter(colorFilter);
-            paint.setColorFilter(colorFilter);
-        }
-
-        @Override
-        public void setTint(int tint) {
-            super.setTint(tint);
-            baseIcon.setTint(tint);
-        }
-
-        @Override
-        public void setTintList(ColorStateList tint) {
-            super.setTintList(tint);
-            baseIcon.setTintList(tint);
-        }
-
-        @Override
-        public void setTintMode(PorterDuff.Mode tintMode) {
-            super.setTintMode(tintMode);
-            baseIcon.setTintMode(tintMode);
-        }
-
-        @Override
-        public int getOpacity() {
-            return PixelFormat.TRANSLUCENT;
-        }
-    }
-
-    private static class MemoryGraphIcon extends QSTile.Icon {
-        long rss, limit;
-
-        public void setRss(long rss) {
-            this.rss = rss;
-        }
-
-        public void setHeapLimit(long limit) {
-            this.limit = limit;
-        }
-
-        @Override
-        public Drawable getDrawable(Context context) {
-            final MemoryIconDrawable drawable = new MemoryIconDrawable(context);
-            drawable.setRss(rss);
-            drawable.setLimit(limit);
-            return drawable;
-        }
-    }
-
-    public static class MemoryTile extends QSTileImpl<QSTile.State> {
-        public static final String TILE_SPEC = "dbg:mem";
-
-        private final GarbageMonitor gm;
-        private ProcessMemInfo pmi;
-        private boolean dumpInProgress;
-        private final PanelInteractor mPanelInteractor;
-
-        @Inject
-        public MemoryTile(
-                QSHost host,
-                QsEventLogger uiEventLogger,
-                @Background Looper backgroundLooper,
-                @Main Handler mainHandler,
-                FalsingManager falsingManager,
-                MetricsLogger metricsLogger,
-                StatusBarStateController statusBarStateController,
-                ActivityStarter activityStarter,
-                QSLogger qsLogger,
-                GarbageMonitor monitor,
-                PanelInteractor panelInteractor
-        ) {
-            super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
-                    statusBarStateController, activityStarter, qsLogger);
-            gm = monitor;
-            mPanelInteractor = panelInteractor;
-        }
-
-        @Override
-        public State newTileState() {
-            return new QSTile.State();
-        }
-
-        @Override
-        public Intent getLongClickIntent() {
-            return new Intent();
-        }
-
-        @Override
-        protected void handleClick(@Nullable View view) {
-            if (dumpInProgress) return;
-
-            dumpInProgress = true;
-            refreshState();
-            new Thread("HeapDumpThread") {
-                @Override
-                public void run() {
-                    try {
-                        // wait for animations & state changes
-                        Thread.sleep(500);
-                    } catch (InterruptedException ignored) { }
-                    final Intent shareIntent = gm.dumpHprofAndGetShareIntent();
-                    mHandler.post(() -> {
-                        dumpInProgress = false;
-                        refreshState();
-                        mPanelInteractor.collapsePanels();
-                        mActivityStarter.postStartActivityDismissingKeyguard(shareIntent, 0);
-                    });
-                }
-            }.start();
-        }
-
-        @Override
-        public int getMetricsCategory() {
-            return VIEW_UNKNOWN;
-        }
-
-        @Override
-        public void handleSetListening(boolean listening) {
-            super.handleSetListening(listening);
-            if (gm != null) gm.setTile(listening ? this : null);
-
-            final ActivityManager am = mContext.getSystemService(ActivityManager.class);
-            if (listening && gm.mHeapLimit > 0) {
-                am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes?
-            } else {
-                am.clearWatchHeapLimit();
-            }
-        }
-
-        @Override
-        public CharSequence getTileLabel() {
-            return getState().label;
-        }
-
-        @Override
-        protected void handleUpdateState(State state, Object arg) {
-            pmi = gm.getMemInfo(Process.myPid());
-            final MemoryGraphIcon icon = new MemoryGraphIcon();
-            icon.setHeapLimit(gm.mHeapLimit);
-            state.state = dumpInProgress ? STATE_UNAVAILABLE : STATE_ACTIVE;
-            state.label = dumpInProgress
-                    ? "Dumping..."
-                    : mContext.getString(R.string.heap_dump_tile_name);
-            if (pmi != null) {
-                icon.setRss(pmi.currentRss);
-                state.secondaryLabel =
-                        String.format(
-                                "rss: %s / %s",
-                                formatBytes(pmi.currentRss * 1024),
-                                formatBytes(gm.mHeapLimit * 1024));
-            } else {
-                icon.setRss(0);
-                state.secondaryLabel = null;
-            }
-            state.icon = icon;
-        }
-
-        public void update() {
-            refreshState();
-        }
-
-        public long getRss() {
-            return pmi != null ? pmi.currentRss : 0;
-        }
-
-        public long getHeapLimit() {
-            return gm != null ? gm.mHeapLimit : 0;
-        }
-    }
-
-    /** */
-    public static class ProcessMemInfo implements Dumpable {
-        public long pid;
-        public String name;
-        public long startTime;
-        public long currentRss;
-        public long[] rss = new long[HEAP_TRACK_HISTORY_LEN];
-        public long max = 1;
-        public int head = 0;
-
-        public ProcessMemInfo(long pid, String name, long start) {
-            this.pid = pid;
-            this.name = name;
-            this.startTime = start;
-        }
-
-        public long getUptime() {
-            return System.currentTimeMillis() - startTime;
-        }
-
-        @Override
-        public void dump(PrintWriter pw, @Nullable String[] args) {
-            pw.print("{ \"pid\": ");
-            pw.print(pid);
-            pw.print(", \"name\": \"");
-            pw.print(name.replace('"', '-'));
-            pw.print("\", \"start\": ");
-            pw.print(startTime);
-            pw.print(", \"rss\": [");
-            // write rss values starting from the oldest, which is rss[head], wrapping around to
-            // rss[(head-1) % rss.length]
-            for (int i = 0; i < rss.length; i++) {
-                if (i > 0) pw.print(",");
-                pw.print(rss[(head + i) % rss.length]);
-            }
-            pw.println("] }");
-        }
-    }
-
-    /** */
-    @SysUISingleton
-    public static class Service implements CoreStartable,  Dumpable {
-        private final Context mContext;
-        private final GarbageMonitor mGarbageMonitor;
-
-        @Inject
-        public Service(Context context, GarbageMonitor garbageMonitor) {
-            mContext = context;
-            mGarbageMonitor = garbageMonitor;
-        }
-
-        @Override
-        public void start() {
-            boolean forceEnable =
-                    Settings.Secure.getInt(
-                                    mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0)
-                            != 0;
-            if (LEAK_REPORTING_ENABLED || forceEnable) {
-                mGarbageMonitor.startLeakMonitor();
-            }
-            if (HEAP_TRACKING_ENABLED || forceEnable) {
-                mGarbageMonitor.startHeapTracking();
-            }
-        }
-
-        @Override
-        public void dump(PrintWriter pw, @Nullable String[] args) {
-            if (mGarbageMonitor != null) mGarbageMonitor.dump(pw, args);
-        }
-    }
-
-    private void doGarbageInspection(int id) {
-        if (gcAndCheckGarbage()) {
-            mDelayableExecutor.executeDelayed(this::reinspectGarbageAfterGc, 100);
-        }
-
-        mMessageRouter.cancelMessages(DO_GARBAGE_INSPECTION);
-        mMessageRouter.sendMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
-    }
-
-    private void doHeapTrack(int id) {
-        update();
-        mMessageRouter.cancelMessages(DO_HEAP_TRACK);
-        mMessageRouter.sendMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt
deleted file mode 100644
index e975200..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitorModule.kt
+++ /dev/null
@@ -1,39 +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.leak
-
-import com.android.systemui.CoreStartable
-import com.android.systemui.qs.tileimpl.QSTileImpl
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
-import dagger.multibindings.StringKey
-
-@Module
-interface GarbageMonitorModule {
-    /** Inject into GarbageMonitor.Service. */
-    @Binds
-    @IntoMap
-    @ClassKey(GarbageMonitor::class)
-    fun bindGarbageMonitorService(sysui: GarbageMonitor.Service): CoreStartable
-
-    @Binds
-    @IntoMap
-    @StringKey(GarbageMonitor.MemoryTile.TILE_SPEC)
-    fun bindMemoryTile(memoryTile: GarbageMonitor.MemoryTile): QSTileImpl<*>
-}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
index 88f63ad..a249961 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java
@@ -34,14 +34,12 @@
 import android.widget.RelativeLayout;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory;
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager;
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.plugins.clocks.ClockAnimations;
 import com.android.systemui.plugins.clocks.ClockController;
@@ -56,14 +54,9 @@
 import com.android.systemui.shared.clocks.ClockRegistry;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel;
-import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerAlwaysOnDisplayViewBinder;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -185,9 +178,7 @@
                 mKeyguardSliceViewController,
                 mNotificationIconAreaController,
                 mSmartspaceController,
-                mock(SystemBarUtilsState.class),
-                mock(ScreenOffAnimationController.class),
-                mock(StatusBarIconViewBindingFailureTracker.class),
+                mock(NotificationIconContainerAlwaysOnDisplayViewBinder.class),
                 mKeyguardUnlockAnimationController,
                 mSecureSettings,
                 mExecutor,
@@ -195,11 +186,6 @@
                 mDumpManager,
                 mClockEventController,
                 mLogBuffer,
-                mock(NotificationIconContainerAlwaysOnDisplayViewModel.class),
-                mock(KeyguardRootViewModel.class),
-                mock(ConfigurationState.class),
-                mock(DozeParameters.class),
-                mock(AlwaysOnDisplayNotificationIconViewStore.class),
                 KeyguardInteractorFactory.create(mFakeFeatureFlags).getKeyguardInteractor(),
                 mKeyguardClockInteractor,
                 mFakeFeatureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 5e2423a..ef7798e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -447,10 +447,6 @@
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles_default, "spec1,spec1");
         List<String> specs = QSTileHost.loadTileSpecs(mContext, "default");
-
-        // Remove spurious tiles, like dbg:mem
-        specs.removeIf(spec -> !"spec1".equals(spec));
-        assertEquals(1, specs.size());
     }
 
     @Test
@@ -458,10 +454,6 @@
         mContext.getOrCreateTestableResources()
                 .addOverride(R.string.quick_settings_tiles_default, "spec1");
         List<String> specs = QSTileHost.loadTileSpecs(mContext, "default,spec1");
-
-        // Remove spurious tiles, like dbg:mem
-        specs.removeIf(spec -> !"spec1".equals(spec));
-        assertEquals(1, specs.size());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
index 067218a..5201e5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSFactoryImplTest.kt
@@ -50,7 +50,6 @@
 import com.android.systemui.qs.tiles.ScreenRecordTile
 import com.android.systemui.qs.tiles.UiModeNightTile
 import com.android.systemui.qs.tiles.WorkModeTile
-import com.android.systemui.util.leak.GarbageMonitor
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -117,7 +116,6 @@
     @Mock private lateinit var dataSaverTile: DataSaverTile
     @Mock private lateinit var nightDisplayTile: NightDisplayTile
     @Mock private lateinit var nfcTile: NfcTile
-    @Mock private lateinit var memoryTile: GarbageMonitor.MemoryTile
     @Mock private lateinit var darkModeTile: UiModeNightTile
     @Mock private lateinit var screenRecordTile: ScreenRecordTile
     @Mock private lateinit var reduceBrightColorsTile: ReduceBrightColorsTile
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
index d4e8d37..72fc65b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/MessageContainerControllerTest.kt
@@ -10,8 +10,6 @@
 import androidx.constraintlayout.widget.Guideline
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
@@ -39,7 +37,6 @@
     lateinit var detectionNoticeView: ViewGroup
     lateinit var container: FrameLayout
 
-    var featureFlags = FakeFeatureFlags()
     lateinit var screenshotView: ViewGroup
 
     val userHandle = UserHandle.of(5)
@@ -55,7 +52,6 @@
             MessageContainerController(
                 workProfileMessageController,
                 screenshotDetectionController,
-                featureFlags
             )
         screenshotView = ConstraintLayout(mContext)
         workProfileData = WorkProfileMessageController.WorkProfileFirstRunData(appName, icon)
@@ -105,8 +101,6 @@
 
     @Test
     fun testOnScreenshotTakenScreenshotData_nothingToShow() {
-        featureFlags.set(Flags.SCREENSHOT_DETECTION, true)
-
         messageContainer.onScreenshotTaken(screenshotData)
 
         verify(workProfileMessageController, never()).populateView(any(), any(), any())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 1cc611c..14751c2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -43,7 +43,6 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiBaseFragmentTest;
 import com.android.systemui.animation.AnimatorTestRule;
-import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.log.LogBuffer;
@@ -57,9 +56,7 @@
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.disableflags.DisableFlagsLogger;
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarNotificationIconViewStore;
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerStatusBarViewModel;
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
@@ -70,7 +67,6 @@
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewBinder;
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.FakeCollapsedStatusBarViewModel;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.statusbar.ui.SystemBarUtilsState;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateListener;
 import com.android.systemui.util.CarrierConfigTracker;
@@ -702,7 +698,7 @@
                 mKeyguardStateController,
                 mShadeViewController,
                 mStatusBarStateController,
-                mock(StatusBarIconViewBindingFailureTracker.class),
+                mock(NotificationIconContainerStatusBarViewBinder.class),
                 mCommandQueue,
                 mCarrierConfigTracker,
                 new CollapsedStatusBarFragmentLogger(
@@ -715,10 +711,6 @@
                 mDumpManager,
                 mStatusBarWindowStateController,
                 mKeyguardUpdateMonitor,
-                mock(NotificationIconContainerStatusBarViewModel.class),
-                mock(ConfigurationState.class),
-                mock(SystemBarUtilsState.class),
-                mock(StatusBarNotificationIconViewStore.class),
                 mock(DemoModeController.class));
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index bf851eb..6714c94 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -1115,6 +1115,7 @@
                 broadcastDispatcher = fakeBroadcastDispatcher,
                 keyguardUpdateMonitor = keyguardUpdateMonitor,
                 backgroundDispatcher = utils.testDispatcher,
+                mainDispatcher = utils.testDispatcher,
                 activityManager = activityManager,
                 refreshUsersScheduler = refreshUsersScheduler,
                 guestUserInteractor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index d1870b1..21d4549 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -258,6 +258,7 @@
                     broadcastDispatcher = fakeBroadcastDispatcher,
                     keyguardUpdateMonitor = keyguardUpdateMonitor,
                     backgroundDispatcher = testDispatcher,
+                    mainDispatcher = testDispatcher,
                     activityManager = activityManager,
                     refreshUsersScheduler = refreshUsersScheduler,
                     guestUserInteractor = guestUserInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index b7b24f6..d0804be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -170,6 +170,7 @@
                         broadcastDispatcher = fakeBroadcastDispatcher,
                         keyguardUpdateMonitor = keyguardUpdateMonitor,
                         backgroundDispatcher = testDispatcher,
+                        mainDispatcher = testDispatcher,
                         activityManager = activityManager,
                         refreshUsersScheduler = refreshUsersScheduler,
                         guestUserInteractor = guestUserInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
deleted file mode 100644
index a2b016f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- * Copyright (C) 2017 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.leak;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.testing.AndroidTestingRunner;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.concurrency.MessageRouterImpl;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class GarbageMonitorTest extends SysuiTestCase {
-
-    @Mock private LeakReporter mLeakReporter;
-    @Mock private TrackedGarbage mTrackedGarbage;
-    @Mock private DumpManager mDumpManager;
-    private GarbageMonitor mGarbageMonitor;
-    private final FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        mGarbageMonitor =
-                new GarbageMonitor(
-                        mContext,
-                        mFakeExecutor,
-                        new MessageRouterImpl(mFakeExecutor),
-                        new LeakDetector(null, mTrackedGarbage, null, mDumpManager),
-                        mLeakReporter,
-                        mDumpManager);
-    }
-
-    @Test
-    public void testALittleGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE);
-
-        mGarbageMonitor.reinspectGarbageAfterGc();
-
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-    }
-
-    @Test
-    public void testTransientGarbage_doesntDump() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        // Start the leak monitor. Nothing gets reported immediately.
-        mGarbageMonitor.startLeakMonitor();
-        mFakeExecutor.runAllReady();
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-
-        // Garbage gets reset to 0 before the leak reporte actually gets called.
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(0);
-        mFakeExecutor.advanceClockToLast();
-        mFakeExecutor.runAllReady();
-
-        // Therefore nothing gets dumped.
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-    }
-
-    @Test
-    public void testLotsOfPersistentGarbage_dumps() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        mGarbageMonitor.reinspectGarbageAfterGc();
-
-        verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-    }
-
-    @Test
-    public void testLotsOfPersistentGarbage_dumpsAfterAtime() {
-        when(mTrackedGarbage.countOldGarbage()).thenReturn(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-
-        // Start the leak monitor. Nothing gets reported immediately.
-        mGarbageMonitor.startLeakMonitor();
-        mFakeExecutor.runAllReady();
-        verify(mLeakReporter, never()).dumpLeak(anyInt());
-
-        mFakeExecutor.advanceClockToLast();
-        mFakeExecutor.runAllReady();
-
-        verify(mLeakReporter).dumpLeak(GarbageMonitor.GARBAGE_ALLOWANCE + 1);
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
similarity index 64%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
index f7f16a4..67fecb4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/ShelfNotificationIconViewStoreKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt
@@ -16,9 +16,22 @@
 
 package com.android.systemui.statusbar.notification.icon.ui.viewbinder
 
+import com.android.systemui.common.ui.configurationState
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection
+import com.android.systemui.statusbar.ui.systemBarUtilsState
+
+val Kosmos.notificationIconContainerShelfViewBinder by Fixture {
+    NotificationIconContainerShelfViewBinder(
+        notificationIconContainerShelfViewModel,
+        configurationState,
+        systemBarUtilsState,
+        statusBarIconViewBindingFailureTracker,
+        shelfNotificationIconViewStore,
+    )
+}
 
 val Kosmos.shelfNotificationIconViewStore by Fixture {
     ShelfNotificationIconViewStore(notifCollection = notifCollection)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
index 988172c..b906b60 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelKosmos.kt
@@ -18,7 +18,6 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.activatableNotificationViewModel
 import com.android.systemui.statusbar.notification.shelf.domain.interactor.notificationShelfInteractor
 
@@ -26,6 +25,5 @@
     NotificationShelfViewModel(
         interactor = notificationShelfInteractor,
         activatableViewModel = activatableNotificationViewModel,
-        icons = notificationIconContainerShelfViewModel,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
index ca5b401..04716b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt
@@ -22,11 +22,9 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.shelfNotificationIconViewStore
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.statusBarIconViewBindingFailureTracker
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel
 import com.android.systemui.statusbar.phone.notificationIconAreaController
-import com.android.systemui.statusbar.ui.systemBarUtilsState
 
 val Kosmos.notificationListViewBinder by Fixture {
     NotificationListViewBinder(
@@ -35,9 +33,7 @@
         configuration = configurationState,
         falsingManager = falsingManager,
         iconAreaController = notificationIconAreaController,
-        iconViewBindingFailureTracker = statusBarIconViewBindingFailureTracker,
         metricsLogger = metricsLogger,
-        shelfIconViewStore = shelfNotificationIconViewStore,
-        systemBarUtilsState = systemBarUtilsState,
+        nicBinder = notificationIconContainerShelfViewBinder,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
index 42c77aa..4e2dc7a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorKosmos.kt
@@ -47,6 +47,7 @@
             broadcastDispatcher = broadcastDispatcher,
             keyguardUpdateMonitor = keyguardUpdateMonitor,
             backgroundDispatcher = testDispatcher,
+            mainDispatcher = testDispatcher,
             activityManager = activityManager,
             refreshUsersScheduler = refreshUsersScheduler,
             guestUserInteractor = guestUserInteractor,
diff --git a/services/tests/media/OWNERS b/services/tests/media/OWNERS
new file mode 100644
index 0000000..160767a6
--- /dev/null
+++ b/services/tests/media/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 137631
+include platform/frameworks/av:/media/janitors/media_solutions_OWNERS
diff --git a/services/tests/media/mediarouterservicetest/Android.bp b/services/tests/media/mediarouterservicetest/Android.bp
new file mode 100644
index 0000000..aed3af6
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/Android.bp
@@ -0,0 +1,39 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "MediaRouterServiceTests",
+    srcs: [
+        "src/**/*.java",
+    ],
+
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.rules",
+        "androidx.test.runner",
+        "compatibility-device-util-axt",
+        "junit",
+        "platform-test-annotations",
+        "services.core",
+        "truth",
+    ],
+
+    platform_apis: true,
+
+    test_suites: [
+        // "device-tests",
+        "general-tests",
+    ],
+
+    certificate: "platform",
+    dxflags: ["--multi-dex"],
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/services/tests/media/mediarouterservicetest/AndroidManifest.xml b/services/tests/media/mediarouterservicetest/AndroidManifest.xml
new file mode 100644
index 0000000..fe65f86
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+     package="com.android.server.media.tests">
+
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
+
+    <application android:testOnly="true" android:debuggable="true">
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+         android:targetPackage="com.android.server.media.tests"
+         android:label="Frameworks Services Tests"/>
+</manifest>
diff --git a/services/tests/media/mediarouterservicetest/AndroidTest.xml b/services/tests/media/mediarouterservicetest/AndroidTest.xml
new file mode 100644
index 0000000..b065681
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<configuration description="Runs MediaRouter Service tests.">
+    <option name="test-tag" value="MediaRouterServiceTests" />
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="MediaRouterServiceTests.apk"/>
+        <option name="install-arg" value="-t" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.server.media.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
new file mode 100644
index 0000000..6f9b6fa
--- /dev/null
+++ b/services/tests/media/mediarouterservicetest/src/com/android/server/media/AudioPoliciesDeviceRouteControllerTest.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2023 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.media;
+
+import static com.android.server.media.AudioRoutingUtils.ATTRIBUTES_MEDIA;
+import static com.android.server.media.AudioRoutingUtils.getMediaAudioProductStrategy;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.res.Resources;
+import android.media.AudioDeviceAttributes;
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
+import android.media.AudioDevicePort;
+import android.media.AudioManager;
+import android.media.AudioSystem;
+import android.media.MediaRoute2Info;
+import android.media.audiopolicy.AudioProductStrategy;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class AudioPoliciesDeviceRouteControllerTest {
+
+    private static final String FAKE_ROUTE_NAME = "fake name";
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_SPEAKER, "name_builtin", /* address= */ null);
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_WIRED_HEADSET, "name_wired_hs", /* address= */ null);
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "name_a2dp", /* address= */ "12:34:45");
+
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_EARPIECE, /* name= */ null, /* address= */ null);
+
+    private static final AudioDeviceInfo FAKE_AUDIO_DEVICE_NO_NAME =
+            createAudioDeviceInfo(
+                    AudioSystem.DEVICE_OUT_DGTL_DOCK_HEADSET,
+                    /* name= */ null,
+                    /* address= */ null);
+
+    private AudioDeviceInfo mSelectedAudioDeviceInfo;
+    private Set<AudioDeviceInfo> mAvailableAudioDeviceInfos;
+    @Mock private AudioManager mMockAudioManager;
+    @Mock private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
+    private AudioPoliciesDeviceRouteController mControllerUnderTest;
+    private AudioDeviceCallback mAudioDeviceCallback;
+    private AudioProductStrategy mMediaAudioProductStrategy;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        Resources mockResources = Mockito.mock(Resources.class);
+        when(mockResources.getText(anyInt())).thenReturn(FAKE_ROUTE_NAME);
+        Context realContext = InstrumentationRegistry.getInstrumentation().getContext();
+        Context mockContext = Mockito.mock(Context.class);
+        when(mockContext.getResources()).thenReturn(mockResources);
+        // The bluetooth stack needs the application info, but we cannot use a spy because the
+        // concrete class is package private, so we just return the application info through the
+        // mock.
+        when(mockContext.getApplicationInfo()).thenReturn(realContext.getApplicationInfo());
+
+        // Setup the initial state so that the route controller is created in a sensible state.
+        mSelectedAudioDeviceInfo = FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER;
+        mAvailableAudioDeviceInfos = Set.of(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER);
+        updateMockAudioManagerState();
+        mMediaAudioProductStrategy = getMediaAudioProductStrategy();
+
+        BluetoothAdapter btAdapter =
+                realContext.getSystemService(BluetoothManager.class).getAdapter();
+        mControllerUnderTest =
+                new AudioPoliciesDeviceRouteController(
+                        mockContext,
+                        mMockAudioManager,
+                        Looper.getMainLooper(),
+                        mMediaAudioProductStrategy,
+                        btAdapter,
+                        mOnDeviceRouteChangedListener);
+        mControllerUnderTest.start(UserHandle.CURRENT_OR_SELF);
+
+        ArgumentCaptor<AudioDeviceCallback> deviceCallbackCaptor =
+                ArgumentCaptor.forClass(AudioDeviceCallback.class);
+        verify(mMockAudioManager)
+                .registerAudioDeviceCallback(deviceCallbackCaptor.capture(), any());
+        mAudioDeviceCallback = deviceCallbackCaptor.getValue();
+
+        // We clear any invocations during setup.
+        clearInvocations(mOnDeviceRouteChangedListener);
+    }
+
+    @Test
+    public void getSelectedRoute_afterDevicesConnect_returnsRightSelectedRoute() {
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged();
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+    }
+
+    @Test
+    public void getSelectedRoute_afterDeviceRemovals_returnsExpectedRoutes() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        verify(mOnDeviceRouteChangedListener).onDeviceRouteChanged();
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mOnDeviceRouteChangedListener, times(2)).onDeviceRouteChanged();
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        removeAvailableAudioDeviceInfos(
+                /* newSelectedDevice= */ null,
+                /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
+
+        removeAvailableAudioDeviceInfos(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER,
+                /* devicesToRemove...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+    }
+
+    @Test
+    public void onAudioDevicesAdded_clearsAudioRoutingPoliciesCorrectly() {
+        clearInvocations(mMockAudioManager);
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE);
+        verifyNoMoreInteractions(mMockAudioManager);
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP);
+        verify(mMockAudioManager).removePreferredDeviceForStrategy(mMediaAudioProductStrategy);
+    }
+
+    @Test
+    public void getAvailableDevices_ignoresInvalidMediaOutputs() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ null, // Selected device doesn't change.
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_BUILTIN_EARPIECE);
+        verifyNoMoreInteractions(mOnDeviceRouteChangedListener);
+        assertThat(
+                        mControllerUnderTest.getAvailableRoutes().stream()
+                                .map(MediaRoute2Info::getType)
+                                .toList())
+                .containsExactly(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        assertThat(mControllerUnderTest.getSelectedRoute().getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+    }
+
+    @Test
+    public void transferTo_setsTheExpectedRoutingPolicy() {
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_BLUETOOTH_A2DP,
+                FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+        MediaRoute2Info builtInSpeakerRoute =
+                getAvailableRouteWithType(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        mControllerUnderTest.transferTo(builtInSpeakerRoute.getId());
+        verify(mMockAudioManager)
+                .setPreferredDeviceForStrategy(
+                        mMediaAudioProductStrategy,
+                        createAudioDeviceAttribute(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER));
+
+        MediaRoute2Info wiredHeadsetRoute =
+                getAvailableRouteWithType(MediaRoute2Info.TYPE_WIRED_HEADSET);
+        mControllerUnderTest.transferTo(wiredHeadsetRoute.getId());
+        verify(mMockAudioManager)
+                .setPreferredDeviceForStrategy(
+                        mMediaAudioProductStrategy,
+                        createAudioDeviceAttribute(AudioDeviceInfo.TYPE_WIRED_HEADSET));
+    }
+
+    @Test
+    public void updateVolume_propagatesCorrectlyToRouteInfo() {
+        when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(2);
+        when(mMockAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(3);
+        when(mMockAudioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)).thenReturn(1);
+        when(mMockAudioManager.isVolumeFixed()).thenReturn(false);
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_INFO_WIRED_HEADSET);
+
+        MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(selectedRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADSET);
+        assertThat(selectedRoute.getVolume()).isEqualTo(2);
+        assertThat(selectedRoute.getVolumeMax()).isEqualTo(3);
+        assertThat(selectedRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE);
+
+        MediaRoute2Info onlyTransferrableRoute =
+                mControllerUnderTest.getAvailableRoutes().stream()
+                        .filter(it -> !it.equals(selectedRoute))
+                        .findAny()
+                        .orElseThrow();
+        assertThat(onlyTransferrableRoute.getType())
+                .isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
+        assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolumeMax()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolume()).isEqualTo(0);
+        assertThat(onlyTransferrableRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED);
+
+        when(mMockAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)).thenReturn(0);
+        when(mMockAudioManager.isVolumeFixed()).thenReturn(true);
+        mControllerUnderTest.updateVolume(0);
+        MediaRoute2Info newSelectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(newSelectedRoute.getVolume()).isEqualTo(0);
+        assertThat(newSelectedRoute.getVolumeHandling())
+                .isEqualTo(MediaRoute2Info.PLAYBACK_VOLUME_FIXED);
+    }
+
+    @Test
+    public void getAvailableRoutes_whenNoProductNameIsProvided_usesTypeToPopulateName() {
+        assertThat(mControllerUnderTest.getSelectedRoute().getName().toString())
+                .isEqualTo(FAKE_AUDIO_DEVICE_INFO_BUILTIN_SPEAKER.getProductName().toString());
+
+        addAvailableAudioDeviceInfo(
+                /* newSelectedDevice= */ FAKE_AUDIO_DEVICE_NO_NAME,
+                /* newAvailableDevices...= */ FAKE_AUDIO_DEVICE_NO_NAME);
+
+        MediaRoute2Info selectedRoute = mControllerUnderTest.getSelectedRoute();
+        assertThat(selectedRoute.getName().toString()).isEqualTo(FAKE_ROUTE_NAME);
+    }
+
+    // Internal methods.
+
+    @NonNull
+    private MediaRoute2Info getAvailableRouteWithType(int type) {
+        return mControllerUnderTest.getAvailableRoutes().stream()
+                .filter(it -> it.getType() == type)
+                .findFirst()
+                .orElseThrow();
+    }
+
+    private void addAvailableAudioDeviceInfo(
+            @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... newAvailableDevices) {
+        Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos);
+        newAvailableDeviceInfos.addAll(List.of(newAvailableDevices));
+        mAvailableAudioDeviceInfos = newAvailableDeviceInfos;
+        if (newSelectedDevice != null) {
+            mSelectedAudioDeviceInfo = newSelectedDevice;
+        }
+        updateMockAudioManagerState();
+        mAudioDeviceCallback.onAudioDevicesAdded(newAvailableDevices);
+    }
+
+    private void removeAvailableAudioDeviceInfos(
+            @Nullable AudioDeviceInfo newSelectedDevice, AudioDeviceInfo... devicesToRemove) {
+        Set<AudioDeviceInfo> newAvailableDeviceInfos = new HashSet<>(mAvailableAudioDeviceInfos);
+        List.of(devicesToRemove).forEach(newAvailableDeviceInfos::remove);
+        mAvailableAudioDeviceInfos = newAvailableDeviceInfos;
+        if (newSelectedDevice != null) {
+            mSelectedAudioDeviceInfo = newSelectedDevice;
+        }
+        updateMockAudioManagerState();
+        mAudioDeviceCallback.onAudioDevicesRemoved(devicesToRemove);
+    }
+
+    private void updateMockAudioManagerState() {
+        when(mMockAudioManager.getDevicesForAttributes(ATTRIBUTES_MEDIA))
+                .thenReturn(
+                        List.of(createAudioDeviceAttribute(mSelectedAudioDeviceInfo.getType())));
+        when(mMockAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
+                .thenReturn(mAvailableAudioDeviceInfos.toArray(new AudioDeviceInfo[0]));
+    }
+
+    private static AudioDeviceAttributes createAudioDeviceAttribute(int type) {
+        // Address is unused.
+        return new AudioDeviceAttributes(
+                AudioDeviceAttributes.ROLE_OUTPUT, type, /* address= */ "");
+    }
+
+    private static AudioDeviceInfo createAudioDeviceInfo(
+            int type, @NonNull String name, @NonNull String address) {
+        return new AudioDeviceInfo(AudioDevicePort.createForTesting(type, name, address));
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 650c473..d24500d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -28,6 +28,8 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
+import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
 import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_APPLIED_CONSTRAINTS;
@@ -212,6 +214,7 @@
         JobStatus js = JobStatus.createFromJobInfo(
                 jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag);
         js.enqueueTime = FROZEN_TIME;
+        js.setStandbyBucket(ACTIVE_INDEX);
         if (js.hasFlexibilityConstraint()) {
             js.setNumAppliedFlexibleConstraints(Integer.bitCount(
                     mFlexibilityController.getRelevantAppliedConstraintsLocked(js)));
@@ -847,14 +850,75 @@
     }
 
     @Test
+    public void testAllowlistedAppBypass() {
+        JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
+        JobStatus jsDefault = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT));
+        JobStatus jsLow = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW));
+        JobStatus jsMin = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN));
+        jsHigh.setStandbyBucket(EXEMPTED_INDEX);
+        jsDefault.setStandbyBucket(EXEMPTED_INDEX);
+        jsLow.setStandbyBucket(EXEMPTED_INDEX);
+        jsMin.setStandbyBucket(EXEMPTED_INDEX);
+
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+    }
+
+    @Test
+    public void testForegroundAppBypass() {
+        JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_HIGH));
+        JobStatus jsDefault = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT));
+        JobStatus jsLow = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_LOW));
+        JobStatus jsMin = createJobStatus("testAllowlistedAppBypass",
+                createJob(0).setPriority(JobInfo.PRIORITY_MIN));
+
+        when(mJobSchedulerService.getUidBias(mSourceUid)).thenReturn(JobInfo.BIAS_DEFAULT);
+        synchronized (mFlexibilityController.mLock) {
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+
+        when(mJobSchedulerService.getUidBias(mSourceUid))
+                .thenReturn(JobInfo.BIAS_BOUND_FOREGROUND_SERVICE);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+
+        when(mJobSchedulerService.getUidBias(mSourceUid))
+                .thenReturn(JobInfo.BIAS_FOREGROUND_SERVICE);
+        synchronized (mFlexibilityController.mLock) {
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh));
+            assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow));
+            assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin));
+        }
+    }
+
+    @Test
     public void testTopAppBypass() {
-        JobInfo.Builder jb = createJob(0);
+        JobInfo.Builder jb = createJob(0).setPriority(JobInfo.PRIORITY_MIN);
         JobStatus js = createJobStatus("testTopAppBypass", jb);
         mJobStore.add(js);
 
         // Needed because if before and after Uid bias is the same, nothing happens.
         when(mJobSchedulerService.getUidBias(mSourceUid))
-                .thenReturn(JobInfo.BIAS_FOREGROUND_SERVICE);
+                .thenReturn(JobInfo.BIAS_DEFAULT);
 
         synchronized (mFlexibilityController.mLock) {
             mFlexibilityController.maybeStartTrackingJobLocked(js, null);
@@ -865,7 +929,7 @@
             assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
             assertTrue(js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE));
 
-            setUidBias(mSourceUid, JobInfo.BIAS_FOREGROUND_SERVICE);
+            setUidBias(mSourceUid, JobInfo.BIAS_SYNC_INITIALIZATION);
 
             assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(js));
             assertFalse(js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE));