Merge "Remove unused scaffolding for partial shelf overflow" into tm-qpr-dev
diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java
index 794c694..c5dedb3 100644
--- a/core/java/android/app/LocaleManager.java
+++ b/core/java/android/app/LocaleManager.java
@@ -173,7 +173,7 @@
     @TestApi
     public void setSystemLocales(@NonNull LocaleList locales) {
         try {
-            Configuration conf = ActivityManager.getService().getConfiguration();
+            Configuration conf = new Configuration();
             conf.setLocales(locales);
             ActivityManager.getService().updatePersistentConfiguration(conf);
         } catch (RemoteException e) {
diff --git a/core/java/com/android/internal/app/LocalePicker.java b/core/java/com/android/internal/app/LocalePicker.java
index 3c53d07..7dd1d26 100644
--- a/core/java/com/android/internal/app/LocalePicker.java
+++ b/core/java/com/android/internal/app/LocalePicker.java
@@ -311,8 +311,7 @@
 
         try {
             final IActivityManager am = ActivityManager.getService();
-            final Configuration config = am.getConfiguration();
-
+            final Configuration config = new Configuration();
             config.setLocales(locales);
             config.userSetLocale = true;
 
diff --git a/core/res/res/color/letterbox_background.xml b/core/res/res/color/letterbox_background.xml
new file mode 100644
index 0000000..955948a
--- /dev/null
+++ b/core/res/res/color/letterbox_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@color/system_neutral1_500" android:lStar="5" />
+</selector>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 881d499..aa9a949 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5239,7 +5239,7 @@
             but isn't supported on the device or both dark scrim alpha and blur radius aren't
             provided.
      -->
-    <color name="config_letterboxBackgroundColor">@android:color/system_neutral2_900</color>
+    <color name="config_letterboxBackgroundColor">@color/letterbox_background</color>
 
     <!-- Horizontal position of a center of the letterboxed app window.
         0 corresponds to the left side of the screen and 1 to the right side. If given value < 0
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 00be5a6e..77284c41 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -109,6 +109,12 @@
         return (mSplitRule instanceof SplitPlaceholderRule);
     }
 
+    @NonNull
+    SplitInfo toSplitInfo() {
+        return new SplitInfo(mPrimaryContainer.toActivityStack(),
+                mSecondaryContainer.toActivityStack(), mSplitAttributes);
+    }
+
     static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
         final boolean isPlaceholderContainer = splitRule instanceof SplitPlaceholderRule;
         final boolean shouldFinishPrimaryWithSecondary = (splitRule instanceof SplitPairRule)
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index bf7326a..1d513e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -1422,6 +1422,11 @@
     @GuardedBy("mLock")
     void updateContainer(@NonNull WindowContainerTransaction wct,
             @NonNull TaskFragmentContainer container) {
+        if (!container.getTaskContainer().isVisible()) {
+            // Wait until the Task is visible to avoid unnecessary update when the Task is still in
+            // background.
+            return;
+        }
         if (launchPlaceholderIfNecessary(wct, container)) {
             // Placeholder was launched, the positions will be updated when the activity is added
             // to the secondary container.
@@ -1643,16 +1648,14 @@
     /**
      * Notifies listeners about changes to split states if necessary.
      */
+    @VisibleForTesting
     @GuardedBy("mLock")
-    private void updateCallbackIfNecessary() {
-        if (mEmbeddingCallback == null) {
+    void updateCallbackIfNecessary() {
+        if (mEmbeddingCallback == null || !readyToReportToClient()) {
             return;
         }
-        if (!allActivitiesCreated()) {
-            return;
-        }
-        List<SplitInfo> currentSplitStates = getActiveSplitStates();
-        if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) {
+        final List<SplitInfo> currentSplitStates = getActiveSplitStates();
+        if (mLastReportedSplitStates.equals(currentSplitStates)) {
             return;
         }
         mLastReportedSplitStates.clear();
@@ -1661,48 +1664,27 @@
     }
 
     /**
-     * @return a list of descriptors for currently active split states. If the value returned is
-     * null, that indicates that the active split states are in an intermediate state and should
-     * not be reported.
+     * Returns a list of descriptors for currently active split states.
      */
     @GuardedBy("mLock")
-    @Nullable
+    @NonNull
     private List<SplitInfo> getActiveSplitStates() {
-        List<SplitInfo> splitStates = new ArrayList<>();
+        final List<SplitInfo> splitStates = new ArrayList<>();
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<SplitContainer> splitContainers = mTaskContainers.valueAt(i)
-                    .mSplitContainers;
-            for (SplitContainer container : splitContainers) {
-                if (container.getPrimaryContainer().isEmpty()
-                        || container.getSecondaryContainer().isEmpty()) {
-                    // We are in an intermediate state because either the split container is about
-                    // to be removed or the primary or secondary container are about to receive an
-                    // activity.
-                    return null;
-                }
-                final ActivityStack primaryContainer = container.getPrimaryContainer()
-                        .toActivityStack();
-                final ActivityStack secondaryContainer = container.getSecondaryContainer()
-                        .toActivityStack();
-                final SplitInfo splitState = new SplitInfo(primaryContainer, secondaryContainer,
-                        container.getSplitAttributes());
-                splitStates.add(splitState);
-            }
+            mTaskContainers.valueAt(i).getSplitStates(splitStates);
         }
         return splitStates;
     }
 
     /**
-     * Checks if all activities that are registered with the containers have already appeared in
-     * the client.
+     * Whether we can now report the split states to the client.
      */
-    private boolean allActivitiesCreated() {
+    @GuardedBy("mLock")
+    private boolean readyToReportToClient() {
         for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-            final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i).mContainers;
-            for (TaskFragmentContainer container : containers) {
-                if (!container.taskInfoActivityCountMatchesCreated()) {
-                    return false;
-                }
+            if (mTaskContainers.valueAt(i).isInIntermediateState()) {
+                // If any Task is in an intermediate state, wait for the server update.
+                return false;
             }
         }
         return true;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index 00943f2d..231da05 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -221,6 +221,24 @@
         return mContainers.indexOf(child);
     }
 
+    /** Whether the Task is in an intermediate state waiting for the server update.*/
+    boolean isInIntermediateState() {
+        for (TaskFragmentContainer container : mContainers) {
+            if (container.isInIntermediateState()) {
+                // We are in an intermediate state to wait for server update on this TaskFragment.
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */
+    void getSplitStates(@NonNull List<SplitInfo> outSplitStates) {
+        for (SplitContainer container : mSplitContainers) {
+            outSplitStates.add(container.toSplitInfo());
+        }
+    }
+
     /**
      * A wrapper class which contains the display ID and {@link Configuration} of a
      * {@link TaskContainer}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
index ef5ea56..a7d47ef 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java
@@ -161,7 +161,7 @@
         // The position should be 0-based as we will post translate in
         // TaskFragmentAnimationAdapter#onAnimationUpdate
         final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
-                0, 0);
+                startBounds.top - endBounds.top, 0);
         endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
         endSet.addAnimation(endTranslate);
         // The end leash is resizing, we should update the window crop based on the clip rect.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 18712ae..71b8840 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -166,16 +166,34 @@
         return allActivities;
     }
 
-    /**
-     * Checks if the count of activities from the same process in task fragment info corresponds to
-     * the ones created and available on the client side.
-     */
-    boolean taskInfoActivityCountMatchesCreated() {
+    /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/
+    boolean isInIntermediateState() {
         if (mInfo == null) {
-            return false;
+            // Haven't received onTaskFragmentAppeared event.
+            return true;
         }
-        return mPendingAppearedActivities.isEmpty()
-                && mInfo.getActivities().size() == collectNonFinishingActivities().size();
+        if (mInfo.isEmpty()) {
+            // Empty TaskFragment will be removed or will have activity launched into it soon.
+            return true;
+        }
+        if (!mPendingAppearedActivities.isEmpty()) {
+            // Reparented activity hasn't appeared.
+            return true;
+        }
+        // Check if there is any reported activity that is no longer alive.
+        for (IBinder token : mInfo.getActivities()) {
+            final Activity activity = mController.getActivity(token);
+            if (activity == null && !mTaskContainer.isVisible()) {
+                // Activity can be null if the activity is not attached to process yet. That can
+                // happen when the activity is started in background.
+                continue;
+            }
+            if (activity == null || activity.isFinishing()) {
+                // One of the reported activity is no longer alive, wait for the server update.
+                return true;
+            }
+        }
+        return false;
     }
 
     @NonNull
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index a403031..87d0278 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -102,6 +102,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Test class for {@link SplitController}.
@@ -132,6 +133,8 @@
 
     private SplitController mSplitController;
     private SplitPresenter mSplitPresenter;
+    private Consumer<List<SplitInfo>> mEmbeddingCallback;
+    private List<SplitInfo> mSplitInfos;
     private TransactionManager mTransactionManager;
 
     @Before
@@ -141,9 +144,16 @@
                 .getCurrentWindowLayoutInfo(anyInt(), any());
         mSplitController = new SplitController(mWindowLayoutComponent);
         mSplitPresenter = mSplitController.mPresenter;
+        mSplitInfos = new ArrayList<>();
+        mEmbeddingCallback = splitInfos -> {
+            mSplitInfos.clear();
+            mSplitInfos.addAll(splitInfos);
+        };
+        mSplitController.setSplitInfoCallback(mEmbeddingCallback);
         mTransactionManager = mSplitController.mTransactionManager;
         spyOn(mSplitController);
         spyOn(mSplitPresenter);
+        spyOn(mEmbeddingCallback);
         spyOn(mTransactionManager);
         doNothing().when(mSplitPresenter).applyTransaction(any(), anyInt(), anyBoolean());
         final Configuration activityConfig = new Configuration();
@@ -329,6 +339,30 @@
     }
 
     @Test
+    public void testUpdateContainer_skipIfTaskIsInvisible() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+        final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+        final TaskFragmentContainer taskFragmentContainer = taskContainer.mContainers.get(0);
+        spyOn(taskContainer);
+
+        // No update when the Task is invisible.
+        clearInvocations(mSplitPresenter);
+        doReturn(false).when(taskContainer).isVisible();
+        mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+
+        verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+
+        // Update the split when the Task is visible.
+        doReturn(true).when(taskContainer).isVisible();
+        mSplitController.updateContainer(mTransaction, taskFragmentContainer);
+
+        verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
+                taskFragmentContainer, mTransaction);
+    }
+
+    @Test
     public void testOnStartActivityResultError() {
         final Intent intent = new Intent();
         final TaskContainer taskContainer = createTestTaskContainer();
@@ -1162,14 +1196,69 @@
                         new WindowMetrics(TASK_BOUNDS, WindowInsets.CONSUMED)));
     }
 
+    @Test
+    public void testSplitInfoCallback_reportSplit() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+
+        mSplitController.updateCallbackIfNecessary();
+        assertEquals(1, mSplitInfos.size());
+        final SplitInfo splitInfo = mSplitInfos.get(0);
+        assertEquals(1, splitInfo.getPrimaryActivityStack().getActivities().size());
+        assertEquals(1, splitInfo.getSecondaryActivityStack().getActivities().size());
+        assertEquals(r0, splitInfo.getPrimaryActivityStack().getActivities().get(0));
+        assertEquals(r1, splitInfo.getSecondaryActivityStack().getActivities().get(0));
+    }
+
+    @Test
+    public void testSplitInfoCallback_reportSplitInMultipleTasks() {
+        final int taskId0 = 1;
+        final int taskId1 = 2;
+        final Activity r0 = createMockActivity(taskId0);
+        final Activity r1 = createMockActivity(taskId0);
+        final Activity r2 = createMockActivity(taskId1);
+        final Activity r3 = createMockActivity(taskId1);
+        addSplitTaskFragments(r0, r1);
+        addSplitTaskFragments(r2, r3);
+
+        mSplitController.updateCallbackIfNecessary();
+        assertEquals(2, mSplitInfos.size());
+    }
+
+    @Test
+    public void testSplitInfoCallback_doNotReportIfInIntermediateState() {
+        final Activity r0 = createMockActivity();
+        final Activity r1 = createMockActivity();
+        addSplitTaskFragments(r0, r1);
+        final TaskFragmentContainer tf0 = mSplitController.getContainerWithActivity(r0);
+        final TaskFragmentContainer tf1 = mSplitController.getContainerWithActivity(r1);
+        spyOn(tf0);
+        spyOn(tf1);
+
+        // Do not report if activity has not appeared in the TaskFragmentContainer in split.
+        doReturn(true).when(tf0).isInIntermediateState();
+        mSplitController.updateCallbackIfNecessary();
+        verify(mEmbeddingCallback, never()).accept(any());
+
+        doReturn(false).when(tf0).isInIntermediateState();
+        mSplitController.updateCallbackIfNecessary();
+        verify(mEmbeddingCallback).accept(any());
+    }
+
     /** Creates a mock activity in the organizer process. */
     private Activity createMockActivity() {
+        return createMockActivity(TASK_ID);
+    }
+
+    /** Creates a mock activity in the organizer process. */
+    private Activity createMockActivity(int taskId) {
         final Activity activity = mock(Activity.class);
         doReturn(mActivityResources).when(activity).getResources();
         final IBinder activityToken = new Binder();
         doReturn(activityToken).when(activity).getActivityToken();
         doReturn(activity).when(mSplitController).getActivity(activityToken);
-        doReturn(TASK_ID).when(activity).getTaskId();
+        doReturn(taskId).when(activity).getTaskId();
         doReturn(new ActivityInfo()).when(activity).getActivityInfo();
         doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
         return activity;
@@ -1177,7 +1266,8 @@
 
     /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
     private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
-        final TaskFragmentContainer container = mSplitController.newContainer(activity, TASK_ID);
+        final TaskFragmentContainer container = mSplitController.newContainer(activity,
+                activity.getTaskId());
         setupTaskFragmentInfo(container, activity);
         return container;
     }
@@ -1268,7 +1358,7 @@
 
         // We need to set those in case we are not respecting clear top.
         // TODO(b/231845476) we should always respect clearTop.
-        final int windowingMode = mSplitController.getTaskContainer(TASK_ID)
+        final int windowingMode = mSplitController.getTaskContainer(primaryContainer.getTaskId())
                 .getWindowingModeForSplitTaskFragment(TASK_BOUNDS);
         primaryContainer.setLastRequestedWindowingMode(windowingMode);
         secondaryContainer.setLastRequestedWindowingMode(windowingMode);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 35415d8..d43c471 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -334,6 +334,70 @@
         assertFalse(container.hasActivity(mActivity.getActivityToken()));
     }
 
+    @Test
+    public void testIsInIntermediateState() {
+        // True if no info set.
+        final TaskContainer taskContainer = createTestTaskContainer();
+        final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
+                mIntent, taskContainer, mController);
+        spyOn(taskContainer);
+        doReturn(true).when(taskContainer).isVisible();
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // True if empty info set.
+        final List<IBinder> activities = new ArrayList<>();
+        doReturn(activities).when(mInfo).getActivities();
+        doReturn(true).when(mInfo).isEmpty();
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if info is not empty.
+        doReturn(false).when(mInfo).isEmpty();
+        container.setInfo(mTransaction, mInfo);
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+
+        // True if there is pending appeared activity.
+        container.addPendingAppearedActivity(mActivity);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // True if the activity is finishing.
+        activities.add(mActivity.getActivityToken());
+        doReturn(true).when(mActivity).isFinishing();
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if the activity is not finishing.
+        doReturn(false).when(mActivity).isFinishing();
+        container.setInfo(mTransaction, mInfo);
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+
+        // True if there is a token that can't find associated activity.
+        activities.clear();
+        activities.add(new Binder());
+        container.setInfo(mTransaction, mInfo);
+
+        assertTrue(container.isInIntermediateState());
+        assertTrue(taskContainer.isInIntermediateState());
+
+        // False if there is a token that can't find associated activity when the Task is invisible.
+        doReturn(false).when(taskContainer).isVisible();
+
+        assertFalse(container.isInIntermediateState());
+        assertFalse(taskContainer.isInIntermediateState());
+    }
+
     /** Creates a mock activity in the organizer process. */
     private Activity createMockActivity() {
         final Activity activity = mock(Activity.class);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index 490975c..921861a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -303,6 +303,7 @@
         // 3. Animate the TaskFragment using Activity Change info (start/end bounds).
         // This is because the TaskFragment surface/change won't contain the Activity's before its
         // reparent.
+        Animation changeAnimation = null;
         for (TransitionInfo.Change change : info.getChanges()) {
             if (change.getMode() != TRANSIT_CHANGE
                     || change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
@@ -325,8 +326,14 @@
                 }
             }
 
+            // There are two animations in the array. The first one is for the start leash
+            // (snapshot), and the second one is for the end leash (TaskFragment).
             final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change,
                     boundsAnimationChange.getEndAbsBounds());
+            // Keep track as we might need to add background color for the animation.
+            // Although there may be multiple change animation, record one of them is sufficient
+            // because the background color will be added to the root leash for the whole animation.
+            changeAnimation = animations[1];
 
             // Create a screenshot based on change, but attach it to the top of the
             // boundsAnimationChange.
@@ -345,6 +352,9 @@
                     animations[1], boundsAnimationChange));
         }
 
+        // If there is no corresponding open/close window with the change, we should show background
+        // color to cover the empty part of the screen.
+        boolean shouldShouldBackgroundColor = true;
         // Handle the other windows that don't have bounds change in the same transition.
         for (TransitionInfo.Change change : info.getChanges()) {
             if (handledChanges.contains(change)) {
@@ -359,11 +369,20 @@
                 animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
             } else if (Transitions.isClosingType(change.getMode())) {
                 animation = mAnimationSpec.createChangeBoundsCloseAnimation(change);
+                shouldShouldBackgroundColor = false;
             } else {
                 animation = mAnimationSpec.createChangeBoundsOpenAnimation(change);
+                shouldShouldBackgroundColor = false;
             }
             adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change));
         }
+
+        if (shouldShouldBackgroundColor && changeAnimation != null) {
+            // Change animation may leave part of the screen empty. Show background color to cover
+            // that.
+            changeAnimation.setShowBackdrop(true);
+        }
+
         return adapters;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 58b2366..2bb7369 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -158,7 +158,7 @@
         // The position should be 0-based as we will post translate in
         // ActivityEmbeddingAnimationAdapter#onAnimationUpdate
         final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
-                0, 0);
+                startBounds.top - endBounds.top, 0);
         endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
         endSet.addAnimation(endTranslate);
         // The end leash is resizing, we should update the window crop based on the clip rect.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 9c2c2fa..af79386 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -619,12 +619,13 @@
         // Animation length is already expected to be scaled.
         va.overrideDurationScale(1.0f);
         va.setDuration(anim.computeDurationHint());
-        va.addUpdateListener(animation -> {
+        final ValueAnimator.AnimatorUpdateListener updateListener = animation -> {
             final long currentPlayTime = Math.min(va.getDuration(), va.getCurrentPlayTime());
 
             applyTransformation(currentPlayTime, transaction, leash, anim, transformation, matrix,
                     position, cornerRadius, clipRect);
-        });
+        };
+        va.addUpdateListener(updateListener);
 
         final Runnable finisher = () -> {
             applyTransformation(va.getDuration(), transaction, leash, anim, transformation, matrix,
@@ -637,20 +638,30 @@
             });
         };
         va.addListener(new AnimatorListenerAdapter() {
+            // It is possible for the end/cancel to be called more than once, which may cause
+            // issues if the animating surface has already been released. Track the finished
+            // state here to skip duplicate callbacks. See b/252872225.
             private boolean mFinished = false;
 
             @Override
             public void onAnimationEnd(Animator animation) {
-                if (mFinished) return;
-                mFinished = true;
-                finisher.run();
+                onFinish();
             }
 
             @Override
             public void onAnimationCancel(Animator animation) {
+                onFinish();
+            }
+
+            private void onFinish() {
                 if (mFinished) return;
                 mFinished = true;
                 finisher.run();
+                // The update listener can continue to be called after the animation has ended if
+                // end() is called manually again before the finisher removes the animation.
+                // Remove it manually here to prevent animating a released surface.
+                // See b/252872225.
+                va.removeUpdateListener(updateListener);
             }
         });
         animations.add(va);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index eb53ea1..950ee21 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -758,23 +758,16 @@
     }
 
     public boolean isBusy() {
-        for (CachedBluetoothDevice memberDevice : getMemberDevice()) {
-            if (isBusyState(memberDevice)) {
-                return true;
+        synchronized (mProfileLock) {
+            for (LocalBluetoothProfile profile : mProfiles) {
+                int status = getProfileConnectionState(profile);
+                if (status == BluetoothProfile.STATE_CONNECTING
+                        || status == BluetoothProfile.STATE_DISCONNECTING) {
+                    return true;
+                }
             }
+            return getBondState() == BluetoothDevice.BOND_BONDING;
         }
-        return isBusyState(this);
-    }
-
-    private boolean isBusyState(CachedBluetoothDevice device){
-        for (LocalBluetoothProfile profile : device.getProfiles()) {
-            int status = device.getProfileConnectionState(profile);
-            if (status == BluetoothProfile.STATE_CONNECTING
-                    || status == BluetoothProfile.STATE_DISCONNECTING) {
-                return true;
-            }
-        }
-        return device.getBondState() == BluetoothDevice.BOND_BONDING;
     }
 
     private boolean updateProfiles() {
@@ -920,7 +913,14 @@
 
     @Override
     public String toString() {
-        return mDevice.toString();
+        return "CachedBluetoothDevice ("
+                + "anonymizedAddress="
+                + mDevice.getAnonymizedAddress()
+                + ", name="
+                + getName()
+                + ", groupId="
+                + mGroupId
+                + ")";
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 8a9f9dd..fb861da 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -231,7 +231,7 @@
             if (DEBUG) {
                 Log.d(TAG, "Adding local Volume Control profile");
             }
-            mVolumeControlProfile = new VolumeControlProfile();
+            mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this);
             // Note: no event handler for VCP, only for being connectable.
             mProfileNameMap.put(VolumeControlProfile.NAME, mVolumeControlProfile);
         }
@@ -553,6 +553,10 @@
         return mCsipSetCoordinatorProfile;
     }
 
+    public VolumeControlProfile getVolumeControlProfile() {
+        return mVolumeControlProfile;
+    }
+
     /**
      * Fill in a list of LocalBluetoothProfile objects that are supported by
      * the local device and the remote device.
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
index 511df28..57867be 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
@@ -16,18 +16,91 @@
 
 package com.android.settingslib.bluetooth;
 
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
+import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothVolumeControl;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.RequiresApi;
 
 /**
  * VolumeControlProfile handles Bluetooth Volume Control Controller role
  */
 public class VolumeControlProfile implements LocalBluetoothProfile {
     private static final String TAG = "VolumeControlProfile";
+    private static boolean DEBUG = true;
     static final String NAME = "VCP";
     // Order of this profile in device profiles list
-    private static final int ORDINAL = 23;
+    private static final int ORDINAL = 1;
+
+    private Context mContext;
+    private final CachedBluetoothDeviceManager mDeviceManager;
+    private final LocalBluetoothProfileManager mProfileManager;
+
+    private BluetoothVolumeControl mService;
+    private boolean mIsProfileReady;
+
+    // These callbacks run on the main thread.
+    private final class VolumeControlProfileServiceListener
+            implements BluetoothProfile.ServiceListener {
+
+        @RequiresApi(Build.VERSION_CODES.S)
+        public void onServiceConnected(int profile, BluetoothProfile proxy) {
+            if (DEBUG) {
+                Log.d(TAG, "Bluetooth service connected");
+            }
+            mService = (BluetoothVolumeControl) proxy;
+            // We just bound to the service, so refresh the UI for any connected
+            // VolumeControlProfile devices.
+            List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+            while (!deviceList.isEmpty()) {
+                BluetoothDevice nextDevice = deviceList.remove(0);
+                CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
+                // we may add a new device here, but generally this should not happen
+                if (device == null) {
+                    if (DEBUG) {
+                        Log.d(TAG, "VolumeControlProfile found new device: " + nextDevice);
+                    }
+                    device = mDeviceManager.addDevice(nextDevice);
+                }
+                device.onProfileStateChanged(VolumeControlProfile.this,
+                        BluetoothProfile.STATE_CONNECTED);
+                device.refresh();
+            }
+
+            mProfileManager.callServiceConnectedListeners();
+            mIsProfileReady = true;
+        }
+
+        public void onServiceDisconnected(int profile) {
+            if (DEBUG) {
+                Log.d(TAG, "Bluetooth service disconnected");
+            }
+            mProfileManager.callServiceDisconnectedListeners();
+            mIsProfileReady = false;
+        }
+    }
+
+    VolumeControlProfile(Context context, CachedBluetoothDeviceManager deviceManager,
+            LocalBluetoothProfileManager profileManager) {
+        mContext = context;
+        mDeviceManager = deviceManager;
+        mProfileManager = profileManager;
+
+        BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
+                new VolumeControlProfile.VolumeControlProfileServiceListener(),
+                BluetoothProfile.VOLUME_CONTROL);
+    }
 
     @Override
     public boolean accessProfileEnabled() {
@@ -39,29 +112,70 @@
         return true;
     }
 
+    /**
+     * Get VolumeControlProfile devices matching connection states{
+     *
+     * @return Matching device list
+     * @code BluetoothProfile.STATE_CONNECTED,
+     * @code BluetoothProfile.STATE_CONNECTING,
+     * @code BluetoothProfile.STATE_DISCONNECTING}
+     */
+    public List<BluetoothDevice> getConnectedDevices() {
+        if (mService == null) {
+            return new ArrayList<BluetoothDevice>(0);
+        }
+        return mService.getDevicesMatchingConnectionStates(
+                new int[]{BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING,
+                        BluetoothProfile.STATE_DISCONNECTING});
+    }
+
     @Override
     public int getConnectionStatus(BluetoothDevice device) {
-        return BluetoothProfile.STATE_DISCONNECTED; // Settings app doesn't handle VCP
+        if (mService == null) {
+            return BluetoothProfile.STATE_DISCONNECTED;
+        }
+        return mService.getConnectionState(device);
     }
 
     @Override
     public boolean isEnabled(BluetoothDevice device) {
-        return false;
+        if (mService == null || device == null) {
+            return false;
+        }
+        return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
     }
 
     @Override
     public int getConnectionPolicy(BluetoothDevice device) {
-        return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; // Settings app doesn't handle VCP
+        if (mService == null || device == null) {
+            return CONNECTION_POLICY_FORBIDDEN;
+        }
+        return mService.getConnectionPolicy(device);
     }
 
     @Override
     public boolean setEnabled(BluetoothDevice device, boolean enabled) {
-        return false;
+        boolean isSuccessful = false;
+        if (mService == null || device == null) {
+            return false;
+        }
+        if (DEBUG) {
+            Log.d(TAG, device.getAnonymizedAddress() + " setEnabled: " + enabled);
+        }
+        if (enabled) {
+            if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
+                isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
+            }
+        } else {
+            isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
+        }
+
+        return isSuccessful;
     }
 
     @Override
     public boolean isProfileReady() {
-        return true;
+        return mIsProfileReady;
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index 1606540..2614644 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -92,7 +92,8 @@
             COMPLICATION_TYPE_AIR_QUALITY,
             COMPLICATION_TYPE_CAST_INFO,
             COMPLICATION_TYPE_HOME_CONTROLS,
-            COMPLICATION_TYPE_SMARTSPACE
+            COMPLICATION_TYPE_SMARTSPACE,
+            COMPLICATION_TYPE_MEDIA_ENTRY
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ComplicationType {
@@ -105,6 +106,7 @@
     public static final int COMPLICATION_TYPE_CAST_INFO = 5;
     public static final int COMPLICATION_TYPE_HOME_CONTROLS = 6;
     public static final int COMPLICATION_TYPE_SMARTSPACE = 7;
+    public static final int COMPLICATION_TYPE_MEDIA_ENTRY = 8;
 
     private final Context mContext;
     private final IDreamManager mDreamManager;
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
index 315ab0a..79e9938 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java
@@ -1069,80 +1069,4 @@
         assertThat(mSubCachedDevice.mDevice).isEqualTo(mDevice);
         assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
     }
-
-    @Test
-    public void isBusy_mainDeviceIsConnecting_returnsBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isTrue();
-    }
-
-    @Test
-    public void isBusy_mainDeviceIsBonding_returnsBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isTrue();
-    }
-
-    @Test
-    public void isBusy_memberDeviceIsConnecting_returnsBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTING);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isTrue();
-    }
-
-    @Test
-    public void isBusy_memberDeviceIsBonding_returnsBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDING);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isTrue();
-    }
-
-    @Test
-    public void isBusy_allDevicesAreNotBusy_returnsNotBusy() {
-        mCachedDevice.addMemberDevice(mSubCachedDevice);
-        updateProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        updateSubDeviceProfileStatus(mA2dpProfile, BluetoothProfile.STATE_CONNECTED);
-        when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-        when(mSubDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
-
-        assertThat(mCachedDevice.getMemberDevice().contains(mSubCachedDevice)).isTrue();
-        assertThat(mCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mSubCachedDevice.getProfiles().contains(mA2dpProfile)).isTrue();
-        assertThat(mCachedDevice.isBusy()).isFalse();
-    }
 }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 808ea9e..6d375ac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -549,7 +549,7 @@
 
         try {
             IActivityManager am = ActivityManager.getService();
-            Configuration config = am.getConfiguration();
+            final Configuration config = new Configuration();
             config.setLocales(merged);
             // indicate this isn't some passing default - the user wants this remembered
             config.userSetLocale = true;
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index b5145f9..4267ba2 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -289,6 +289,12 @@
     <!-- Query all packages on device on R+ -->
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.NOTES" />
+        </intent>
+    </queries>
+
     <!-- Permission to register process observer -->
     <uses-permission android:name="android.permission.SET_ACTIVITY_WATCHER"/>
 
diff --git a/packages/SystemUI/res/drawable/overlay_badge_background.xml b/packages/SystemUI/res/drawable/overlay_badge_background.xml
new file mode 100644
index 0000000..857632e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/overlay_badge_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+        android:shape="oval">
+    <solid android:color="?androidprv:attr/colorSurface"/>
+</shape>
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index 2fb6d6c..9fc3f40 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -23,6 +23,7 @@
     android:layout_height="wrap_content"
     android:layout_gravity="@integer/notification_panel_layout_gravity"
     android:background="@android:color/transparent"
+    android:importantForAccessibility="no"
     android:baselineAligned="false"
     android:clickable="false"
     android:clipChildren="false"
@@ -56,7 +57,7 @@
             android:clipToPadding="false"
             android:focusable="true"
             android:paddingBottom="@dimen/qqs_layout_padding_bottom"
-            android:importantForAccessibility="yes">
+            android:importantForAccessibility="no">
         </com.android.systemui.qs.QuickQSPanel>
     </RelativeLayout>
 
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
index 60bc373..8b5d953 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_header_date_privacy.xml
@@ -25,6 +25,7 @@
     android:gravity="center"
     android:layout_gravity="top"
     android:orientation="horizontal"
+    android:importantForAccessibility="no"
     android:clickable="true"
     android:minHeight="48dp">
 
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index 9c02749..1ac78d4 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -103,8 +103,18 @@
         app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
         app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
         app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"
-        app:layout_constraintTop_toTopOf="@id/screenshot_preview_border">
-    </ImageView>
+        app:layout_constraintTop_toTopOf="@id/screenshot_preview_border"/>
+    <ImageView
+        android:id="@+id/screenshot_badge"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:padding="4dp"
+        android:visibility="gone"
+        android:background="@drawable/overlay_badge_background"
+        android:elevation="8dp"
+        android:src="@drawable/overlay_cancel"
+        app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
+        app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
     <FrameLayout
         android:id="@+id/screenshot_dismiss_button"
         android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 9188ce0..93982cb 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -643,6 +643,18 @@
         <item>26</item> <!-- MOUTH_COVERING_DETECTED -->
     </integer-array>
 
+    <!-- Which device wake-ups will trigger face auth. These values correspond with
+         PowerManager#WakeReason. -->
+    <integer-array name="config_face_auth_wake_up_triggers">
+        <item>1</item> <!-- WAKE_REASON_POWER_BUTTON -->
+        <item>4</item> <!-- WAKE_REASON_GESTURE -->
+        <item>6</item> <!-- WAKE_REASON_WAKE_KEY -->
+        <item>7</item> <!-- WAKE_REASON_WAKE_MOTION -->
+        <item>9</item> <!-- WAKE_REASON_LID -->
+        <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED -->
+        <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE -->
+    </integer-array>
+
     <!-- Whether the communal service should be enabled -->
     <bool name="config_communalServiceEnabled">false</bool>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index cd27263..48821e8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -18,7 +18,6 @@
 import android.graphics.drawable.Drawable
 import android.net.Uri
 import android.os.Handler
-import android.os.UserHandle
 import android.provider.Settings
 import android.util.Log
 import com.android.internal.annotations.Keep
@@ -39,15 +38,15 @@
     val context: Context,
     val pluginManager: PluginManager,
     val handler: Handler,
-    defaultClockProvider: ClockProvider
+    val isEnabled: Boolean,
+    userHandle: Int,
+    defaultClockProvider: ClockProvider,
 ) {
     // Usually this would be a typealias, but a SAM provides better java interop
     fun interface ClockChangeListener {
         fun onClockChanged()
     }
 
-    var isEnabled: Boolean = false
-
     private val gson = Gson()
     private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
@@ -97,14 +96,19 @@
             )
         }
 
-        pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java,
-            true /* allowMultiple */)
-        context.contentResolver.registerContentObserver(
-            Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
-            false,
-            settingObserver,
-            UserHandle.USER_ALL
-        )
+        if (isEnabled) {
+            pluginManager.addPluginListener(
+                pluginListener,
+                ClockProviderPlugin::class.java,
+                /*allowMultiple=*/ true
+            )
+            context.contentResolver.registerContentObserver(
+                Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
+                /*notifyForDescendants=*/ false,
+                settingObserver,
+                userHandle
+            )
+        }
     }
 
     private fun connectClocks(provider: ClockProvider) {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index 8d1768c..e1e8063 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -26,6 +26,7 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
@@ -228,7 +229,8 @@
     public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
         return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null)
-                && wallpapers == ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0));
+                && wallpapers == change.hasFlags(FLAG_IS_WALLPAPER)
+                && !change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY));
     }
 
     private static RemoteAnimationTarget[] wrap(TransitionInfo info,
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 386c095..40a96b0 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -30,6 +30,7 @@
 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.DOZING_MIGRATION_1
 import com.android.systemui.flags.Flags.REGION_SAMPLING
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -221,8 +222,11 @@
         disposableHandle = parent.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 listenForDozing(this)
-                listenForDozeAmount(this)
-                listenForDozeAmountTransition(this)
+                if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                    listenForDozeAmountTransition(this)
+                } else {
+                    listenForDozeAmount(this)
+                }
             }
         }
     }
@@ -265,10 +269,9 @@
     @VisibleForTesting
     internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
         return scope.launch {
-            keyguardTransitionInteractor.aodToLockscreenTransition.collect {
-                // Would eventually run this:
-                // dozeAmount = it.value
-                // clock?.animations?.doze(dozeAmount)
+            keyguardTransitionInteractor.dozeAmountTransition.collect {
+                dozeAmount = it.value
+                clock?.animations?.doze(dozeAmount)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index 6fcb6f5..4a41b3f 100644
--- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
+++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
@@ -17,6 +17,7 @@
 package com.android.keyguard
 
 import android.annotation.StringDef
+import android.os.PowerManager
 import com.android.internal.logging.UiEvent
 import com.android.internal.logging.UiEventLogger
 import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED
@@ -122,122 +123,93 @@
         "Face auth started/stopped because biometric is enabled on keyguard"
 }
 
-/** UiEvents that are logged to identify why face auth is being triggered. */
-enum class FaceAuthUiEvent constructor(private val id: Int, val reason: String) :
+/**
+ * UiEvents that are logged to identify why face auth is being triggered.
+ * @param extraInfo is logged as the position. See [UiEventLogger#logWithInstanceIdAndPosition]
+ */
+enum class FaceAuthUiEvent
+constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) :
     UiEventLogger.UiEventEnum {
     @UiEvent(doc = OCCLUDING_APP_REQUESTED)
     FACE_AUTH_TRIGGERED_OCCLUDING_APP_REQUESTED(1146, OCCLUDING_APP_REQUESTED),
-
     @UiEvent(doc = UDFPS_POINTER_DOWN)
     FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN(1147, UDFPS_POINTER_DOWN),
-
     @UiEvent(doc = SWIPE_UP_ON_BOUNCER)
     FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER(1148, SWIPE_UP_ON_BOUNCER),
-
     @UiEvent(doc = DEVICE_WOKEN_UP_ON_REACH_GESTURE)
     FACE_AUTH_TRIGGERED_ON_REACH_GESTURE_ON_AOD(1149, DEVICE_WOKEN_UP_ON_REACH_GESTURE),
-
     @UiEvent(doc = FACE_LOCKOUT_RESET)
     FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET(1150, FACE_LOCKOUT_RESET),
-
-    @UiEvent(doc = QS_EXPANDED)
-    FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED),
-
+    @UiEvent(doc = QS_EXPANDED) FACE_AUTH_TRIGGERED_QS_EXPANDED(1151, QS_EXPANDED),
     @UiEvent(doc = NOTIFICATION_PANEL_CLICKED)
     FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED(1152, NOTIFICATION_PANEL_CLICKED),
-
     @UiEvent(doc = PICK_UP_GESTURE_TRIGGERED)
     FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED(1153, PICK_UP_GESTURE_TRIGGERED),
-
     @UiEvent(doc = ALTERNATE_BIOMETRIC_BOUNCER_SHOWN)
-    FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154,
-        ALTERNATE_BIOMETRIC_BOUNCER_SHOWN),
-
+    FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN(1154, ALTERNATE_BIOMETRIC_BOUNCER_SHOWN),
     @UiEvent(doc = PRIMARY_BOUNCER_SHOWN)
     FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN(1155, PRIMARY_BOUNCER_SHOWN),
-
     @UiEvent(doc = PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN)
     FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN(
         1197,
         PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN
     ),
-
     @UiEvent(doc = RETRY_AFTER_HW_UNAVAILABLE)
     FACE_AUTH_TRIGGERED_RETRY_AFTER_HW_UNAVAILABLE(1156, RETRY_AFTER_HW_UNAVAILABLE),
-
-    @UiEvent(doc = TRUST_DISABLED)
-    FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED),
-
-    @UiEvent(doc = TRUST_ENABLED)
-    FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED),
-
+    @UiEvent(doc = TRUST_DISABLED) FACE_AUTH_TRIGGERED_TRUST_DISABLED(1158, TRUST_DISABLED),
+    @UiEvent(doc = TRUST_ENABLED) FACE_AUTH_STOPPED_TRUST_ENABLED(1173, TRUST_ENABLED),
     @UiEvent(doc = KEYGUARD_OCCLUSION_CHANGED)
     FACE_AUTH_UPDATED_KEYGUARD_OCCLUSION_CHANGED(1159, KEYGUARD_OCCLUSION_CHANGED),
-
     @UiEvent(doc = ASSISTANT_VISIBILITY_CHANGED)
     FACE_AUTH_UPDATED_ASSISTANT_VISIBILITY_CHANGED(1160, ASSISTANT_VISIBILITY_CHANGED),
-
     @UiEvent(doc = STARTED_WAKING_UP)
-    FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP),
-
+    FACE_AUTH_UPDATED_STARTED_WAKING_UP(1161, STARTED_WAKING_UP) {
+        override fun extraInfoToString(): String {
+            return PowerManager.wakeReasonToString(extraInfo)
+        }
+    },
+    @Deprecated(
+        "Not a face auth trigger.",
+        ReplaceWith(
+            "FACE_AUTH_UPDATED_STARTED_WAKING_UP, " +
+                "extraInfo=PowerManager.WAKE_REASON_DREAM_FINISHED"
+        )
+    )
     @UiEvent(doc = DREAM_STOPPED)
     FACE_AUTH_TRIGGERED_DREAM_STOPPED(1162, DREAM_STOPPED),
-
     @UiEvent(doc = ALL_AUTHENTICATORS_REGISTERED)
     FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED(1163, ALL_AUTHENTICATORS_REGISTERED),
-
     @UiEvent(doc = ENROLLMENTS_CHANGED)
     FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED(1164, ENROLLMENTS_CHANGED),
-
     @UiEvent(doc = KEYGUARD_VISIBILITY_CHANGED)
     FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED(1165, KEYGUARD_VISIBILITY_CHANGED),
-
     @UiEvent(doc = FACE_CANCEL_NOT_RECEIVED)
     FACE_AUTH_STOPPED_FACE_CANCEL_NOT_RECEIVED(1174, FACE_CANCEL_NOT_RECEIVED),
-
     @UiEvent(doc = AUTH_REQUEST_DURING_CANCELLATION)
     FACE_AUTH_TRIGGERED_DURING_CANCELLATION(1175, AUTH_REQUEST_DURING_CANCELLATION),
-
-    @UiEvent(doc = DREAM_STARTED)
-    FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED),
-
-    @UiEvent(doc = FP_LOCKED_OUT)
-    FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT),
-
+    @UiEvent(doc = DREAM_STARTED) FACE_AUTH_STOPPED_DREAM_STARTED(1176, DREAM_STARTED),
+    @UiEvent(doc = FP_LOCKED_OUT) FACE_AUTH_STOPPED_FP_LOCKED_OUT(1177, FP_LOCKED_OUT),
     @UiEvent(doc = FACE_AUTH_STOPPED_ON_USER_INPUT)
     FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER(1178, FACE_AUTH_STOPPED_ON_USER_INPUT),
-
     @UiEvent(doc = KEYGUARD_GOING_AWAY)
     FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY(1179, KEYGUARD_GOING_AWAY),
-
-    @UiEvent(doc = CAMERA_LAUNCHED)
-    FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED),
-
-    @UiEvent(doc = FP_AUTHENTICATED)
-    FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED),
-
-    @UiEvent(doc = GOING_TO_SLEEP)
-    FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP),
-
+    @UiEvent(doc = CAMERA_LAUNCHED) FACE_AUTH_UPDATED_CAMERA_LAUNCHED(1180, CAMERA_LAUNCHED),
+    @UiEvent(doc = FP_AUTHENTICATED) FACE_AUTH_UPDATED_FP_AUTHENTICATED(1181, FP_AUTHENTICATED),
+    @UiEvent(doc = GOING_TO_SLEEP) FACE_AUTH_UPDATED_GOING_TO_SLEEP(1182, GOING_TO_SLEEP),
     @UiEvent(doc = FINISHED_GOING_TO_SLEEP)
     FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP(1183, FINISHED_GOING_TO_SLEEP),
-
-    @UiEvent(doc = KEYGUARD_INIT)
-    FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT),
-
-    @UiEvent(doc = KEYGUARD_RESET)
-    FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET),
-
-    @UiEvent(doc = USER_SWITCHING)
-    FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING),
-
+    @UiEvent(doc = KEYGUARD_INIT) FACE_AUTH_UPDATED_ON_KEYGUARD_INIT(1189, KEYGUARD_INIT),
+    @UiEvent(doc = KEYGUARD_RESET) FACE_AUTH_UPDATED_KEYGUARD_RESET(1185, KEYGUARD_RESET),
+    @UiEvent(doc = USER_SWITCHING) FACE_AUTH_UPDATED_USER_SWITCHING(1186, USER_SWITCHING),
     @UiEvent(doc = FACE_AUTHENTICATED)
     FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED(1187, FACE_AUTHENTICATED),
-
     @UiEvent(doc = BIOMETRIC_ENABLED)
     FACE_AUTH_UPDATED_BIOMETRIC_ENABLED_ON_KEYGUARD(1188, BIOMETRIC_ENABLED);
 
     override fun getId(): Int = this.id
+
+    /** Convert [extraInfo] to a human-readable string. By default, this is empty. */
+    open fun extraInfoToString(): String = ""
 }
 
 private val apiRequestReasonToUiEvent =
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
new file mode 100644
index 0000000..a0c43fb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/FaceWakeUpTriggersConfig.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.content.res.Resources
+import android.os.Build
+import android.os.PowerManager
+import com.android.systemui.Dumpable
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.settings.GlobalSettings
+import java.io.PrintWriter
+import java.util.stream.Collectors
+import javax.inject.Inject
+
+/** Determines which device wake-ups should trigger face authentication. */
+@SysUISingleton
+class FaceWakeUpTriggersConfig
+@Inject
+constructor(@Main resources: Resources, globalSettings: GlobalSettings, dumpManager: DumpManager) :
+    Dumpable {
+    private val defaultTriggerFaceAuthOnWakeUpFrom: Set<Int> =
+        resources.getIntArray(R.array.config_face_auth_wake_up_triggers).toSet()
+    private val triggerFaceAuthOnWakeUpFrom: Set<Int>
+
+    init {
+        triggerFaceAuthOnWakeUpFrom =
+            if (Build.IS_DEBUGGABLE) {
+                // Update face wake triggers via adb on debuggable builds:
+                // ie: adb shell settings put global face_wake_triggers "1\|4" &&
+                //     adb shell am crash com.android.systemui
+                processStringArray(
+                    globalSettings.getString("face_wake_triggers"),
+                    defaultTriggerFaceAuthOnWakeUpFrom
+                )
+            } else {
+                defaultTriggerFaceAuthOnWakeUpFrom
+            }
+        dumpManager.registerDumpable(this)
+    }
+
+    fun shouldTriggerFaceAuthOnWakeUpFrom(@PowerManager.WakeReason pmWakeReason: Int): Boolean {
+        return triggerFaceAuthOnWakeUpFrom.contains(pmWakeReason)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("FaceWakeUpTriggers:")
+        for (pmWakeReason in triggerFaceAuthOnWakeUpFrom) {
+            pw.println("    ${PowerManager.wakeReasonToString(pmWakeReason)}")
+        }
+    }
+
+    /** Convert a pipe-separated set of integers into a set of ints. */
+    private fun processStringArray(stringSetting: String?, default: Set<Int>): Set<Int> {
+        return stringSetting?.let {
+            stringSetting.split("|").stream().map(Integer::parseInt).collect(Collectors.toSet())
+        }
+            ?: default
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 35eecdf..d3cc7ed 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -37,8 +37,6 @@
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
@@ -120,8 +118,7 @@
             SecureSettings secureSettings,
             @Main Executor uiExecutor,
             DumpManager dumpManager,
-            ClockEventController clockEventController,
-            FeatureFlags featureFlags) {
+            ClockEventController clockEventController) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
         mClockRegistry = clockRegistry;
@@ -134,7 +131,6 @@
         mDumpManager = dumpManager;
         mClockEventController = clockEventController;
 
-        mClockRegistry.setEnabled(featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS));
         mClockChangedListener = () -> {
             setClock(mClockRegistry.createCurrentClock());
         };
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index cb1330d..0351b41 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -44,7 +44,6 @@
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_STOPPED_USER_INPUT_ON_BOUNCER;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALL_AUTHENTICATORS_REGISTERED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN;
-import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DREAM_STOPPED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_DURING_CANCELLATION;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED;
 import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_TRIGGERED_FACE_LOCKOUT_RESET;
@@ -303,6 +302,7 @@
             }
         }
     };
+    private final FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
 
     HashMap<Integer, SimData> mSimDatas = new HashMap<>();
     HashMap<Integer, ServiceState> mServiceStates = new HashMap<>();
@@ -1823,11 +1823,21 @@
         }
     }
 
-    protected void handleStartedWakingUp() {
+    protected void handleStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
         Trace.beginSection("KeyguardUpdateMonitor#handleStartedWakingUp");
         Assert.isMainThread();
-        updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_STARTED_WAKING_UP);
-        requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp");
+
+        updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
+        if (mFaceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(pmWakeReason)) {
+            FACE_AUTH_UPDATED_STARTED_WAKING_UP.setExtraInfo(pmWakeReason);
+            updateFaceListeningState(BIOMETRIC_ACTION_UPDATE,
+                    FACE_AUTH_UPDATED_STARTED_WAKING_UP);
+            requestActiveUnlock(ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.WAKE, "wakingUp - "
+                    + PowerManager.wakeReasonToString(pmWakeReason));
+        } else {
+            mLogger.logSkipUpdateFaceListeningOnWakeup(pmWakeReason);
+        }
+
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -1879,12 +1889,9 @@
                 cb.onDreamingStateChanged(mIsDreaming);
             }
         }
+        updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
         if (mIsDreaming) {
-            updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
             updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_DREAM_STARTED);
-        } else {
-            updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
-                    FACE_AUTH_TRIGGERED_DREAM_STOPPED);
         }
     }
 
@@ -1964,7 +1971,8 @@
             PackageManager packageManager,
             @Nullable FaceManager faceManager,
             @Nullable FingerprintManager fingerprintManager,
-            @Nullable BiometricManager biometricManager) {
+            @Nullable BiometricManager biometricManager,
+            FaceWakeUpTriggersConfig faceWakeUpTriggersConfig) {
         mContext = context;
         mSubscriptionManager = subscriptionManager;
         mTelephonyListenerManager = telephonyListenerManager;
@@ -2003,6 +2011,7 @@
                         R.array.config_face_acquire_device_entry_ignorelist))
                 .boxed()
                 .collect(Collectors.toSet());
+        mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig;
 
         mHandler = new Handler(mainLooper) {
             @Override
@@ -2052,7 +2061,7 @@
                         break;
                     case MSG_STARTED_WAKING_UP:
                         Trace.beginSection("KeyguardUpdateMonitor#handler MSG_STARTED_WAKING_UP");
-                        handleStartedWakingUp();
+                        handleStartedWakingUp(msg.arg1);
                         Trace.endSection();
                         break;
                     case MSG_SIM_SUBSCRIPTION_INFO_CHANGED:
@@ -2799,8 +2808,14 @@
             // Waiting for ERROR_CANCELED before requesting auth again
             return;
         }
-        mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent.getReason());
-        mUiEventLogger.log(faceAuthUiEvent, getKeyguardSessionId());
+        mLogger.logStartedListeningForFace(mFaceRunningState, faceAuthUiEvent);
+        mUiEventLogger.logWithInstanceIdAndPosition(
+                faceAuthUiEvent,
+                0,
+                null,
+                getKeyguardSessionId(),
+                faceAuthUiEvent.getExtraInfo()
+        );
 
         if (unlockPossible) {
             mFaceCancelSignal = new CancellationSignal();
@@ -3579,11 +3594,16 @@
 
     // TODO: use these callbacks elsewhere in place of the existing notifyScreen*()
     // (KeyguardViewMediator, KeyguardHostView)
-    public void dispatchStartedWakingUp() {
+    /**
+     * Dispatch wakeup events to:
+     *  - update biometric listening states
+     *  - send to registered KeyguardUpdateMonitorCallbacks
+     */
+    public void dispatchStartedWakingUp(@PowerManager.WakeReason int pmWakeReason) {
         synchronized (this) {
             mDeviceInteractive = true;
         }
-        mHandler.sendEmptyMessage(MSG_STARTED_WAKING_UP);
+        mHandler.sendMessage(mHandler.obtainMessage(MSG_STARTED_WAKING_UP, pmWakeReason, 0));
     }
 
     public void dispatchStartedGoingToSleep(int why) {
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index c41b752..fe7c70a 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -23,6 +23,8 @@
 import static com.android.keyguard.LockIconView.ICON_UNLOCK;
 import static com.android.systemui.classifier.Classifier.LOCK_ICON;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
+import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
 
 import android.content.res.Configuration;
 import android.content.res.Resources;
@@ -44,6 +46,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 
 import com.android.systemui.Dumpable;
@@ -53,6 +56,10 @@
 import com.android.systemui.biometrics.UdfpsController;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
@@ -65,6 +72,7 @@
 
 import java.io.PrintWriter;
 import java.util.Objects;
+import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
@@ -101,6 +109,9 @@
     @NonNull private CharSequence mLockedLabel;
     @NonNull private final VibratorHelper mVibrator;
     @Nullable private final AuthRippleController mAuthRippleController;
+    @NonNull private final FeatureFlags mFeatureFlags;
+    @NonNull private final KeyguardTransitionInteractor mTransitionInteractor;
+    @NonNull private final KeyguardInteractor mKeyguardInteractor;
 
     // Tracks the velocity of a touch to help filter out the touches that move too fast.
     private VelocityTracker mVelocityTracker;
@@ -137,6 +148,20 @@
     private boolean mDownDetected;
     private final Rect mSensorTouchLocation = new Rect();
 
+    @VisibleForTesting
+    final Consumer<TransitionStep> mDozeTransitionCallback = (TransitionStep step) -> {
+        mInterpolatedDarkAmount = step.getValue();
+        mView.setDozeAmount(step.getValue());
+        updateBurnInOffsets();
+    };
+
+    @VisibleForTesting
+    final Consumer<Boolean> mIsDozingCallback = (Boolean isDozing) -> {
+        mIsDozing = isDozing;
+        updateBurnInOffsets();
+        updateVisibility();
+    };
+
     @Inject
     public LockIconViewController(
             @Nullable LockIconView view,
@@ -152,7 +177,10 @@
             @NonNull @Main DelayableExecutor executor,
             @NonNull VibratorHelper vibrator,
             @Nullable AuthRippleController authRippleController,
-            @NonNull @Main Resources resources
+            @NonNull @Main Resources resources,
+            @NonNull KeyguardTransitionInteractor transitionInteractor,
+            @NonNull KeyguardInteractor keyguardInteractor,
+            @NonNull FeatureFlags featureFlags
     ) {
         super(view);
         mStatusBarStateController = statusBarStateController;
@@ -166,6 +194,9 @@
         mExecutor = executor;
         mVibrator = vibrator;
         mAuthRippleController = authRippleController;
+        mTransitionInteractor = transitionInteractor;
+        mKeyguardInteractor = keyguardInteractor;
+        mFeatureFlags = featureFlags;
 
         mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
         mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
@@ -182,6 +213,12 @@
     @Override
     protected void onInit() {
         mView.setAccessibilityDelegate(mAccessibilityDelegate);
+
+        if (mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+            collectFlow(mView, mTransitionInteractor.getDozeAmountTransition(),
+                    mDozeTransitionCallback);
+            collectFlow(mView, mKeyguardInteractor.isDozing(), mIsDozingCallback);
+        }
     }
 
     @Override
@@ -377,14 +414,17 @@
         pw.println(" mShowUnlockIcon: " + mShowUnlockIcon);
         pw.println(" mShowLockIcon: " + mShowLockIcon);
         pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon);
-        pw.println("  mIsDozing: " + mIsDozing);
-        pw.println("  mIsBouncerShowing: " + mIsBouncerShowing);
-        pw.println("  mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
-        pw.println("  mRunningFPS: " + mRunningFPS);
-        pw.println("  mCanDismissLockScreen: " + mCanDismissLockScreen);
-        pw.println("  mStatusBarState: " + StatusBarState.toString(mStatusBarState));
-        pw.println("  mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
-        pw.println("  mSensorTouchLocation: " + mSensorTouchLocation);
+        pw.println();
+        pw.println(" mIsDozing: " + mIsDozing);
+        pw.println(" isFlagEnabled(DOZING_MIGRATION_1): "
+                + mFeatureFlags.isEnabled(DOZING_MIGRATION_1));
+        pw.println(" mIsBouncerShowing: " + mIsBouncerShowing);
+        pw.println(" mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
+        pw.println(" mRunningFPS: " + mRunningFPS);
+        pw.println(" mCanDismissLockScreen: " + mCanDismissLockScreen);
+        pw.println(" mStatusBarState: " + StatusBarState.toString(mStatusBarState));
+        pw.println(" mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
+        pw.println(" mSensorTouchLocation: " + mSensorTouchLocation);
 
         if (mView != null) {
             mView.dump(pw, args);
@@ -425,16 +465,20 @@
             new StatusBarStateController.StateListener() {
                 @Override
                 public void onDozeAmountChanged(float linear, float eased) {
-                    mInterpolatedDarkAmount = eased;
-                    mView.setDozeAmount(eased);
-                    updateBurnInOffsets();
+                    if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                        mInterpolatedDarkAmount = eased;
+                        mView.setDozeAmount(eased);
+                        updateBurnInOffsets();
+                    }
                 }
 
                 @Override
                 public void onDozingChanged(boolean isDozing) {
-                    mIsDozing = isDozing;
-                    updateBurnInOffsets();
-                    updateVisibility();
+                    if (!mFeatureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                        mIsDozing = isDozing;
+                        updateBurnInOffsets();
+                        updateVisibility();
+                    }
                 }
 
                 @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index f43f559..9767313 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -18,10 +18,13 @@
 
 import android.content.Context;
 import android.os.Handler;
+import android.os.UserHandle;
 
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.shared.clocks.ClockRegistry;
 import com.android.systemui.shared.clocks.DefaultClockProvider;
 import com.android.systemui.shared.plugins.PluginManager;
@@ -39,7 +42,14 @@
             @Application Context context,
             PluginManager pluginManager,
             @Main Handler handler,
-            DefaultClockProvider defaultClockProvider) {
-        return new ClockRegistry(context, pluginManager, handler, defaultClockProvider);
+            DefaultClockProvider defaultClockProvider,
+            FeatureFlags featureFlags) {
+        return new ClockRegistry(
+                context,
+                pluginManager,
+                handler,
+                featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
+                UserHandle.USER_ALL,
+                defaultClockProvider);
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
index 2f79e30..31fc320 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -17,9 +17,12 @@
 package com.android.keyguard.logging
 
 import android.hardware.biometrics.BiometricConstants.LockoutMode
+import android.os.PowerManager
+import android.os.PowerManager.WakeReason
 import android.telephony.ServiceState
 import android.telephony.SubscriptionInfo
 import com.android.keyguard.ActiveUnlockConfig
+import com.android.keyguard.FaceAuthUiEvent
 import com.android.keyguard.KeyguardListenModel
 import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.plugins.log.LogBuffer
@@ -269,11 +272,19 @@
         logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" })
     }
 
-    fun logStartedListeningForFace(faceRunningState: Int, faceAuthReason: String) {
+    fun logStartedListeningForFace(faceRunningState: Int, faceAuthUiEvent: FaceAuthUiEvent) {
         logBuffer.log(TAG, VERBOSE, {
             int1 = faceRunningState
-            str1 = faceAuthReason
-        }, { "startListeningForFace(): $int1, reason: $str1" })
+            str1 = faceAuthUiEvent.reason
+            str2 = faceAuthUiEvent.extraInfoToString()
+        }, { "startListeningForFace(): $int1, reason: $str1 $str2" })
+    }
+
+    fun logStartedListeningForFaceFromWakeUp(faceRunningState: Int, @WakeReason pmWakeReason: Int) {
+        logBuffer.log(TAG, VERBOSE, {
+            int1 = faceRunningState
+            str1 = PowerManager.wakeReasonToString(pmWakeReason)
+        }, { "startListeningForFace(): $int1, reason: wakeUp-$str1" })
     }
 
     fun logStoppedListeningForFace(faceRunningState: Int, faceAuthReason: String) {
@@ -383,4 +394,10 @@
         }, { "#update secure=$bool1 canDismissKeyguard=$bool2" +
                 " trusted=$bool3 trustManaged=$bool4" })
     }
+
+    fun logSkipUpdateFaceListeningOnWakeup(@WakeReason pmWakeReason: Int) {
+        logBuffer.log(TAG, VERBOSE, {
+            str1 = PowerManager.wakeReasonToString(pmWakeReason)
+        }, { "Skip updating face listening state on wakeup from $str1"})
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 7e31626..e47e636 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -48,6 +48,7 @@
 import com.android.systemui.mediaprojection.appselector.MediaProjectionModule;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.navigationbar.NavigationBarComponent;
+import com.android.systemui.notetask.NoteTaskModule;
 import com.android.systemui.people.PeopleModule;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
 import com.android.systemui.privacy.PrivacyModule;
@@ -152,6 +153,7 @@
             TunerModule.class,
             UserModule.class,
             UtilModule.class,
+            NoteTaskModule.class,
             WalletModule.class
         },
         subcomponents = {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
index 29bb2f4..41f5578 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
@@ -164,7 +164,8 @@
             COMPLICATION_TYPE_AIR_QUALITY,
             COMPLICATION_TYPE_CAST_INFO,
             COMPLICATION_TYPE_HOME_CONTROLS,
-            COMPLICATION_TYPE_SMARTSPACE
+            COMPLICATION_TYPE_SMARTSPACE,
+            COMPLICATION_TYPE_MEDIA_ENTRY
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface ComplicationType {}
@@ -177,6 +178,7 @@
     int COMPLICATION_TYPE_CAST_INFO = 1 << 4;
     int COMPLICATION_TYPE_HOME_CONTROLS = 1 << 5;
     int COMPLICATION_TYPE_SMARTSPACE = 1 << 6;
+    int COMPLICATION_TYPE_MEDIA_ENTRY = 1 << 7;
 
     /**
      * The {@link Host} interface specifies a way a {@link Complication} to communicate with its
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
index 75a97de..18aacd2 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
@@ -20,6 +20,7 @@
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_NONE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME;
@@ -54,6 +55,8 @@
                 return COMPLICATION_TYPE_HOME_CONTROLS;
             case DreamBackend.COMPLICATION_TYPE_SMARTSPACE:
                 return COMPLICATION_TYPE_SMARTSPACE;
+            case DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY:
+                return COMPLICATION_TYPE_MEDIA_ENTRY;
             default:
                 return COMPLICATION_TYPE_NONE;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
index 1166c2f..deff060 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
@@ -55,6 +55,11 @@
         return mComponentFactory.create().getViewHolder();
     }
 
+    @Override
+    public int getRequiredTypeAvailability() {
+        return COMPLICATION_TYPE_MEDIA_ENTRY;
+    }
+
     /**
      * Contains values/logic associated with the dream complication view.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 9ef3f5d..561222f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -57,7 +57,7 @@
     val INSTANT_VOICE_REPLY = UnreleasedFlag(111, teamfood = true)
 
     // TODO(b/254512425): Tracking Bug
-    val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = false)
+    val NOTIFICATION_MEMORY_MONITOR_ENABLED = UnreleasedFlag(112, teamfood = true)
 
     // TODO(b/254512731): Tracking Bug
     @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true)
@@ -118,6 +118,12 @@
      */
     @JvmField val STEP_CLOCK_ANIMATION = UnreleasedFlag(212)
 
+    /**
+     * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository
+     * will occur in stages. This is one stage of many to come.
+     */
+    @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213, teamfood = true)
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = ReleasedFlag(300)
@@ -156,9 +162,6 @@
     // TODO(b/254513246): Tracking Bug
     val STATUS_BAR_USER_SWITCHER = ResourceBooleanFlag(602, R.bool.flag_user_switcher_chip)
 
-    // TODO(b/254513025): Tracking Bug
-    val STATUS_BAR_LETTERBOX_APPEARANCE = ReleasedFlag(603, teamfood = false)
-
     // TODO(b/254512623): Tracking Bug
     @Deprecated("Replaced by mobile and wifi specific flags.")
     val NEW_STATUS_BAR_PIPELINE_BACKEND = UnreleasedFlag(604, teamfood = false)
@@ -322,6 +325,9 @@
     // 1800 - shade container
     @JvmField val LEAVE_SHADE_OPEN_FOR_BUGREPORT = UnreleasedFlag(1800, true)
 
+    // 1900 - note task
+    @JvmField val NOTE_TASKS = SysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks")
+
     // Pay no attention to the reflection behind the curtain.
     // ========================== Curtain ==========================
     // |                                                           |
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index ddcd053..8846bbd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -554,7 +554,7 @@
                 @PowerManager.WakeReason int pmWakeReason, boolean cameraGestureTriggered) {
             Trace.beginSection("KeyguardService.mBinder#onStartedWakingUp");
             checkPermission();
-            mKeyguardViewMediator.onStartedWakingUp(cameraGestureTriggered);
+            mKeyguardViewMediator.onStartedWakingUp(pmWakeReason, cameraGestureTriggered);
             mKeyguardLifecyclesDispatcher.dispatch(
                     KeyguardLifecyclesDispatcher.STARTED_WAKING_UP, pmWakeReason);
             Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index c8370b4..6f1ad70 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -91,6 +91,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.jank.InteractionJankMonitor.Configuration;
@@ -322,6 +323,12 @@
     // true if the keyguard is hidden by another window
     private boolean mOccluded = false;
 
+    /**
+     * Whether the {@link #mOccludeAnimationController} is currently playing the occlusion
+     * animation.
+     */
+    private boolean mOccludeAnimationPlaying = false;
+
     private boolean mWakeAndUnlocking = false;
 
     /**
@@ -335,12 +342,6 @@
      */
     private int mDelayedProfileShowingSequence;
 
-    /**
-     * If the user has disabled the keyguard, then requests to exit, this is
-     * how we'll ultimately let them know whether it was successful.  We use this
-     * var being non-null as an indicator that there is an in progress request.
-     */
-    private IKeyguardExitCallback mExitSecureCallback;
     private final DismissCallbackRegistry mDismissCallbackRegistry;
 
     // the properties of the keyguard
@@ -836,15 +837,22 @@
     /**
      * Animation launch controller for activities that occlude the keyguard.
      */
-    private final ActivityLaunchAnimator.Controller mOccludeAnimationController =
+    @VisibleForTesting
+    final ActivityLaunchAnimator.Controller mOccludeAnimationController =
             new ActivityLaunchAnimator.Controller() {
                 @Override
-                public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {}
+                public void onLaunchAnimationStart(boolean isExpandingFullyAbove) {
+                    mOccludeAnimationPlaying = true;
+                }
 
                 @Override
                 public void onLaunchAnimationCancelled(@Nullable Boolean newKeyguardOccludedState) {
                     Log.d(TAG, "Occlude launch animation cancelled. Occluded state is now: "
                             + mOccluded);
+                    mOccludeAnimationPlaying = false;
+
+                    // Ensure keyguard state is set correctly if we're cancelled.
+                    mCentralSurfaces.updateIsKeyguard();
                 }
 
                 @Override
@@ -853,6 +861,12 @@
                         mCentralSurfaces.instantCollapseNotificationPanel();
                     }
 
+                    mOccludeAnimationPlaying = false;
+
+                    // Hide the keyguard now that we're done launching the occluding activity over
+                    // it.
+                    mCentralSurfaces.updateIsKeyguard();
+
                     mInteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION);
                 }
 
@@ -1321,18 +1335,7 @@
                             || !mLockPatternUtils.isSecure(currentUser);
             long timeout = getLockTimeout(KeyguardUpdateMonitor.getCurrentUser());
             mLockLater = false;
-            if (mExitSecureCallback != null) {
-                if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled");
-                try {
-                    mExitSecureCallback.onKeyguardExitResult(false);
-                } catch (RemoteException e) {
-                    Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
-                }
-                mExitSecureCallback = null;
-                if (!mExternallyEnabled) {
-                    hideLocked();
-                }
-            } else if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) {
+            if (mShowing && !mKeyguardStateController.isKeyguardGoingAway()) {
                 // If we are going to sleep but the keyguard is showing (and will continue to be
                 // showing, not in the process of going away) then reset its state. Otherwise, let
                 // this fall through and explicitly re-lock the keyguard.
@@ -1574,7 +1577,8 @@
     /**
      * It will let us know when the device is waking up.
      */
-    public void onStartedWakingUp(boolean cameraGestureTriggered) {
+    public void onStartedWakingUp(@PowerManager.WakeReason int pmWakeReason,
+            boolean cameraGestureTriggered) {
         Trace.beginSection("KeyguardViewMediator#onStartedWakingUp");
 
         // TODO: Rename all screen off/on references to interactive/sleeping
@@ -1589,7 +1593,7 @@
             if (DEBUG) Log.d(TAG, "onStartedWakingUp, seq = " + mDelayedShowingSequence);
             notifyStartedWakingUp();
         }
-        mUpdateMonitor.dispatchStartedWakingUp();
+        mUpdateMonitor.dispatchStartedWakingUp(pmWakeReason);
         maybeSendUserPresentBroadcast();
         Trace.endSection();
     }
@@ -1651,13 +1655,6 @@
             mExternallyEnabled = enabled;
 
             if (!enabled && mShowing) {
-                if (mExitSecureCallback != null) {
-                    if (DEBUG) Log.d(TAG, "in process of verifyUnlock request, ignoring");
-                    // we're in the process of handling a request to verify the user
-                    // can get past the keyguard. ignore extraneous requests to disable / re-enable
-                    return;
-                }
-
                 // hiding keyguard that is showing, remember to reshow later
                 if (DEBUG) Log.d(TAG, "remembering to reshow, hiding keyguard, "
                         + "disabling status bar expansion");
@@ -1671,33 +1668,23 @@
                 mNeedToReshowWhenReenabled = false;
                 updateInputRestrictedLocked();
 
-                if (mExitSecureCallback != null) {
-                    if (DEBUG) Log.d(TAG, "onKeyguardExitResult(false), resetting");
-                    try {
-                        mExitSecureCallback.onKeyguardExitResult(false);
-                    } catch (RemoteException e) {
-                        Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
-                    }
-                    mExitSecureCallback = null;
-                    resetStateLocked();
-                } else {
-                    showLocked(null);
+                showLocked(null);
 
-                    // block until we know the keyguard is done drawing (and post a message
-                    // to unblock us after a timeout, so we don't risk blocking too long
-                    // and causing an ANR).
-                    mWaitingUntilKeyguardVisible = true;
-                    mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING, KEYGUARD_DONE_DRAWING_TIMEOUT_MS);
-                    if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false");
-                    while (mWaitingUntilKeyguardVisible) {
-                        try {
-                            wait();
-                        } catch (InterruptedException e) {
-                            Thread.currentThread().interrupt();
-                        }
+                // block until we know the keyguard is done drawing (and post a message
+                // to unblock us after a timeout, so we don't risk blocking too long
+                // and causing an ANR).
+                mWaitingUntilKeyguardVisible = true;
+                mHandler.sendEmptyMessageDelayed(KEYGUARD_DONE_DRAWING,
+                        KEYGUARD_DONE_DRAWING_TIMEOUT_MS);
+                if (DEBUG) Log.d(TAG, "waiting until mWaitingUntilKeyguardVisible is false");
+                while (mWaitingUntilKeyguardVisible) {
+                    try {
+                        wait();
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
                     }
-                    if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible");
                 }
+                if (DEBUG) Log.d(TAG, "done waiting for mWaitingUntilKeyguardVisible");
             }
         }
     }
@@ -1727,13 +1714,6 @@
                 } catch (RemoteException e) {
                     Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
                 }
-            } else if (mExitSecureCallback != null) {
-                // already in progress with someone else
-                try {
-                    callback.onKeyguardExitResult(false);
-                } catch (RemoteException e) {
-                    Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
-                }
             } else if (!isSecure()) {
 
                 // Keyguard is not secure, no need to do anything, and we don't need to reshow
@@ -1767,6 +1747,10 @@
         return mShowing && !mOccluded;
     }
 
+    public boolean isOccludeAnimationPlaying() {
+        return mOccludeAnimationPlaying;
+    }
+
     /**
      * Notify us when the keyguard is occluded by another window
      */
@@ -2270,21 +2254,6 @@
             return;
         }
         setPendingLock(false); // user may have authenticated during the screen off animation
-        if (mExitSecureCallback != null) {
-            try {
-                mExitSecureCallback.onKeyguardExitResult(true /* authenciated */);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to call onKeyguardExitResult()", e);
-            }
-
-            mExitSecureCallback = null;
-
-            // after successfully exiting securely, no need to reshow
-            // the keyguard when they've released the lock
-            mExternallyEnabled = true;
-            mNeedToReshowWhenReenabled = false;
-            updateInputRestricted();
-        }
 
         handleHide();
         mUpdateMonitor.clearBiometricRecognizedWhenKeyguardDone(currentUser);
@@ -3093,7 +3062,6 @@
         pw.print("  mInputRestricted: "); pw.println(mInputRestricted);
         pw.print("  mOccluded: "); pw.println(mOccluded);
         pw.print("  mDelayedShowingSequence: "); pw.println(mDelayedShowingSequence);
-        pw.print("  mExitSecureCallback: "); pw.println(mExitSecureCallback);
         pw.print("  mDeviceInteractive: "); pw.println(mDeviceInteractive);
         pw.print("  mGoingToSleep: "); pw.println(mGoingToSleep);
         pw.print("  mHiding: "); pw.println(mHiding);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index e8532ec..ab25597 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -20,6 +20,7 @@
 import android.animation.ValueAnimator
 import android.animation.ValueAnimator.AnimatorUpdateListener
 import android.annotation.FloatRange
+import android.os.Trace
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -157,12 +158,36 @@
         value: Float,
         transitionState: TransitionState
     ) {
+        trace(info, transitionState)
+
         if (transitionState == TransitionState.FINISHED) {
             currentTransitionInfo = null
         }
         _transitions.value = TransitionStep(info.from, info.to, value, transitionState)
     }
 
+    private fun trace(info: TransitionInfo, transitionState: TransitionState) {
+        if (
+            transitionState != TransitionState.STARTED &&
+                transitionState != TransitionState.FINISHED
+        ) {
+            return
+        }
+        val traceName =
+            "Transition: ${info.from} -> ${info.to} " +
+                if (info.animator == null) {
+                    "(manual)"
+                } else {
+                    ""
+                }
+        val traceCookie = traceName.hashCode()
+        if (transitionState == TransitionState.STARTED) {
+            Trace.beginAsyncSection(traceName, traceCookie)
+        } else if (transitionState == TransitionState.FINISHED) {
+            Trace.endAsyncSection(traceName, traceCookie)
+        }
+    }
+
     companion object {
         private const val TAG = "KeyguardTransitionRepository"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 59bb22786..7409aec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -24,6 +24,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 
 /** Encapsulates business-logic related to the keyguard transitions. */
 @SysUISingleton
@@ -34,4 +36,17 @@
 ) {
     /** AOD->LOCKSCREEN transition information. */
     val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN)
+
+    /** LOCKSCREEN->AOD transition information. */
+    val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
+
+    /**
+     * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
+     * Lockscreen (0f).
+     */
+    val dozeAmountTransition: Flow<TransitionStep> =
+        merge(
+            aodToLockscreenTransition.map { step -> step.copy(value = 1f - step.value) },
+            lockscreenToAodTransition,
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
index 38c971e..120f7d6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt
@@ -43,6 +43,21 @@
         )
     }
 
+    /**
+     * Logs an error in trying to update to [displayState].
+     *
+     * [displayState] is either a [android.app.StatusBarManager.MediaTransferSenderState] or
+     * a [android.app.StatusBarManager.MediaTransferReceiverState].
+     */
+    fun logStateChangeError(displayState: Int) {
+        buffer.log(
+            tag,
+            LogLevel.ERROR,
+            { int1 = displayState },
+            { "Cannot display state=$int1; aborting" }
+        )
+    }
+
     /** Logs that we couldn't find information for [packageName]. */
     fun logPackageNotFound(packageName: String) {
         buffer.log(
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 089625c..dc794e6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -25,7 +25,6 @@
 import android.media.MediaRoute2Info
 import android.os.Handler
 import android.os.PowerManager
-import android.util.Log
 import android.view.Gravity
 import android.view.View
 import android.view.ViewGroup
@@ -116,7 +115,7 @@
         logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
 
         if (chipState == null) {
-            Log.e(RECEIVER_TAG, "Unhandled MediaTransferReceiverState $displayState")
+            logger.logStateChangeError(displayState)
             return
         }
         uiEventLogger.logReceiverStateChange(chipState)
@@ -236,5 +235,3 @@
 ) : TemporaryViewInfo {
     override fun getTimeoutMs() = DEFAULT_TIMEOUT_MILLIS
 }
-
-private const val RECEIVER_TAG = "MediaTapToTransferRcvr"
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
index edf759d..1fa8fae 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt
@@ -19,7 +19,6 @@
 import android.app.StatusBarManager
 import android.content.Context
 import android.media.MediaRoute2Info
-import android.util.Log
 import android.view.View
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.statusbar.IUndoMediaTransferCallback
@@ -34,7 +33,6 @@
 import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
 import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem
 import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo
-import com.android.systemui.temporarydisplay.chipbar.SENDER_TAG
 import javax.inject.Inject
 
 /**
@@ -86,7 +84,7 @@
         logger.logStateChange(stateName, routeInfo.id, routeInfo.clientPackageName)
 
         if (chipState == null) {
-            Log.e(SENDER_TAG, "Unhandled MediaTransferSenderState $displayState")
+            logger.logStateChangeError(displayState)
             return
         }
         uiEventLogger.logSenderStateChange(chipState)
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
new file mode 100644
index 0000000..d247f24
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.os.UserManager
+import android.view.KeyEvent
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.kotlin.getOrNull
+import com.android.wm.shell.floating.FloatingTasks
+import java.util.Optional
+import javax.inject.Inject
+
+/**
+ * Entry point for creating and managing note.
+ *
+ * The controller decides how a note is launched based in the device state: locked or unlocked.
+ *
+ * Currently, we only support a single task per time.
+ */
+@SysUISingleton
+internal class NoteTaskController
+@Inject
+constructor(
+    private val context: Context,
+    private val intentResolver: NoteTaskIntentResolver,
+    private val optionalFloatingTasks: Optional<FloatingTasks>,
+    private val optionalKeyguardManager: Optional<KeyguardManager>,
+    private val optionalUserManager: Optional<UserManager>,
+    @NoteTaskEnabledKey private val isEnabled: Boolean,
+) {
+
+    fun handleSystemKey(keyCode: Int) {
+        if (!isEnabled) return
+
+        if (keyCode == KeyEvent.KEYCODE_VIDEO_APP_1) {
+            showNoteTask()
+        }
+    }
+
+    private fun showNoteTask() {
+        val floatingTasks = optionalFloatingTasks.getOrNull() ?: return
+        val keyguardManager = optionalKeyguardManager.getOrNull() ?: return
+        val userManager = optionalUserManager.getOrNull() ?: return
+        val intent = intentResolver.resolveIntent() ?: return
+
+        // TODO(b/249954038): We should handle direct boot (isUserUnlocked). For now, we do nothing.
+        if (!userManager.isUserUnlocked) return
+
+        if (keyguardManager.isKeyguardLocked) {
+            context.startActivity(intent)
+        } else {
+            // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter.
+            floatingTasks.showOrSetStashed(intent)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt
new file mode 100644
index 0000000..e0bf1da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskEnabledKey.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import javax.inject.Qualifier
+
+/** Key associated with a [Boolean] flag that enables or disables the note task feature. */
+@Qualifier internal annotation class NoteTaskEnabledKey
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
new file mode 100644
index 0000000..d84717d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import com.android.systemui.statusbar.CommandQueue
+import com.android.wm.shell.floating.FloatingTasks
+import dagger.Lazy
+import java.util.Optional
+import javax.inject.Inject
+
+/** Class responsible to "glue" all note task dependencies. */
+internal class NoteTaskInitializer
+@Inject
+constructor(
+    private val optionalFloatingTasks: Optional<FloatingTasks>,
+    private val lazyNoteTaskController: Lazy<NoteTaskController>,
+    private val commandQueue: CommandQueue,
+    @NoteTaskEnabledKey private val isEnabled: Boolean,
+) {
+
+    private val callbacks =
+        object : CommandQueue.Callbacks {
+            override fun handleSystemKey(keyCode: Int) {
+                lazyNoteTaskController.get().handleSystemKey(keyCode)
+            }
+        }
+
+    fun initialize() {
+        if (isEnabled && optionalFloatingTasks.isPresent) {
+            commandQueue.addCallback(callbacks)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
new file mode 100644
index 0000000..98d6991
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskIntentResolver.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import javax.inject.Inject
+
+/**
+ * Class responsible to query all apps and find one that can handle the [NOTES_ACTION]. If found, an
+ * [Intent] ready for be launched will be returned. Otherwise, returns null.
+ *
+ * TODO(b/248274123): should be revisited once the notes role is implemented.
+ */
+internal class NoteTaskIntentResolver
+@Inject
+constructor(
+    private val packageManager: PackageManager,
+) {
+
+    fun resolveIntent(): Intent? {
+        val intent = Intent(NOTES_ACTION)
+        val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
+        val infoList = packageManager.queryIntentActivities(intent, flags)
+
+        for (info in infoList) {
+            val packageName = info.serviceInfo.applicationInfo.packageName ?: continue
+            val activityName = resolveActivityNameForNotesAction(packageName) ?: continue
+
+            return Intent(NOTES_ACTION)
+                .setComponent(ComponentName(packageName, activityName))
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        }
+
+        return null
+    }
+
+    private fun resolveActivityNameForNotesAction(packageName: String): String? {
+        val intent = Intent(NOTES_ACTION).setPackage(packageName)
+        val flags = ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
+        val resolveInfo = packageManager.resolveActivity(intent, flags)
+
+        val activityInfo = resolveInfo?.activityInfo ?: return null
+        if (activityInfo.name.isNullOrBlank()) return null
+        if (!activityInfo.exported) return null
+        if (!activityInfo.enabled) return null
+        if (!activityInfo.showWhenLocked) return null
+        if (!activityInfo.turnScreenOn) return null
+
+        return activityInfo.name
+    }
+
+    companion object {
+        // TODO(b/254606432): Use Intent.ACTION_NOTES and Intent.ACTION_NOTES_LOCKED instead.
+        const val NOTES_ACTION = "android.intent.action.NOTES"
+    }
+}
+
+private val ActivityInfo.showWhenLocked: Boolean
+    get() = flags and ActivityInfo.FLAG_SHOW_WHEN_LOCKED != 0
+
+private val ActivityInfo.turnScreenOn: Boolean
+    get() = flags and ActivityInfo.FLAG_TURN_SCREEN_ON != 0
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
new file mode 100644
index 0000000..035396a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.os.UserManager
+import androidx.core.content.getSystemService
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import dagger.Module
+import dagger.Provides
+import java.util.*
+
+/** Compose all dependencies required by Note Task feature. */
+@Module
+internal class NoteTaskModule {
+
+    @[Provides NoteTaskEnabledKey]
+    fun provideIsNoteTaskEnabled(featureFlags: FeatureFlags): Boolean {
+        return featureFlags.isEnabled(Flags.NOTE_TASKS)
+    }
+
+    @Provides
+    fun provideOptionalKeyguardManager(context: Context): Optional<KeyguardManager> {
+        return Optional.ofNullable(context.getSystemService())
+    }
+
+    @Provides
+    fun provideOptionalUserManager(context: Context): Optional<UserManager> {
+        return Optional.ofNullable(context.getSystemService())
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index b6b657e..57a00c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -204,6 +204,15 @@
             Trace.endSection();
         }
 
+        @Override
+        public void onUserListItemClicked(@NonNull UserRecord record,
+                @Nullable UserSwitchDialogController.DialogShower dialogShower) {
+            if (dialogShower != null) {
+                mDialogShower.dismiss();
+            }
+            super.onUserListItemClicked(record, dialogShower);
+        }
+
         public void linkToViewGroup(ViewGroup viewGroup) {
             PseudoGridView.ViewGroupAdapterBridge.link(viewGroup, this);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 231e415..d524a35 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -20,6 +20,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
 
+import static com.android.systemui.flags.Flags.SCREENSHOT_WORK_PROFILE_POLICY;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
@@ -634,6 +635,11 @@
                         return true;
                     }
                 });
+
+        if (mFlags.isEnabled(SCREENSHOT_WORK_PROFILE_POLICY)) {
+            mScreenshotView.badgeScreenshot(
+                    mContext.getPackageManager().getUserBadgeForDensity(owner, 0));
+        }
         mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
         if (DEBUG_WINDOW) {
             Log.d(TAG, "setContentView: " + mScreenshotView);
@@ -1038,7 +1044,7 @@
 
     private boolean isUserSetupComplete(UserHandle owner) {
         return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
-                        .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+                .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 1b9cdd4..27331ae 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -74,7 +74,6 @@
 import android.view.WindowManager;
 import android.view.WindowMetrics;
 import android.view.accessibility.AccessibilityManager;
-import android.view.animation.AccelerateInterpolator;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
@@ -122,15 +121,9 @@
     private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
     private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
     private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
-    private static final long SCREENSHOT_DISMISS_X_DURATION_MS = 350;
-    private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 350;
-    private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
     private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
-    private static final float ROUNDED_CORNER_RADIUS = .25f;
     private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
 
-    private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator();
-
     private final Resources mResources;
     private final Interpolator mFastOutSlowIn;
     private final DisplayMetrics mDisplayMetrics;
@@ -145,6 +138,7 @@
     private ImageView mScrollingScrim;
     private DraggableConstraintLayout mScreenshotStatic;
     private ImageView mScreenshotPreview;
+    private ImageView mScreenshotBadge;
     private View mScreenshotPreviewBorder;
     private ImageView mScrollablePreview;
     private ImageView mScreenshotFlash;
@@ -355,6 +349,7 @@
         mScreenshotPreviewBorder = requireNonNull(
                 findViewById(R.id.screenshot_preview_border));
         mScreenshotPreview.setClipToOutline(true);
+        mScreenshotBadge = requireNonNull(findViewById(R.id.screenshot_badge));
 
         mActionsContainerBackground = requireNonNull(findViewById(
                 R.id.actions_container_background));
@@ -595,8 +590,11 @@
 
         ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1);
         borderFadeIn.setDuration(100);
-        borderFadeIn.addUpdateListener((animation) ->
-                mScreenshotPreviewBorder.setAlpha(animation.getAnimatedFraction()));
+        borderFadeIn.addUpdateListener((animation) -> {
+            float borderAlpha = animation.getAnimatedFraction();
+            mScreenshotPreviewBorder.setAlpha(borderAlpha);
+            mScreenshotBadge.setAlpha(borderAlpha);
+        });
 
         if (showFlash) {
             dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
@@ -763,6 +761,11 @@
         return animator;
     }
 
+    void badgeScreenshot(Drawable badge) {
+        mScreenshotBadge.setImageDrawable(badge);
+        mScreenshotBadge.setVisibility(badge != null ? View.VISIBLE : View.GONE);
+    }
+
     void setChipIntents(ScreenshotController.SavedImageData imageData) {
         mShareChip.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
@@ -1027,6 +1030,9 @@
         mScreenshotPreview.setVisibility(View.INVISIBLE);
         mScreenshotPreview.setAlpha(1f);
         mScreenshotPreviewBorder.setAlpha(0);
+        mScreenshotBadge.setAlpha(0f);
+        mScreenshotBadge.setVisibility(View.GONE);
+        mScreenshotBadge.setImageDrawable(null);
         mPendingSharedTransition = false;
         mActionsContainerBackground.setVisibility(View.GONE);
         mActionsContainer.setVisibility(View.GONE);
@@ -1082,6 +1088,7 @@
             mActionsContainerBackground.setAlpha(alpha);
             mActionsContainer.setAlpha(alpha);
             mScreenshotPreviewBorder.setAlpha(alpha);
+            mScreenshotBadge.setAlpha(alpha);
         });
         alphaAnim.setDuration(600);
         return alphaAnim;
diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
index bbba007..b36f0d7 100644
--- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
+++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java
@@ -33,13 +33,13 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.ColorUtils;
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 
 /**
  * Drawable used on SysUI scrims.
  */
 public class ScrimDrawable extends Drawable {
     private static final String TAG = "ScrimDrawable";
-    private static final long COLOR_ANIMATION_DURATION = 2000;
 
     private final Paint mPaint;
     private int mAlpha = 255;
@@ -76,7 +76,7 @@
             final int mainFrom = mMainColor;
 
             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
-            anim.setDuration(COLOR_ANIMATION_DURATION);
+            anim.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
             anim.addUpdateListener(animation -> {
                 float ratio = (float) animation.getAnimatedValue();
                 mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 8de0365..277ad8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1374,13 +1374,8 @@
         if (bubbleButton == null || actionContainer == null) {
             return;
         }
-        boolean isPersonWithShortcut =
-                mPeopleIdentifier.getPeopleNotificationType(entry)
-                        >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
-        boolean showButton = BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
-                && isPersonWithShortcut
-                && entry.getBubbleMetadata() != null;
-        if (showButton) {
+
+        if (shouldShowBubbleButton(entry)) {
             // explicitly resolve drawable resource using SystemUI's theme
             Drawable d = mContext.getDrawable(entry.isBubble()
                     ? R.drawable.bubble_ic_stop_bubble
@@ -1410,6 +1405,16 @@
         }
     }
 
+    @VisibleForTesting
+    boolean shouldShowBubbleButton(NotificationEntry entry) {
+        boolean isPersonWithShortcut =
+                mPeopleIdentifier.getPeopleNotificationType(entry)
+                        >= PeopleNotificationIdentifier.TYPE_FULL_PERSON;
+        return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser())
+                && isPersonWithShortcut
+                && entry.getBubbleMetadata() != null;
+    }
+
     private void applySnoozeAction(View layout) {
         if (layout == null || mContainingNotification == null) {
             return;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index db8aac95..9da5027 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2980,7 +2980,10 @@
             //  * When phone is unlocked: we still don't want to execute hiding of the keyguard
             //    as the animation could prepare 'fake AOD' interface (without actually
             //    transitioning to keyguard state) and this might reset the view states
-            if (!mScreenOffAnimationController.isKeyguardHideDelayed()) {
+            if (!mScreenOffAnimationController.isKeyguardHideDelayed()
+                    // If we're animating occluded, there's an activity launching over the keyguard
+                    // UI. Wait to hide it until after the animation concludes.
+                    && !mKeyguardViewMediator.isOccludeAnimationPlaying()) {
                 return hideKeyguardImpl(forceStateChange);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 8490768..cf3a48c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -53,6 +53,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.statusbar.notification.stack.ViewState;
@@ -204,6 +205,7 @@
     private final ScreenOffAnimationController mScreenOffAnimationController;
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    private KeyguardViewMediator mKeyguardViewMediator;
 
     private GradientColors mColors;
     private boolean mNeedsDrawableColorUpdate;
@@ -273,7 +275,8 @@
             @Main Executor mainExecutor,
             ScreenOffAnimationController screenOffAnimationController,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
-            StatusBarKeyguardViewManager statusBarKeyguardViewManager) {
+            StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+            KeyguardViewMediator keyguardViewMediator) {
         mScrimStateListener = lightBarController::setScrimState;
         mDefaultScrimAlpha = BUSY_SCRIM_ALPHA;
 
@@ -312,6 +315,8 @@
             }
         });
         mColors = new GradientColors();
+
+        mKeyguardViewMediator = keyguardViewMediator;
     }
 
     /**
@@ -807,6 +812,13 @@
                         mBehindTint,
                         interpolatedFraction);
             }
+
+            // If we're unlocked but still playing the occlude animation, remain at the keyguard
+            // alpha temporarily.
+            if (mKeyguardViewMediator.isOccludeAnimationPlaying()
+                    || mState.mLaunchingAffordanceWithPreview) {
+                mNotificationsAlpha = KEYGUARD_SCRIM_ALPHA;
+            }
         } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) {
             float behindFraction = getInterpolatedFraction();
             behindFraction = (float) Math.pow(behindFraction, 0.8f);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
index a0415f2..6cd8c78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt
@@ -22,8 +22,6 @@
 import com.android.internal.statusbar.LetterboxDetails
 import com.android.internal.view.AppearanceRegion
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
@@ -42,7 +40,6 @@
 @Inject
 internal constructor(
     private val centralSurfaces: CentralSurfaces,
-    private val featureFlags: FeatureFlags,
     private val letterboxAppearanceCalculator: LetterboxAppearanceCalculator,
     private val statusBarStateController: SysuiStatusBarStateController,
     private val lightBarController: LightBarController,
@@ -127,15 +124,11 @@
         }
 
     private fun shouldUseLetterboxAppearance(letterboxDetails: Array<LetterboxDetails>) =
-        isLetterboxAppearanceFlagEnabled() && letterboxDetails.isNotEmpty()
-
-    private fun isLetterboxAppearanceFlagEnabled() =
-        featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)
+        letterboxDetails.isNotEmpty()
 
     private fun dump(printWriter: PrintWriter, strings: Array<String>) {
         printWriter.println("lastSystemBarAttributesParams: $lastSystemBarAttributesParams")
         printWriter.println("lastLetterboxAppearance: $lastLetterboxAppearance")
-        printWriter.println("letterbox appearance flag: ${isLetterboxAppearanceFlagEnabled()}")
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 2aaa085..fcd1b8a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -18,10 +18,14 @@
 
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryImpl
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
@@ -41,10 +45,16 @@
     abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
 
     @Binds
-    abstract fun mobileSubscriptionRepository(
-        impl: MobileSubscriptionRepositoryImpl
-    ): MobileSubscriptionRepository
+    abstract fun mobileConnectionsRepository(
+        impl: MobileConnectionsRepositoryImpl
+    ): MobileConnectionsRepository
 
     @Binds
     abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository
+
+    @Binds
+    abstract fun mobileMappingsProxy(impl: MobileMappingsProxyImpl): MobileMappingsProxy
+
+    @Binds
+    abstract fun mobileIconsInteractor(impl: MobileIconsInteractorImpl): MobileIconsInteractor
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
index 46ccf32c..eaba0e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt
@@ -27,6 +27,7 @@
 import android.telephony.TelephonyCallback.SignalStrengthsListener
 import android.telephony.TelephonyDisplayInfo
 import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 
 /**
  * Data class containing all of the relevant information for a particular line of service, known as
@@ -57,6 +58,11 @@
     /** From [CarrierNetworkListener.onCarrierNetworkChange] */
     val carrierNetworkChangeActive: Boolean? = null,
 
-    /** From [DisplayInfoListener.onDisplayInfoChanged] */
-    val displayInfo: TelephonyDisplayInfo? = null
+    /**
+     * From [DisplayInfoListener.onDisplayInfoChanged].
+     *
+     * [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
+     * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
+     */
+    val resolvedNetworkType: ResolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
new file mode 100644
index 0000000..f385806
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.model
+
+import android.telephony.Annotation.NetworkType
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+
+/**
+ * A SysUI type to represent the [NetworkType] that we pull out of [TelephonyDisplayInfo]. Depending
+ * on whether or not the display info contains an override type, we may have to call different
+ * methods on [MobileMappingsProxy] to generate an icon lookup key.
+ */
+sealed interface ResolvedNetworkType {
+    @NetworkType val type: Int
+}
+
+data class DefaultNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
+
+data class OverrideNetworkType(@NetworkType override val type: Int) : ResolvedNetworkType
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
new file mode 100644
index 0000000..45284cf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrength
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
+import android.telephony.TelephonyManager
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import java.lang.IllegalStateException
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
+ * repository for each individual, tracked subscription via [MobileConnectionsRepository], and this
+ * repository is responsible for setting up a [TelephonyManager] object tied to its subscriptionId
+ *
+ * There should only ever be one [MobileConnectionRepository] per subscription, since
+ * [TelephonyManager] limits the number of callbacks that can be registered per process.
+ *
+ * This repository should have all of the relevant information for a single line of service, which
+ * eventually becomes a single icon in the status bar.
+ */
+interface MobileConnectionRepository {
+    /**
+     * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single
+     * listener + model.
+     */
+    val subscriptionModelFlow: Flow<MobileSubscriptionModel>
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+class MobileConnectionRepositoryImpl(
+    private val subId: Int,
+    telephonyManager: TelephonyManager,
+    bgDispatcher: CoroutineDispatcher,
+    logger: ConnectivityPipelineLogger,
+    scope: CoroutineScope,
+) : MobileConnectionRepository {
+    init {
+        if (telephonyManager.subscriptionId != subId) {
+            throw IllegalStateException(
+                "TelephonyManager should be created with subId($subId). " +
+                    "Found ${telephonyManager.subscriptionId} instead."
+            )
+        }
+    }
+
+    override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
+        var state = MobileSubscriptionModel()
+        conflatedCallbackFlow {
+                // TODO (b/240569788): log all of these into the connectivity logger
+                val callback =
+                    object :
+                        TelephonyCallback(),
+                        TelephonyCallback.ServiceStateListener,
+                        TelephonyCallback.SignalStrengthsListener,
+                        TelephonyCallback.DataConnectionStateListener,
+                        TelephonyCallback.DataActivityListener,
+                        TelephonyCallback.CarrierNetworkListener,
+                        TelephonyCallback.DisplayInfoListener {
+                        override fun onServiceStateChanged(serviceState: ServiceState) {
+                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
+                            trySend(state)
+                        }
+
+                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+                            val cdmaLevel =
+                                signalStrength
+                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
+                                    .let { strengths ->
+                                        if (!strengths.isEmpty()) {
+                                            strengths[0].level
+                                        } else {
+                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+                                        }
+                                    }
+
+                            val primaryLevel = signalStrength.level
+
+                            state =
+                                state.copy(
+                                    cdmaLevel = cdmaLevel,
+                                    primaryLevel = primaryLevel,
+                                    isGsm = signalStrength.isGsm,
+                                )
+                            trySend(state)
+                        }
+
+                        override fun onDataConnectionStateChanged(
+                            dataState: Int,
+                            networkType: Int
+                        ) {
+                            state = state.copy(dataConnectionState = dataState)
+                            trySend(state)
+                        }
+
+                        override fun onDataActivity(direction: Int) {
+                            state = state.copy(dataActivityDirection = direction)
+                            trySend(state)
+                        }
+
+                        override fun onCarrierNetworkChange(active: Boolean) {
+                            state = state.copy(carrierNetworkChangeActive = active)
+                            trySend(state)
+                        }
+
+                        override fun onDisplayInfoChanged(
+                            telephonyDisplayInfo: TelephonyDisplayInfo
+                        ) {
+                            val networkType =
+                                if (
+                                    telephonyDisplayInfo.overrideNetworkType ==
+                                        OVERRIDE_NETWORK_TYPE_NONE
+                                ) {
+                                    DefaultNetworkType(telephonyDisplayInfo.networkType)
+                                } else {
+                                    OverrideNetworkType(telephonyDisplayInfo.overrideNetworkType)
+                                }
+                            state = state.copy(resolvedNetworkType = networkType)
+                            trySend(state)
+                        }
+                    }
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .onEach { logger.logOutputChange("mobileSubscriptionModel", it.toString()) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+    }
+
+    class Factory
+    @Inject
+    constructor(
+        private val telephonyManager: TelephonyManager,
+        private val logger: ConnectivityPipelineLogger,
+        @Background private val bgDispatcher: CoroutineDispatcher,
+        @Application private val scope: CoroutineScope,
+    ) {
+        fun build(subId: Int): MobileConnectionRepository {
+            return MobileConnectionRepositoryImpl(
+                subId,
+                telephonyManager.createForSubscriptionId(subId),
+                bgDispatcher,
+                logger,
+                scope,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
new file mode 100644
index 0000000..0e2428a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.content.Context
+import android.content.IntentFilter
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.annotation.VisibleForTesting
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+/**
+ * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
+ * on various policy
+ */
+interface MobileConnectionsRepository {
+    /** Observable list of current mobile subscriptions */
+    val subscriptionsFlow: Flow<List<SubscriptionInfo>>
+
+    /** Observable for the subscriptionId of the current mobile data connection */
+    val activeMobileDataSubscriptionId: Flow<Int>
+
+    /** Observable for [MobileMappings.Config] tracking the defaults */
+    val defaultDataSubRatConfig: StateFlow<Config>
+
+    /** Get or create a repository for the line of service for the given subscription ID */
+    fun getRepoForSubId(subId: Int): MobileConnectionRepository
+}
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class MobileConnectionsRepositoryImpl
+@Inject
+constructor(
+    private val subscriptionManager: SubscriptionManager,
+    private val telephonyManager: TelephonyManager,
+    private val logger: ConnectivityPipelineLogger,
+    broadcastDispatcher: BroadcastDispatcher,
+    private val context: Context,
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    @Application private val scope: CoroutineScope,
+    private val mobileConnectionRepositoryFactory: MobileConnectionRepositoryImpl.Factory
+) : MobileConnectionsRepository {
+    private val subIdRepositoryCache: MutableMap<Int, MobileConnectionRepository> = mutableMapOf()
+
+    /**
+     * State flow that emits the set of mobile data subscriptions, each represented by its own
+     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
+     * info object, but for now we keep track of the infos themselves.
+     */
+    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
+                        override fun onSubscriptionsChanged() {
+                            trySend(Unit)
+                        }
+                    }
+
+                subscriptionManager.addOnSubscriptionsChangedListener(
+                    bgDispatcher.asExecutor(),
+                    callback,
+                )
+
+                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
+            }
+            .mapLatest { fetchSubscriptionsList() }
+            .onEach { infos -> dropUnusedReposFromCache(infos) }
+            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+
+    /** StateFlow that keeps track of the current active mobile data subscription */
+    override val activeMobileDataSubscriptionId: StateFlow<Int> =
+        conflatedCallbackFlow {
+                val callback =
+                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
+                            trySend(subId)
+                        }
+                    }
+
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+            }
+            .stateIn(
+                scope,
+                started = SharingStarted.WhileSubscribed(),
+                SubscriptionManager.INVALID_SUBSCRIPTION_ID
+            )
+
+    private val defaultDataSubChangedEvent =
+        broadcastDispatcher.broadcastFlow(
+            IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+        )
+
+    private val carrierConfigChangedEvent =
+        broadcastDispatcher.broadcastFlow(
+            IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+        )
+
+    /**
+     * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
+     * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
+     * config, so this will apply to every icon that we care about.
+     *
+     * Relevant bits in the config are things like
+     * [CarrierConfigManager.KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL]
+     *
+     * This flow will produce whenever the default data subscription or the carrier config changes.
+     */
+    override val defaultDataSubRatConfig: StateFlow<Config> =
+        combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ ->
+                Config.readConfig(context)
+            }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                initialValue = Config.readConfig(context)
+            )
+
+    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+        if (!isValidSubId(subId)) {
+            throw IllegalArgumentException(
+                "subscriptionId $subId is not in the list of valid subscriptions"
+            )
+        }
+
+        return subIdRepositoryCache[subId]
+            ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
+    }
+
+    private fun isValidSubId(subId: Int): Boolean {
+        subscriptionsFlow.value.forEach {
+            if (it.subscriptionId == subId) {
+                return true
+            }
+        }
+
+        return false
+    }
+
+    @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
+
+    private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
+        return mobileConnectionRepositoryFactory.build(subId)
+    }
+
+    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
+        // Remove any connection repository from the cache that isn't in the new set of IDs. They
+        // will get garbage collected once their subscribers go away
+        val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
+
+        subIdRepositoryCache.keys.forEach {
+            if (!currentValidSubscriptionIds.contains(it)) {
+                subIdRepositoryCache.remove(it)
+            }
+        }
+    }
+
+    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
+        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
deleted file mode 100644
index 36de2a2..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.CellSignalStrength
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyCallback.CarrierNetworkListener
-import android.telephony.TelephonyCallback.DataActivityListener
-import android.telephony.TelephonyCallback.DataConnectionStateListener
-import android.telephony.TelephonyCallback.DisplayInfoListener
-import android.telephony.TelephonyCallback.ServiceStateListener
-import android.telephony.TelephonyCallback.SignalStrengthsListener
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyManager
-import androidx.annotation.VisibleForTesting
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
-
-/**
- * Repo for monitoring the complete active subscription info list, to be consumed and filtered based
- * on various policy
- */
-interface MobileSubscriptionRepository {
-    /** Observable list of current mobile subscriptions */
-    val subscriptionsFlow: Flow<List<SubscriptionInfo>>
-
-    /** Observable for the subscriptionId of the current mobile data connection */
-    val activeMobileDataSubscriptionId: Flow<Int>
-
-    /** Get or create an observable for the given subscription ID */
-    fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel>
-}
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class MobileSubscriptionRepositoryImpl
-@Inject
-constructor(
-    private val subscriptionManager: SubscriptionManager,
-    private val telephonyManager: TelephonyManager,
-    @Background private val bgDispatcher: CoroutineDispatcher,
-    @Application private val scope: CoroutineScope,
-) : MobileSubscriptionRepository {
-    private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf()
-
-    /**
-     * State flow that emits the set of mobile data subscriptions, each represented by its own
-     * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each
-     * info object, but for now we keep track of the infos themselves.
-     */
-    override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : SubscriptionManager.OnSubscriptionsChangedListener() {
-                        override fun onSubscriptionsChanged() {
-                            trySend(Unit)
-                        }
-                    }
-
-                subscriptionManager.addOnSubscriptionsChangedListener(
-                    bgDispatcher.asExecutor(),
-                    callback,
-                )
-
-                awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
-            }
-            .mapLatest { fetchSubscriptionsList() }
-            .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
-
-    /** StateFlow that keeps track of the current active mobile data subscription */
-    override val activeMobileDataSubscriptionId: StateFlow<Int> =
-        conflatedCallbackFlow {
-                val callback =
-                    object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
-                        override fun onActiveDataSubscriptionIdChanged(subId: Int) {
-                            trySend(subId)
-                        }
-                    }
-
-                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
-                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
-            }
-            .stateIn(
-                scope,
-                started = SharingStarted.WhileSubscribed(),
-                SubscriptionManager.INVALID_SUBSCRIPTION_ID
-            )
-
-    /**
-     * Each mobile subscription needs its own flow, which comes from registering listeners on the
-     * system. Use this method to create those flows and cache them for reuse
-     */
-    override fun getFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> {
-        return subIdFlowCache[subId]
-            ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it }
-    }
-
-    @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache
-
-    private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run {
-        var state = MobileSubscriptionModel()
-        conflatedCallbackFlow {
-                val phony = telephonyManager.createForSubscriptionId(subId)
-                // TODO (b/240569788): log all of these into the connectivity logger
-                val callback =
-                    object :
-                        TelephonyCallback(),
-                        ServiceStateListener,
-                        SignalStrengthsListener,
-                        DataConnectionStateListener,
-                        DataActivityListener,
-                        CarrierNetworkListener,
-                        DisplayInfoListener {
-                        override fun onServiceStateChanged(serviceState: ServiceState) {
-                            state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly)
-                            trySend(state)
-                        }
-                        override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
-                            val cdmaLevel =
-                                signalStrength
-                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
-                                    .let { strengths ->
-                                        if (!strengths.isEmpty()) {
-                                            strengths[0].level
-                                        } else {
-                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
-                                        }
-                                    }
-
-                            val primaryLevel = signalStrength.level
-
-                            state =
-                                state.copy(
-                                    cdmaLevel = cdmaLevel,
-                                    primaryLevel = primaryLevel,
-                                    isGsm = signalStrength.isGsm,
-                                )
-                            trySend(state)
-                        }
-                        override fun onDataConnectionStateChanged(
-                            dataState: Int,
-                            networkType: Int
-                        ) {
-                            state = state.copy(dataConnectionState = dataState)
-                            trySend(state)
-                        }
-                        override fun onDataActivity(direction: Int) {
-                            state = state.copy(dataActivityDirection = direction)
-                            trySend(state)
-                        }
-                        override fun onCarrierNetworkChange(active: Boolean) {
-                            state = state.copy(carrierNetworkChangeActive = active)
-                            trySend(state)
-                        }
-                        override fun onDisplayInfoChanged(
-                            telephonyDisplayInfo: TelephonyDisplayInfo
-                        ) {
-                            state = state.copy(displayInfo = telephonyDisplayInfo)
-                            trySend(state)
-                        }
-                    }
-                phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
-                awaitClose {
-                    phony.unregisterTelephonyCallback(callback)
-                    // Release the cached flow
-                    subIdFlowCache.remove(subId)
-                }
-            }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
-    }
-
-    private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
-        withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 40fe0f3..15f4acc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -17,32 +17,58 @@
 package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
 
 import android.telephony.CarrierConfigManager
-import com.android.settingslib.SignalIcon
-import com.android.settingslib.mobile.TelephonyIcons
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 
 interface MobileIconInteractor {
-    /** Identifier for RAT type indicator */
-    val iconGroup: Flow<SignalIcon.MobileIconGroup>
+    /** Observable for RAT type (network type) indicator */
+    val networkTypeIconGroup: Flow<MobileIconGroup>
+
     /** True if this line of service is emergency-only */
     val isEmergencyOnly: Flow<Boolean>
+
     /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
     val level: Flow<Int>
+
     /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
     val numberOfLevels: Flow<Int>
+
     /** True when we want to draw an icon that makes room for the exclamation mark */
     val cutOut: Flow<Boolean>
 }
 
 /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
 class MobileIconInteractorImpl(
-    mobileStatusInfo: Flow<MobileSubscriptionModel>,
+    defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
+    defaultMobileIconGroup: Flow<MobileIconGroup>,
+    mobileMappingsProxy: MobileMappingsProxy,
+    connectionRepository: MobileConnectionRepository,
 ) : MobileIconInteractor {
-    override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G)
+    private val mobileStatusInfo = connectionRepository.subscriptionModelFlow
+
+    /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
+    override val networkTypeIconGroup: Flow<MobileIconGroup> =
+        combine(
+            mobileStatusInfo,
+            defaultMobileIconMapping,
+            defaultMobileIconGroup,
+        ) { info, mapping, defaultGroup ->
+            val lookupKey =
+                when (val resolved = info.resolvedNetworkType) {
+                    is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
+                    is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type)
+                }
+            mapping[lookupKey] ?: defaultGroup
+        }
+
     override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
 
     override val level: Flow<Int> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 8e67e19..cd411a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -19,29 +19,51 @@
 import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 
 /**
- * Business layer logic for mobile subscription icons
+ * Business layer logic for the set of mobile subscription icons.
  *
- * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s
- * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs
+ * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]).
+ * The list of subscriptions is filtered based on the opportunistic flags on the infos.
+ *
+ * It provides the default mapping between the telephony display info and the icon group that
+ * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
+ * icon
  */
+interface MobileIconsInteractor {
+    val filteredSubscriptions: Flow<List<SubscriptionInfo>>
+    val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
+    val defaultMobileIconGroup: Flow<MobileIconGroup>
+    val isUserSetup: Flow<Boolean>
+    fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor
+}
+
 @SysUISingleton
-class MobileIconsInteractor
+class MobileIconsInteractorImpl
 @Inject
 constructor(
-    private val mobileSubscriptionRepo: MobileSubscriptionRepository,
+    private val mobileSubscriptionRepo: MobileConnectionsRepository,
     private val carrierConfigTracker: CarrierConfigTracker,
+    private val mobileMappingsProxy: MobileMappingsProxy,
     userSetupRepo: UserSetupRepository,
-) {
+    @Application private val scope: CoroutineScope,
+) : MobileIconsInteractor {
     private val activeMobileDataSubscriptionId =
         mobileSubscriptionRepo.activeMobileDataSubscriptionId
 
@@ -61,7 +83,7 @@
      * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
      * and by checking which subscription is opportunistic, or which one is active.
      */
-    val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
+    override val filteredSubscriptions: Flow<List<SubscriptionInfo>> =
         combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId
             ->
             // Based on the old logic,
@@ -92,15 +114,29 @@
             }
         }
 
-    val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
+    /**
+     * Mapping from network type to [MobileIconGroup] using the config generated for the default
+     * subscription Id. This mapping is the same for every subscription.
+     */
+    override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
+        mobileSubscriptionRepo.defaultDataSubRatConfig
+            .map { mobileMappingsProxy.mapIconSets(it) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
+
+    /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
+    override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
+        mobileSubscriptionRepo.defaultDataSubRatConfig
+            .map { mobileMappingsProxy.getDefaultIcons(it) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G)
+
+    override val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow
 
     /** Vends out new [MobileIconInteractor] for a particular subId */
-    fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
-        MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId))
-
-    /**
-     * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections
-     */
-    private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> =
-        mobileSubscriptionRepo.getFlowForSubId(subId)
+    override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
+        MobileIconInteractorImpl(
+            defaultMobileIconMapping,
+            defaultMobileIconGroup,
+            mobileMappingsProxy,
+            mobileSubscriptionRepo.getRepoForSubId(subId),
+        )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 1405b05..67ea139 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.binder
 
 import android.content.res.ColorStateList
+import android.view.View.GONE
+import android.view.View.VISIBLE
 import android.view.ViewGroup
 import android.widget.ImageView
 import androidx.core.view.isVisible
@@ -24,6 +26,7 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.settingslib.graph.SignalDrawable
 import com.android.systemui.R
+import com.android.systemui.common.ui.binder.IconViewBinder
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
 import kotlinx.coroutines.flow.collect
@@ -37,6 +40,7 @@
         view: ViewGroup,
         viewModel: MobileIconViewModel,
     ) {
+        val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
         val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
         val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
 
@@ -52,10 +56,20 @@
                     }
                 }
 
+                // Set the network type icon
+                launch {
+                    viewModel.networkTypeIcon.distinctUntilChanged().collect { dataTypeId ->
+                        dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) }
+                        networkTypeView.visibility = if (dataTypeId != null) VISIBLE else GONE
+                    }
+                }
+
                 // Set the tint
                 launch {
                     viewModel.tint.collect { tint ->
-                        iconView.imageTintList = ColorStateList.valueOf(tint)
+                        val tintList = ColorStateList.valueOf(tint)
+                        iconView.imageTintList = tintList
+                        networkTypeView.imageTintList = tintList
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index cfabeba..cc8f6dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -18,6 +18,8 @@
 
 import android.graphics.Color
 import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
@@ -26,6 +28,7 @@
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 
 /**
  * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -54,5 +57,15 @@
             .distinctUntilChanged()
             .logOutputChange(logger, "iconId($subscriptionId)")
 
+    /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
+    var networkTypeIcon: Flow<Icon?> =
+        iconInteractor.networkTypeIconGroup.map {
+            val desc =
+                if (it.dataContentDescription != 0)
+                    ContentDescription.Resource(it.dataContentDescription)
+                else null
+            Icon.Resource(it.dataType, desc)
+        }
+
     var tint: Flow<Int> = flowOf(Color.CYAN)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
new file mode 100644
index 0000000..60bd038
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/MobileMappings.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.util
+
+import android.telephony.Annotation.NetworkType
+import android.telephony.TelephonyDisplayInfo
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.MobileMappings.Config
+import javax.inject.Inject
+
+/**
+ * [MobileMappings] owns the logic on creating the map from [TelephonyDisplayInfo] to
+ * [MobileIconGroup]. It creates that hash map and also manages the creation of lookup keys. This
+ * interface allows us to proxy those calls to the static java methods in SettingsLib and also fake
+ * them out in tests
+ */
+interface MobileMappingsProxy {
+    fun mapIconSets(config: Config): Map<String, MobileIconGroup>
+    fun getDefaultIcons(config: Config): MobileIconGroup
+    fun toIconKey(@NetworkType networkType: Int): String
+    fun toIconKeyOverride(@NetworkType networkType: Int): String
+}
+
+/** Injectable wrapper class for [MobileMappings] */
+class MobileMappingsProxyImpl @Inject constructor() : MobileMappingsProxy {
+    override fun mapIconSets(config: Config): Map<String, MobileIconGroup> =
+        MobileMappings.mapIconSets(config)
+
+    override fun getDefaultIcons(config: Config): MobileIconGroup =
+        MobileMappings.getDefaultIcons(config)
+
+    override fun toIconKey(@NetworkType networkType: Int): String =
+        MobileMappings.toIconKey(networkType)
+
+    override fun toIconKeyOverride(networkType: Int): String =
+        MobileMappings.toDisplayIconKey(networkType)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
index 28a9b97..cf4106c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -61,7 +61,7 @@
      * animation to and from the parent dialog.
      */
     @JvmOverloads
-    fun onUserListItemClicked(
+    open fun onUserListItemClicked(
         record: UserRecord,
         dialogShower: DialogShower? = null,
     ) {
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index cd7bd2d..b8930a4 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -204,5 +204,4 @@
     }
 }
 
-const val SENDER_TAG = "MediaTapToTransferSender"
 private const val ANIMATION_DURATION = 500L
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
new file mode 100644
index 0000000..9653985
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -0,0 +1,40 @@
+/*
+ *  Copyright (C) 2022 The Android Open Source Project
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+package com.android.systemui.util.kotlin
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import java.util.function.Consumer
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to
+ * [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners
+ * during onViewAttached() and removing during onViewRemoved()
+ */
+@JvmOverloads
+fun <T> collectFlow(
+    view: View,
+    flow: Flow<T>,
+    consumer: Consumer<T>,
+    state: Lifecycle.State = Lifecycle.State.CREATED,
+) {
+    view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
index 42d7d52..44f6d03 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/ImageWallpaper.java
@@ -47,7 +47,7 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.wallpapers.canvas.WallpaperColorExtractor;
+import com.android.systemui.wallpapers.canvas.WallpaperLocalColorExtractor;
 import com.android.systemui.wallpapers.gl.EglHelper;
 import com.android.systemui.wallpapers.gl.ImageWallpaperRenderer;
 
@@ -521,7 +521,7 @@
 
     class CanvasEngine extends WallpaperService.Engine implements DisplayListener {
         private WallpaperManager mWallpaperManager;
-        private final WallpaperColorExtractor mWallpaperColorExtractor;
+        private final WallpaperLocalColorExtractor mWallpaperLocalColorExtractor;
         private SurfaceHolder mSurfaceHolder;
         @VisibleForTesting
         static final int MIN_SURFACE_WIDTH = 128;
@@ -543,9 +543,9 @@
             super();
             setFixedSizeAllowed(true);
             setShowForAllUsers(true);
-            mWallpaperColorExtractor = new WallpaperColorExtractor(
+            mWallpaperLocalColorExtractor = new WallpaperLocalColorExtractor(
                     mBackgroundExecutor,
-                    new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
+                    new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() {
                         @Override
                         public void onColorsProcessed(List<RectF> regions,
                                 List<WallpaperColors> colors) {
@@ -570,7 +570,7 @@
 
             // if the number of pages is already computed, transmit it to the color extractor
             if (mPagesComputed) {
-                mWallpaperColorExtractor.onPageChanged(mPages);
+                mWallpaperLocalColorExtractor.onPageChanged(mPages);
             }
         }
 
@@ -597,7 +597,7 @@
         public void onDestroy() {
             getDisplayContext().getSystemService(DisplayManager.class)
                     .unregisterDisplayListener(this);
-            mWallpaperColorExtractor.cleanUp();
+            mWallpaperLocalColorExtractor.cleanUp();
             unloadBitmap();
         }
 
@@ -813,7 +813,7 @@
 
         @VisibleForTesting
         void recomputeColorExtractorMiniBitmap() {
-            mWallpaperColorExtractor.onBitmapChanged(mBitmap);
+            mWallpaperLocalColorExtractor.onBitmapChanged(mBitmap);
         }
 
         @VisibleForTesting
@@ -830,14 +830,14 @@
         public void addLocalColorsAreas(@NonNull List<RectF> regions) {
             // this call will activate the offset notifications
             // if no colors were being processed before
-            mWallpaperColorExtractor.addLocalColorsAreas(regions);
+            mWallpaperLocalColorExtractor.addLocalColorsAreas(regions);
         }
 
         @Override
         public void removeLocalColorsAreas(@NonNull List<RectF> regions) {
             // this call will deactivate the offset notifications
             // if we are no longer processing colors
-            mWallpaperColorExtractor.removeLocalColorAreas(regions);
+            mWallpaperLocalColorExtractor.removeLocalColorAreas(regions);
         }
 
         @Override
@@ -853,7 +853,7 @@
             if (pages != mPages || !mPagesComputed) {
                 mPages = pages;
                 mPagesComputed = true;
-                mWallpaperColorExtractor.onPageChanged(mPages);
+                mWallpaperLocalColorExtractor.onPageChanged(mPages);
             }
         }
 
@@ -881,7 +881,7 @@
                     .getSystemService(WindowManager.class)
                     .getCurrentWindowMetrics()
                     .getBounds();
-            mWallpaperColorExtractor.setDisplayDimensions(window.width(), window.height());
+            mWallpaperLocalColorExtractor.setDisplayDimensions(window.width(), window.height());
         }
 
 
@@ -902,7 +902,7 @@
                     : mBitmap.isRecycled() ? "recycled"
                     : mBitmap.getWidth() + "x" + mBitmap.getHeight());
 
-            mWallpaperColorExtractor.dump(prefix, fd, out, args);
+            mWallpaperLocalColorExtractor.dump(prefix, fd, out, args);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java
rename to packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java
index e2e4555..6cac5c9 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractor.java
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractor.java
@@ -45,14 +45,14 @@
  * It uses a background executor, and uses callbacks to inform that the work is done.
  * It uses  a downscaled version of the wallpaper to extract the colors.
  */
-public class WallpaperColorExtractor {
+public class WallpaperLocalColorExtractor {
 
     private Bitmap mMiniBitmap;
 
     @VisibleForTesting
     static final int SMALL_SIDE = 128;
 
-    private static final String TAG = WallpaperColorExtractor.class.getSimpleName();
+    private static final String TAG = WallpaperLocalColorExtractor.class.getSimpleName();
     private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
             new RectF(0, 0, 1, 1);
 
@@ -70,12 +70,12 @@
     @Background
     private final Executor mBackgroundExecutor;
 
-    private final WallpaperColorExtractorCallback mWallpaperColorExtractorCallback;
+    private final WallpaperLocalColorExtractorCallback mWallpaperLocalColorExtractorCallback;
 
     /**
      * Interface to handle the callbacks after the different steps of the color extraction
      */
-    public interface WallpaperColorExtractorCallback {
+    public interface WallpaperLocalColorExtractorCallback {
         /**
          * Callback after the colors of new regions have been extracted
          * @param regions the list of new regions that have been processed
@@ -103,13 +103,13 @@
     /**
      * Creates a new color extractor.
      * @param backgroundExecutor the executor on which the color extraction will be performed
-     * @param wallpaperColorExtractorCallback an interface to handle the callbacks from
+     * @param wallpaperLocalColorExtractorCallback an interface to handle the callbacks from
      *                                        the color extractor.
      */
-    public WallpaperColorExtractor(@Background Executor backgroundExecutor,
-            WallpaperColorExtractorCallback wallpaperColorExtractorCallback) {
+    public WallpaperLocalColorExtractor(@Background Executor backgroundExecutor,
+            WallpaperLocalColorExtractorCallback wallpaperLocalColorExtractorCallback) {
         mBackgroundExecutor = backgroundExecutor;
-        mWallpaperColorExtractorCallback = wallpaperColorExtractorCallback;
+        mWallpaperLocalColorExtractorCallback = wallpaperLocalColorExtractorCallback;
     }
 
     /**
@@ -157,7 +157,7 @@
             mBitmapWidth = bitmap.getWidth();
             mBitmapHeight = bitmap.getHeight();
             mMiniBitmap = createMiniBitmap(bitmap);
-            mWallpaperColorExtractorCallback.onMiniBitmapUpdated();
+            mWallpaperLocalColorExtractorCallback.onMiniBitmapUpdated();
             recomputeColors();
         }
     }
@@ -206,7 +206,7 @@
             boolean wasActive = isActive();
             mPendingRegions.addAll(regions);
             if (!wasActive && isActive()) {
-                mWallpaperColorExtractorCallback.onActivated();
+                mWallpaperLocalColorExtractorCallback.onActivated();
             }
             processColorsInternal();
         }
@@ -228,7 +228,7 @@
             mPendingRegions.removeAll(regions);
             regions.forEach(mProcessedRegions::remove);
             if (wasActive && !isActive()) {
-                mWallpaperColorExtractorCallback.onDeactivated();
+                mWallpaperLocalColorExtractorCallback.onDeactivated();
             }
         }
     }
@@ -252,7 +252,7 @@
     }
 
     private Bitmap createMiniBitmap(@NonNull Bitmap bitmap) {
-        Trace.beginSection("WallpaperColorExtractor#createMiniBitmap");
+        Trace.beginSection("WallpaperLocalColorExtractor#createMiniBitmap");
         // if both sides of the image are larger than SMALL_SIDE, downscale the bitmap.
         int smallestSide = Math.min(bitmap.getWidth(), bitmap.getHeight());
         float scale = Math.min(1.0f, (float) SMALL_SIDE / smallestSide);
@@ -359,7 +359,7 @@
          */
         if (mDisplayWidth < 0 || mDisplayHeight < 0 || mPages < 0) return;
 
-        Trace.beginSection("WallpaperColorExtractor#processColorsInternal");
+        Trace.beginSection("WallpaperLocalColorExtractor#processColorsInternal");
         List<WallpaperColors> processedColors = new ArrayList<>();
         for (int i = 0; i < mPendingRegions.size(); i++) {
             RectF nextArea = mPendingRegions.get(i);
@@ -372,7 +372,7 @@
         mPendingRegions.clear();
         Trace.endSection();
 
-        mWallpaperColorExtractorCallback.onColorsProcessed(processedRegions, processedColors);
+        mWallpaperLocalColorExtractorCallback.onColorsProcessed(processedRegions, processedColors);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 309f168..02738d5 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -49,6 +49,7 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.notetask.NoteTaskInitializer;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.tracing.ProtoTraceable;
 import com.android.systemui.statusbar.CommandQueue;
@@ -58,7 +59,6 @@
 import com.android.systemui.tracing.nano.SystemUiTraceProto;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
-import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.nano.WmShellTraceProto;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.onehanded.OneHandedEventCallback;
@@ -113,7 +113,6 @@
     private final Optional<Pip> mPipOptional;
     private final Optional<SplitScreen> mSplitScreenOptional;
     private final Optional<OneHanded> mOneHandedOptional;
-    private final Optional<FloatingTasks> mFloatingTasksOptional;
     private final Optional<DesktopMode> mDesktopModeOptional;
 
     private final CommandQueue mCommandQueue;
@@ -125,6 +124,7 @@
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private final ProtoTracer mProtoTracer;
     private final UserTracker mUserTracker;
+    private final NoteTaskInitializer mNoteTaskInitializer;
     private final Executor mSysUiMainExecutor;
 
     // Listeners and callbacks. Note that we prefer member variable over anonymous class here to
@@ -176,7 +176,6 @@
             Optional<Pip> pipOptional,
             Optional<SplitScreen> splitScreenOptional,
             Optional<OneHanded> oneHandedOptional,
-            Optional<FloatingTasks> floatingTasksOptional,
             Optional<DesktopMode> desktopMode,
             CommandQueue commandQueue,
             ConfigurationController configurationController,
@@ -187,6 +186,7 @@
             ProtoTracer protoTracer,
             WakefulnessLifecycle wakefulnessLifecycle,
             UserTracker userTracker,
+            NoteTaskInitializer noteTaskInitializer,
             @Main Executor sysUiMainExecutor) {
         mContext = context;
         mShell = shell;
@@ -203,7 +203,7 @@
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mProtoTracer = protoTracer;
         mUserTracker = userTracker;
-        mFloatingTasksOptional = floatingTasksOptional;
+        mNoteTaskInitializer = noteTaskInitializer;
         mSysUiMainExecutor = sysUiMainExecutor;
     }
 
@@ -226,6 +226,8 @@
         mSplitScreenOptional.ifPresent(this::initSplitScreen);
         mOneHandedOptional.ifPresent(this::initOneHanded);
         mDesktopModeOptional.ifPresent(this::initDesktopMode);
+
+        mNoteTaskInitializer.initialize();
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt
new file mode 100644
index 0000000..6c5620d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/FaceWakeUpTriggersConfigTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.os.PowerManager
+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.settings.GlobalSettings
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class FaceWakeUpTriggersConfigTest : SysuiTestCase() {
+    @Mock lateinit var globalSettings: GlobalSettings
+    @Mock lateinit var dumpManager: DumpManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testShouldTriggerFaceAuthOnWakeUpFrom_inConfig_returnsTrue() {
+        val faceWakeUpTriggersConfig =
+            createFaceWakeUpTriggersConfig(
+                intArrayOf(PowerManager.WAKE_REASON_POWER_BUTTON, PowerManager.WAKE_REASON_GESTURE)
+            )
+
+        assertTrue(
+            faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+                PowerManager.WAKE_REASON_POWER_BUTTON
+            )
+        )
+        assertTrue(
+            faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+                PowerManager.WAKE_REASON_GESTURE
+            )
+        )
+        assertFalse(
+            faceWakeUpTriggersConfig.shouldTriggerFaceAuthOnWakeUpFrom(
+                PowerManager.WAKE_REASON_APPLICATION
+            )
+        )
+    }
+
+    private fun createFaceWakeUpTriggersConfig(wakeUpTriggers: IntArray): FaceWakeUpTriggersConfig {
+        overrideResource(
+            com.android.systemui.R.array.config_face_auth_wake_up_triggers,
+            wakeUpTriggers
+        )
+
+        return FaceWakeUpTriggersConfig(mContext.getResources(), globalSettings, dumpManager)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 627d738..61c7bb5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -44,7 +44,6 @@
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.plugins.ClockAnimations;
 import com.android.systemui.plugins.ClockController;
@@ -105,8 +104,6 @@
     private FrameLayout mLargeClockFrame;
     @Mock
     private SecureSettings mSecureSettings;
-    @Mock
-    private FeatureFlags mFeatureFlags;
 
     private final View mFakeSmartspaceView = new View(mContext);
 
@@ -143,8 +140,7 @@
                 mSecureSettings,
                 mExecutor,
                 mDumpManager,
-                mClockEventController,
-                mFeatureFlags
+                mClockEventController
         );
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index ebfb4d4..1238eaf 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -109,6 +109,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.telephony.TelephonyListenerManager;
+import com.android.systemui.util.settings.GlobalSettings;
 
 import org.junit.After;
 import org.junit.Assert;
@@ -209,6 +210,9 @@
     private UiEventLogger mUiEventLogger;
     @Mock
     private PowerManager mPowerManager;
+    @Mock
+    private GlobalSettings mGlobalSettings;
+    private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
 
     private final int mCurrentUserId = 100;
     private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0);
@@ -293,6 +297,12 @@
                 .when(ActivityManager::getCurrentUser);
         ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService);
 
+        mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig(
+                mContext.getResources(),
+                mGlobalSettings,
+                mDumpManager
+        );
+
         mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
         mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
@@ -606,16 +616,22 @@
 
     @Test
     public void testTriesToAuthenticate_whenKeyguard() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
-        mTestableLooper.processAllMessages();
         keyguardIsVisible();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mTestableLooper.processAllMessages();
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+        verify(mUiEventLogger).logWithInstanceIdAndPosition(
+                eq(FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP),
+                eq(0),
+                eq(null),
+                any(),
+                eq(PowerManager.WAKE_REASON_POWER_BUTTON));
     }
 
     @Test
     public void skipsAuthentication_whenStatusBarShadeLocked() {
         mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED);
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
 
         keyguardIsVisible();
@@ -629,7 +645,7 @@
                 STRONG_AUTH_REQUIRED_AFTER_BOOT);
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
 
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
         verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
@@ -683,7 +699,7 @@
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
         when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(strongAuth);
 
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
@@ -708,7 +724,7 @@
     @Test
     public void testTriesToAuthenticate_whenTrustOnAgentKeyguard_ifBypass() {
         mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         when(mKeyguardBypassController.canBypass()).thenReturn(true);
         mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */,
@@ -720,7 +736,7 @@
 
     @Test
     public void testIgnoresAuth_whenTrustAgentOnKeyguard_withoutBypass() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         mKeyguardUpdateMonitor.onTrustChanged(true /* enabled */,
                 KeyguardUpdateMonitor.getCurrentUser(), 0 /* flags */, new ArrayList<>());
@@ -731,7 +747,7 @@
 
     @Test
     public void testIgnoresAuth_whenLockdown() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
@@ -743,7 +759,7 @@
 
     @Test
     public void testTriesToAuthenticate_whenLockout() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         when(mStrongAuthTracker.getStrongAuthForUser(anyInt())).thenReturn(
                 KeyguardUpdateMonitor.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
@@ -767,7 +783,7 @@
 
     @Test
     public void testFaceAndFingerprintLockout_onlyFace() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -778,7 +794,7 @@
 
     @Test
     public void testFaceAndFingerprintLockout_onlyFingerprint() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -790,7 +806,7 @@
 
     @Test
     public void testFaceAndFingerprintLockout() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -889,7 +905,7 @@
         when(mFaceManager.getLockoutModeForUser(eq(FACE_SENSOR_ID), eq(newUser)))
                 .thenReturn(faceLockoutMode);
 
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -1063,7 +1079,7 @@
     @Test
     public void testOccludingAppFingerprintListeningState() {
         // GIVEN keyguard isn't visible (app occluding)
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
         when(mStrongAuthTracker.hasUserAuthenticatedSinceBoot()).thenReturn(true);
 
@@ -1078,7 +1094,7 @@
     @Test
     public void testOccludingAppRequestsFingerprint() {
         // GIVEN keyguard isn't visible (app occluding)
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mKeyguardUpdateMonitor.setKeyguardShowing(true, true);
 
         // WHEN an occluding app requests fp
@@ -1169,7 +1185,7 @@
         biometricsNotDisabledThroughDevicePolicyManager();
         mStatusBarStateListener.onStateChanged(StatusBarState.SHADE_LOCKED);
         setKeyguardBouncerVisibility(false /* isVisible */);
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         when(mKeyguardBypassController.canBypass()).thenReturn(true);
         keyguardIsVisible();
 
@@ -1548,7 +1564,7 @@
 
     @Test
     public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
         mTestableLooper.processAllMessages();
         keyguardIsVisible();
 
@@ -1597,6 +1613,36 @@
         verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
     }
 
+    @Test
+    public void testDreamingStopped_faceDoesNotRun() {
+        mKeyguardUpdateMonitor.dispatchDreamingStopped();
+        mTestableLooper.processAllMessages();
+
+        verify(mFaceManager, never()).authenticate(
+                any(), any(), any(), any(), anyInt(), anyBoolean());
+    }
+
+    @Test
+    public void testFaceWakeupTrigger_runFaceAuth_onlyOnConfiguredTriggers() {
+        // keyguard is visible
+        keyguardIsVisible();
+
+        // WHEN device wakes up from an application
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_APPLICATION);
+        mTestableLooper.processAllMessages();
+
+        // THEN face auth isn't triggered
+        verify(mFaceManager, never()).authenticate(
+                any(), any(), any(), any(), anyInt(), anyBoolean());
+
+        // WHEN device wakes up from the power button
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mTestableLooper.processAllMessages();
+
+        // THEN face auth is triggered
+        verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
+    }
+
     private void cleanupKeyguardUpdateMonitor() {
         if (mKeyguardUpdateMonitor != null) {
             mKeyguardUpdateMonitor.removeCallback(mTestCallback);
@@ -1717,7 +1763,7 @@
     }
 
     private void deviceIsInteractive() {
-        mKeyguardUpdateMonitor.dispatchStartedWakingUp();
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
     }
 
     private void bouncerFullyVisible() {
@@ -1767,7 +1813,8 @@
                     mKeyguardUpdateMonitorLogger, mUiEventLogger, () -> mSessionTracker,
                     mPowerManager, mTrustManager, mSubscriptionManager, mUserManager,
                     mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager,
-                    mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager);
+                    mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager,
+                    mFaceWakeUpTriggersConfig);
             setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
new file mode 100644
index 0000000..ae8f419
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.AnimatedStateListDrawable;
+import android.util.Pair;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.AuthController;
+import com.android.systemui.biometrics.AuthRippleController;
+import com.android.systemui.doze.util.BurnInHelperKt;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.After;
+import org.junit.Before;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+public class LockIconViewControllerBaseTest extends SysuiTestCase {
+    protected static final String UNLOCKED_LABEL = "unlocked";
+    protected static final int PADDING = 10;
+
+    protected MockitoSession mStaticMockSession;
+
+    protected @Mock LockIconView mLockIconView;
+    protected @Mock AnimatedStateListDrawable mIconDrawable;
+    protected @Mock Context mContext;
+    protected @Mock Resources mResources;
+    protected @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager;
+    protected @Mock StatusBarStateController mStatusBarStateController;
+    protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    protected @Mock KeyguardViewController mKeyguardViewController;
+    protected @Mock KeyguardStateController mKeyguardStateController;
+    protected @Mock FalsingManager mFalsingManager;
+    protected @Mock AuthController mAuthController;
+    protected @Mock DumpManager mDumpManager;
+    protected @Mock AccessibilityManager mAccessibilityManager;
+    protected @Mock ConfigurationController mConfigurationController;
+    protected @Mock VibratorHelper mVibrator;
+    protected @Mock AuthRippleController mAuthRippleController;
+    protected @Mock FeatureFlags mFeatureFlags;
+    protected @Mock KeyguardTransitionRepository mTransitionRepository;
+    protected FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
+
+    protected LockIconViewController mUnderTest;
+
+    // Capture listeners so that they can be used to send events
+    @Captor protected ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
+            ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+
+    @Captor protected ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
+            ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
+    protected KeyguardStateController.Callback mKeyguardStateCallback;
+
+    @Captor protected ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
+            ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
+    protected StatusBarStateController.StateListener mStatusBarStateListener;
+
+    @Captor protected ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
+    protected AuthController.Callback mAuthControllerCallback;
+
+    @Captor protected ArgumentCaptor<KeyguardUpdateMonitorCallback>
+            mKeyguardUpdateMonitorCallbackCaptor =
+            ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
+    protected KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
+
+    @Captor protected ArgumentCaptor<Point> mPointCaptor;
+
+    @Before
+    public void setUp() throws Exception {
+        mStaticMockSession = mockitoSession()
+                .mockStatic(BurnInHelperKt.class)
+                .strictness(Strictness.LENIENT)
+                .startMocking();
+        MockitoAnnotations.initMocks(this);
+
+        setupLockIconViewMocks();
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
+        Rect windowBounds = new Rect(0, 0, 800, 1200);
+        when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds);
+        when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
+        when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
+        when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING);
+        when(mAuthController.getScaleFactor()).thenReturn(1f);
+
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+
+        mUnderTest = new LockIconViewController(
+                mLockIconView,
+                mStatusBarStateController,
+                mKeyguardUpdateMonitor,
+                mKeyguardViewController,
+                mKeyguardStateController,
+                mFalsingManager,
+                mAuthController,
+                mDumpManager,
+                mAccessibilityManager,
+                mConfigurationController,
+                mDelayableExecutor,
+                mVibrator,
+                mAuthRippleController,
+                mResources,
+                new KeyguardTransitionInteractor(mTransitionRepository),
+                new KeyguardInteractor(new FakeKeyguardRepository()),
+                mFeatureFlags
+        );
+    }
+
+    @After
+    public void tearDown() {
+        mStaticMockSession.finishMocking();
+    }
+
+    protected Pair<Float, Point> setupUdfps() {
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true);
+        final Point udfpsLocation = new Point(50, 75);
+        final float radius = 33f;
+        when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation);
+        when(mAuthController.getUdfpsRadius()).thenReturn(radius);
+
+        return new Pair(radius, udfpsLocation);
+    }
+
+    protected void setupShowLockIcon() {
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
+        when(mStatusBarStateController.isDozing()).thenReturn(false);
+        when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
+        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
+    }
+
+    protected void captureAuthControllerCallback() {
+        verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
+        mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
+    }
+
+    protected void captureKeyguardStateCallback() {
+        verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
+        mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
+    }
+
+    protected void captureStatusBarStateListener() {
+        verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
+        mStatusBarStateListener = mStatusBarStateCaptor.getValue();
+    }
+
+    protected void captureKeyguardUpdateMonitorCallback() {
+        verify(mKeyguardUpdateMonitor).registerCallback(
+                mKeyguardUpdateMonitorCallbackCaptor.capture());
+        mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
+    }
+
+    protected void setupLockIconViewMocks() {
+        when(mLockIconView.getResources()).thenReturn(mResources);
+        when(mLockIconView.getContext()).thenReturn(mContext);
+    }
+
+    protected void resetLockIconView() {
+        reset(mLockIconView);
+        setupLockIconViewMocks();
+    }
+
+    protected void init(boolean useMigrationFlag) {
+        when(mFeatureFlags.isEnabled(DOZING_MIGRATION_1)).thenReturn(useMigrationFlag);
+        mUnderTest.init();
+
+        verify(mLockIconView, atLeast(1)).addOnAttachStateChangeListener(mAttachCaptor.capture());
+        mAttachCaptor.getValue().onViewAttachedToWindow(mLockIconView);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
new file mode 100644
index 0000000..da40595
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.keyguard;
+
+import static com.android.keyguard.LockIconView.ICON_LOCK;
+import static com.android.keyguard.LockIconView.ICON_UNLOCK;
+
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Point;
+import android.hardware.biometrics.BiometricSourceType;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.doze.util.BurnInHelperKt;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class LockIconViewControllerTest extends LockIconViewControllerBaseTest {
+
+    @Test
+    public void testUpdateFingerprintLocationOnInit() {
+        // GIVEN fp sensor location is available pre-attached
+        Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
+
+        // WHEN lock icon view controller is initialized and attached
+        init(/* useMigrationFlag= */false);
+
+        // THEN lock icon view location is updated to the udfps location with UDFPS radius
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING));
+    }
+
+    @Test
+    public void testUpdatePaddingBasedOnResolutionScale() {
+        // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5
+        Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
+        when(mAuthController.getScaleFactor()).thenReturn(5f);
+
+        // WHEN lock icon view controller is initialized and attached
+        init(/* useMigrationFlag= */false);
+
+        // THEN lock icon view location is updated with the scaled radius
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING * 5));
+    }
+
+    @Test
+    public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
+        // GIVEN fp sensor location is not available pre-init
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+        init(/* useMigrationFlag= */false);
+        resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+        // GIVEN fp sensor location is available post-attached
+        captureAuthControllerCallback();
+        Pair<Float, Point> udfps = setupUdfps();
+
+        // WHEN all authenticators are registered
+        mAuthControllerCallback.onAllAuthenticatorsRegistered();
+        mDelayableExecutor.runAllReady();
+
+        // THEN lock icon view location is updated with the same coordinates as auth controller vals
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING));
+    }
+
+    @Test
+    public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
+        // GIVEN fp sensor location is not available pre-init
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
+        init(/* useMigrationFlag= */false);
+        resetLockIconView(); // reset any method call counts for when we verify method calls later
+
+        // GIVEN fp sensor location is available post-attached
+        captureAuthControllerCallback();
+        Pair<Float, Point> udfps = setupUdfps();
+
+        // WHEN udfps location changes
+        mAuthControllerCallback.onUdfpsLocationChanged();
+        mDelayableExecutor.runAllReady();
+
+        // THEN lock icon view location is updated with the same coordinates as auth controller vals
+        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
+                eq(PADDING));
+    }
+
+    @Test
+    public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
+        // GIVEN Udpfs sensor location is available
+        setupUdfps();
+
+        // WHEN the view is attached
+        init(/* useMigrationFlag= */false);
+
+        // THEN the lock icon view background should be enabled
+        verify(mLockIconView).setUseBackground(true);
+    }
+
+    @Test
+    public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() {
+        // GIVEN Udfps sensor location is not supported
+        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
+
+        // WHEN the view is attached
+        init(/* useMigrationFlag= */false);
+
+        // THEN the lock icon view background should be disabled
+        verify(mLockIconView).setUseBackground(false);
+    }
+
+    @Test
+    public void testUnlockIconShows_biometricUnlockedTrue() {
+        // GIVEN UDFPS sensor location is available
+        setupUdfps();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureKeyguardUpdateMonitorCallback();
+
+        // GIVEN user has unlocked with a biometric auth (ie: face auth)
+        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
+        reset(mLockIconView);
+
+        // WHEN face auth's biometric running state changes
+        mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
+                BiometricSourceType.FACE);
+
+        // THEN the unlock icon is shown
+        verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
+    }
+
+    @Test
+    public void testLockIconStartState() {
+        // GIVEN lock icon state
+        setupShowLockIcon();
+
+        // WHEN lock icon controller is initialized
+        init(/* useMigrationFlag= */false);
+
+        // THEN the lock icon should show
+        verify(mLockIconView).updateIcon(ICON_LOCK, false);
+    }
+
+    @Test
+    public void testLockIcon_updateToUnlock() {
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureKeyguardStateCallback();
+        reset(mLockIconView);
+
+        // WHEN the unlocked state changes to canDismissLockScreen=true
+        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
+        mKeyguardStateCallback.onUnlockedChanged();
+
+        // THEN the unlock should show
+        verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
+    }
+
+    @Test
+    public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
+        // GIVEN udfps not enrolled
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
+
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureStatusBarStateListener();
+        reset(mLockIconView);
+
+        // WHEN the dozing state changes
+        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+        // THEN the icon is cleared
+        verify(mLockIconView).clearIcon();
+    }
+
+    @Test
+    public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
+        // GIVEN udfps enrolled
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+        // GIVEN starting state for the lock icon
+        setupShowLockIcon();
+
+        // GIVEN lock icon controller is initialized and view is attached
+        init(/* useMigrationFlag= */false);
+        captureStatusBarStateListener();
+        reset(mLockIconView);
+
+        // WHEN the dozing state changes
+        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+
+        // THEN the AOD lock icon should show
+        verify(mLockIconView).updateIcon(ICON_LOCK, true);
+    }
+
+    @Test
+    public void testBurnInOffsetsUpdated_onDozeAmountChanged() {
+        // GIVEN udfps enrolled
+        setupUdfps();
+        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
+
+        // GIVEN burn-in offset = 5
+        int burnInOffset = 5;
+        when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset);
+
+        // GIVEN starting state for the lock icon (keyguard)
+        setupShowLockIcon();
+        init(/* useMigrationFlag= */false);
+        captureStatusBarStateListener();
+        reset(mLockIconView);
+
+        // WHEN dozing updates
+        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
+        mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
+
+        // THEN the view's translation is updated to use the AoD burn-in offsets
+        verify(mLockIconView).setTranslationY(burnInOffset);
+        verify(mLockIconView).setTranslationX(burnInOffset);
+        reset(mLockIconView);
+
+        // WHEN the device is no longer dozing
+        mStatusBarStateListener.onDozingChanged(false /* isDozing */);
+        mStatusBarStateListener.onDozeAmountChanged(0f, 0f);
+
+        // THEN the view is updated to NO translation (no burn-in offsets anymore)
+        verify(mLockIconView).setTranslationY(0);
+        verify(mLockIconView).setTranslationX(0);
+
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt
new file mode 100644
index 0000000..d2c54b4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerWithCoroutinesTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.keyguard
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.keyguard.LockIconView.ICON_LOCK
+import com.android.systemui.doze.util.getBurnInOffset
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LockIconViewControllerWithCoroutinesTest : LockIconViewControllerBaseTest() {
+
+    /** After migration, replaces LockIconViewControllerTest version */
+    @Test
+    fun testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN udfps not enrolled
+            setupUdfps()
+            whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false)
+
+            // GIVEN starting state for the lock icon
+            setupShowLockIcon()
+
+            // GIVEN lock icon controller is initialized and view is attached
+            init(/* useMigrationFlag= */ true)
+            reset(mLockIconView)
+
+            // WHEN the dozing state changes
+            mUnderTest.mIsDozingCallback.accept(true)
+
+            // THEN the icon is cleared
+            verify(mLockIconView).clearIcon()
+        }
+
+    /** After migration, replaces LockIconViewControllerTest version */
+    @Test
+    fun testLockIcon_updateToAodLock_whenUdfpsEnrolled() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN udfps enrolled
+            setupUdfps()
+            whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true)
+
+            // GIVEN starting state for the lock icon
+            setupShowLockIcon()
+
+            // GIVEN lock icon controller is initialized and view is attached
+            init(/* useMigrationFlag= */ true)
+            reset(mLockIconView)
+
+            // WHEN the dozing state changes
+            mUnderTest.mIsDozingCallback.accept(true)
+
+            // THEN the AOD lock icon should show
+            verify(mLockIconView).updateIcon(ICON_LOCK, true)
+        }
+
+    /** After migration, replaces LockIconViewControllerTest version */
+    @Test
+    fun testBurnInOffsetsUpdated_onDozeAmountChanged() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN udfps enrolled
+            setupUdfps()
+            whenever(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true)
+
+            // GIVEN burn-in offset = 5
+            val burnInOffset = 5
+            whenever(getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset)
+
+            // GIVEN starting state for the lock icon (keyguard)
+            setupShowLockIcon()
+            init(/* useMigrationFlag= */ true)
+            reset(mLockIconView)
+
+            // WHEN dozing updates
+            mUnderTest.mIsDozingCallback.accept(true)
+            mUnderTest.mDozeTransitionCallback.accept(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+
+            // THEN the view's translation is updated to use the AoD burn-in offsets
+            verify(mLockIconView).setTranslationY(burnInOffset.toFloat())
+            verify(mLockIconView).setTranslationX(burnInOffset.toFloat())
+            reset(mLockIconView)
+
+            // WHEN the device is no longer dozing
+            mUnderTest.mIsDozingCallback.accept(false)
+            mUnderTest.mDozeTransitionCallback.accept(TransitionStep(AOD, LOCKSCREEN, 0f, FINISHED))
+
+            // THEN the view is updated to NO translation (no burn-in offsets anymore)
+            verify(mLockIconView).setTranslationY(0f)
+            verify(mLockIconView).setTranslationX(0f)
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
index e099c92..ea16cb5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
@@ -20,6 +20,7 @@
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_SMARTSPACE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_WEATHER;
@@ -63,6 +64,8 @@
                 .isEqualTo(COMPLICATION_TYPE_HOME_CONTROLS);
         assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_SMARTSPACE))
                 .isEqualTo(COMPLICATION_TYPE_SMARTSPACE);
+        assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_MEDIA_ENTRY))
+                .isEqualTo(COMPLICATION_TYPE_MEDIA_ENTRY);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
index 50f27ea..0295030 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
@@ -16,8 +16,11 @@
 
 package com.android.systemui.dreams.complication;
 
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_MEDIA_ENTRY;
 import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -32,6 +35,7 @@
 import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.controls.ui.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
@@ -51,6 +55,9 @@
 @TestableLooper.RunWithLooper
 public class DreamMediaEntryComplicationTest extends SysuiTestCase {
     @Mock
+    private DreamMediaEntryComplicationComponent.Factory mComponentFactory;
+
+    @Mock
     private View mView;
 
     @Mock
@@ -89,6 +96,14 @@
         when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(false);
     }
 
+    @Test
+    public void testGetRequiredTypeAvailability() {
+        final DreamMediaEntryComplication complication =
+                new DreamMediaEntryComplication(mComponentFactory);
+        assertThat(complication.getRequiredTypeAvailability()).isEqualTo(
+                COMPLICATION_TYPE_MEDIA_ENTRY);
+    }
+
     /**
      * Ensures clicking media entry chip adds/removes media complication.
      */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 4c986bf..2c3ddd5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -60,6 +60,7 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.DozeParameters;
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -112,6 +113,8 @@
 
     private FalsingCollectorFake mFalsingCollector;
 
+    private @Mock CentralSurfaces mCentralSurfaces;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -258,6 +261,26 @@
         verify(mKeyguardStateController).notifyKeyguardGoingAway(false);
     }
 
+    @Test
+    public void testUpdateIsKeyguardAfterOccludeAnimationEnds() {
+        mViewMediator.mOccludeAnimationController.onLaunchAnimationEnd(
+                false /* isExpandingFullyAbove */);
+
+        // Since the updateIsKeyguard call is delayed during the animation, ensure it's called once
+        // it ends.
+        verify(mCentralSurfaces).updateIsKeyguard();
+    }
+
+    @Test
+    public void testUpdateIsKeyguardAfterOccludeAnimationIsCancelled() {
+        mViewMediator.mOccludeAnimationController.onLaunchAnimationCancelled(
+                null /* newKeyguardOccludedState */);
+
+        // Since the updateIsKeyguard call is delayed during the animation, ensure it's called if
+        // it's cancelled.
+        verify(mCentralSurfaces).updateIsKeyguard();
+    }
+
     private void createAndStartViewMediator() {
         mViewMediator = new KeyguardViewMediator(
                 mContext,
@@ -287,5 +310,7 @@
                 mNotificationShadeWindowControllerLazy,
                 () -> mActivityLaunchAnimator);
         mViewMediator.start();
+
+        mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
deleted file mode 100644
index cefd68d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/LockIconViewControllerTest.java
+++ /dev/null
@@ -1,476 +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.keyguard;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-import static com.android.keyguard.LockIconView.ICON_LOCK;
-import static com.android.keyguard.LockIconView.ICON_UNLOCK;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.AnimatedStateListDrawable;
-import android.hardware.biometrics.BiometricSourceType;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.util.Pair;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.accessibility.AccessibilityManager;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.keyguard.KeyguardUpdateMonitorCallback;
-import com.android.keyguard.KeyguardViewController;
-import com.android.keyguard.LockIconView;
-import com.android.keyguard.LockIconViewController;
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.biometrics.AuthRippleController;
-import com.android.systemui.doze.util.BurnInHelperKt;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.VibratorHelper;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class LockIconViewControllerTest extends SysuiTestCase {
-    private static final String UNLOCKED_LABEL = "unlocked";
-    private static final int PADDING = 10;
-
-    private MockitoSession mStaticMockSession;
-
-    private @Mock LockIconView mLockIconView;
-    private @Mock AnimatedStateListDrawable mIconDrawable;
-    private @Mock Context mContext;
-    private @Mock Resources mResources;
-    private @Mock(answer = Answers.RETURNS_DEEP_STUBS) WindowManager mWindowManager;
-    private @Mock StatusBarStateController mStatusBarStateController;
-    private @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private @Mock KeyguardViewController mKeyguardViewController;
-    private @Mock KeyguardStateController mKeyguardStateController;
-    private @Mock FalsingManager mFalsingManager;
-    private @Mock AuthController mAuthController;
-    private @Mock DumpManager mDumpManager;
-    private @Mock AccessibilityManager mAccessibilityManager;
-    private @Mock ConfigurationController mConfigurationController;
-    private @Mock VibratorHelper mVibrator;
-    private @Mock AuthRippleController mAuthRippleController;
-    private FakeExecutor mDelayableExecutor = new FakeExecutor(new FakeSystemClock());
-
-    private LockIconViewController mLockIconViewController;
-
-    // Capture listeners so that they can be used to send events
-    @Captor private ArgumentCaptor<View.OnAttachStateChangeListener> mAttachCaptor =
-            ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
-    private View.OnAttachStateChangeListener mAttachListener;
-
-    @Captor private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateCaptor =
-            ArgumentCaptor.forClass(KeyguardStateController.Callback.class);
-    private KeyguardStateController.Callback mKeyguardStateCallback;
-
-    @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateCaptor =
-            ArgumentCaptor.forClass(StatusBarStateController.StateListener.class);
-    private StatusBarStateController.StateListener mStatusBarStateListener;
-
-    @Captor private ArgumentCaptor<AuthController.Callback> mAuthControllerCallbackCaptor;
-    private AuthController.Callback mAuthControllerCallback;
-
-    @Captor private ArgumentCaptor<KeyguardUpdateMonitorCallback>
-            mKeyguardUpdateMonitorCallbackCaptor =
-            ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);
-    private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
-
-    @Captor private ArgumentCaptor<Point> mPointCaptor;
-
-    @Before
-    public void setUp() throws Exception {
-        mStaticMockSession = mockitoSession()
-                .mockStatic(BurnInHelperKt.class)
-                .strictness(Strictness.LENIENT)
-                .startMocking();
-        MockitoAnnotations.initMocks(this);
-
-        setupLockIconViewMocks();
-        when(mContext.getResources()).thenReturn(mResources);
-        when(mContext.getSystemService(WindowManager.class)).thenReturn(mWindowManager);
-        Rect windowBounds = new Rect(0, 0, 800, 1200);
-        when(mWindowManager.getCurrentWindowMetrics().getBounds()).thenReturn(windowBounds);
-        when(mResources.getString(R.string.accessibility_unlock_button)).thenReturn(UNLOCKED_LABEL);
-        when(mResources.getDrawable(anyInt(), any())).thenReturn(mIconDrawable);
-        when(mResources.getDimensionPixelSize(R.dimen.lock_icon_padding)).thenReturn(PADDING);
-        when(mAuthController.getScaleFactor()).thenReturn(1f);
-
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
-        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
-        when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
-
-        mLockIconViewController = new LockIconViewController(
-                mLockIconView,
-                mStatusBarStateController,
-                mKeyguardUpdateMonitor,
-                mKeyguardViewController,
-                mKeyguardStateController,
-                mFalsingManager,
-                mAuthController,
-                mDumpManager,
-                mAccessibilityManager,
-                mConfigurationController,
-                mDelayableExecutor,
-                mVibrator,
-                mAuthRippleController,
-                mResources
-        );
-    }
-
-    @After
-    public void tearDown() {
-        mStaticMockSession.finishMocking();
-    }
-
-    @Test
-    public void testUpdateFingerprintLocationOnInit() {
-        // GIVEN fp sensor location is available pre-attached
-        Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
-
-        // WHEN lock icon view controller is initialized and attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN lock icon view location is updated to the udfps location with UDFPS radius
-        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
-                eq(PADDING));
-    }
-
-    @Test
-    public void testUpdatePaddingBasedOnResolutionScale() {
-        // GIVEN fp sensor location is available pre-attached & scaled resolution factor is 5
-        Pair<Float, Point> udfps = setupUdfps(); // first = radius, second = udfps location
-        when(mAuthController.getScaleFactor()).thenReturn(5f);
-
-        // WHEN lock icon view controller is initialized and attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN lock icon view location is updated with the scaled radius
-        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
-                eq(PADDING * 5));
-    }
-
-    @Test
-    public void testUpdateLockIconLocationOnAuthenticatorsRegistered() {
-        // GIVEN fp sensor location is not available pre-init
-        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
-        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        resetLockIconView(); // reset any method call counts for when we verify method calls later
-
-        // GIVEN fp sensor location is available post-attached
-        captureAuthControllerCallback();
-        Pair<Float, Point> udfps = setupUdfps();
-
-        // WHEN all authenticators are registered
-        mAuthControllerCallback.onAllAuthenticatorsRegistered();
-        mDelayableExecutor.runAllReady();
-
-        // THEN lock icon view location is updated with the same coordinates as auth controller vals
-        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
-                eq(PADDING));
-    }
-
-    @Test
-    public void testUpdateLockIconLocationOnUdfpsLocationChanged() {
-        // GIVEN fp sensor location is not available pre-init
-        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
-        when(mAuthController.getFingerprintSensorLocation()).thenReturn(null);
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        resetLockIconView(); // reset any method call counts for when we verify method calls later
-
-        // GIVEN fp sensor location is available post-attached
-        captureAuthControllerCallback();
-        Pair<Float, Point> udfps = setupUdfps();
-
-        // WHEN udfps location changes
-        mAuthControllerCallback.onUdfpsLocationChanged();
-        mDelayableExecutor.runAllReady();
-
-        // THEN lock icon view location is updated with the same coordinates as auth controller vals
-        verify(mLockIconView).setCenterLocation(eq(udfps.second), eq(udfps.first),
-                eq(PADDING));
-    }
-
-    @Test
-    public void testLockIconViewBackgroundEnabledWhenUdfpsIsSupported() {
-        // GIVEN Udpfs sensor location is available
-        setupUdfps();
-
-        mLockIconViewController.init();
-        captureAttachListener();
-
-        // WHEN the view is attached
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN the lock icon view background should be enabled
-        verify(mLockIconView).setUseBackground(true);
-    }
-
-    @Test
-    public void testLockIconViewBackgroundDisabledWhenUdfpsIsNotSupported() {
-        // GIVEN Udfps sensor location is not supported
-        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(false);
-
-        mLockIconViewController.init();
-        captureAttachListener();
-
-        // WHEN the view is attached
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN the lock icon view background should be disabled
-        verify(mLockIconView).setUseBackground(false);
-    }
-
-    @Test
-    public void testUnlockIconShows_biometricUnlockedTrue() {
-        // GIVEN UDFPS sensor location is available
-        setupUdfps();
-
-        // GIVEN lock icon controller is initialized and view is attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureKeyguardUpdateMonitorCallback();
-
-        // GIVEN user has unlocked with a biometric auth (ie: face auth)
-        when(mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(anyInt())).thenReturn(true);
-        reset(mLockIconView);
-
-        // WHEN face auth's biometric running state changes
-        mKeyguardUpdateMonitorCallback.onBiometricRunningStateChanged(false,
-                BiometricSourceType.FACE);
-
-        // THEN the unlock icon is shown
-        verify(mLockIconView).setContentDescription(UNLOCKED_LABEL);
-    }
-
-    @Test
-    public void testLockIconStartState() {
-        // GIVEN lock icon state
-        setupShowLockIcon();
-
-        // WHEN lock icon controller is initialized
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-
-        // THEN the lock icon should show
-        verify(mLockIconView).updateIcon(ICON_LOCK, false);
-    }
-
-    @Test
-    public void testLockIcon_updateToUnlock() {
-        // GIVEN starting state for the lock icon
-        setupShowLockIcon();
-
-        // GIVEN lock icon controller is initialized and view is attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureKeyguardStateCallback();
-        reset(mLockIconView);
-
-        // WHEN the unlocked state changes to canDismissLockScreen=true
-        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
-        mKeyguardStateCallback.onUnlockedChanged();
-
-        // THEN the unlock should show
-        verify(mLockIconView).updateIcon(ICON_UNLOCK, false);
-    }
-
-    @Test
-    public void testLockIcon_clearsIconOnAod_whenUdfpsNotEnrolled() {
-        // GIVEN udfps not enrolled
-        setupUdfps();
-        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(false);
-
-        // GIVEN starting state for the lock icon
-        setupShowLockIcon();
-
-        // GIVEN lock icon controller is initialized and view is attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureStatusBarStateListener();
-        reset(mLockIconView);
-
-        // WHEN the dozing state changes
-        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-
-        // THEN the icon is cleared
-        verify(mLockIconView).clearIcon();
-    }
-
-    @Test
-    public void testLockIcon_updateToAodLock_whenUdfpsEnrolled() {
-        // GIVEN udfps enrolled
-        setupUdfps();
-        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
-
-        // GIVEN starting state for the lock icon
-        setupShowLockIcon();
-
-        // GIVEN lock icon controller is initialized and view is attached
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureStatusBarStateListener();
-        reset(mLockIconView);
-
-        // WHEN the dozing state changes
-        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-
-        // THEN the AOD lock icon should show
-        verify(mLockIconView).updateIcon(ICON_LOCK, true);
-    }
-
-    @Test
-    public void testBurnInOffsetsUpdated_onDozeAmountChanged() {
-        // GIVEN udfps enrolled
-        setupUdfps();
-        when(mKeyguardUpdateMonitor.isUdfpsEnrolled()).thenReturn(true);
-
-        // GIVEN burn-in offset = 5
-        int burnInOffset = 5;
-        when(BurnInHelperKt.getBurnInOffset(anyInt(), anyBoolean())).thenReturn(burnInOffset);
-
-        // GIVEN starting state for the lock icon (keyguard)
-        setupShowLockIcon();
-        mLockIconViewController.init();
-        captureAttachListener();
-        mAttachListener.onViewAttachedToWindow(mLockIconView);
-        captureStatusBarStateListener();
-        reset(mLockIconView);
-
-        // WHEN dozing updates
-        mStatusBarStateListener.onDozingChanged(true /* isDozing */);
-        mStatusBarStateListener.onDozeAmountChanged(1f, 1f);
-
-        // THEN the view's translation is updated to use the AoD burn-in offsets
-        verify(mLockIconView).setTranslationY(burnInOffset);
-        verify(mLockIconView).setTranslationX(burnInOffset);
-        reset(mLockIconView);
-
-        // WHEN the device is no longer dozing
-        mStatusBarStateListener.onDozingChanged(false /* isDozing */);
-        mStatusBarStateListener.onDozeAmountChanged(0f, 0f);
-
-        // THEN the view is updated to NO translation (no burn-in offsets anymore)
-        verify(mLockIconView).setTranslationY(0);
-        verify(mLockIconView).setTranslationX(0);
-
-    }
-    private Pair<Float, Point> setupUdfps() {
-        when(mKeyguardUpdateMonitor.isUdfpsSupported()).thenReturn(true);
-        final Point udfpsLocation = new Point(50, 75);
-        final float radius = 33f;
-        when(mAuthController.getUdfpsLocation()).thenReturn(udfpsLocation);
-        when(mAuthController.getUdfpsRadius()).thenReturn(radius);
-
-        return new Pair(radius, udfpsLocation);
-    }
-
-    private void setupShowLockIcon() {
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
-        when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(false);
-        when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarStateController.getDozeAmount()).thenReturn(0f);
-        when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
-        when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false);
-    }
-
-    private void captureAuthControllerCallback() {
-        verify(mAuthController).addCallback(mAuthControllerCallbackCaptor.capture());
-        mAuthControllerCallback = mAuthControllerCallbackCaptor.getValue();
-    }
-
-    private void captureAttachListener() {
-        verify(mLockIconView).addOnAttachStateChangeListener(mAttachCaptor.capture());
-        mAttachListener = mAttachCaptor.getValue();
-    }
-
-    private void captureKeyguardStateCallback() {
-        verify(mKeyguardStateController).addCallback(mKeyguardStateCaptor.capture());
-        mKeyguardStateCallback = mKeyguardStateCaptor.getValue();
-    }
-
-    private void captureStatusBarStateListener() {
-        verify(mStatusBarStateController).addCallback(mStatusBarStateCaptor.capture());
-        mStatusBarStateListener = mStatusBarStateCaptor.getValue();
-    }
-
-    private void captureKeyguardUpdateMonitorCallback() {
-        verify(mKeyguardUpdateMonitor).registerCallback(
-                mKeyguardUpdateMonitorCallbackCaptor.capture());
-        mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
-    }
-
-    private void setupLockIconViewMocks() {
-        when(mLockIconView.getResources()).thenReturn(mResources);
-        when(mLockIconView.getContext()).thenReturn(mContext);
-    }
-
-    private void resetLockIconView() {
-        reset(mLockIconView);
-        setupLockIconViewMocks();
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
new file mode 100644
index 0000000..f20c6a2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.notetask
+
+import android.app.KeyguardManager
+import android.content.Context
+import android.content.Intent
+import android.os.UserManager
+import android.test.suitebuilder.annotation.SmallTest
+import android.view.KeyEvent
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.floating.FloatingTasks
+import java.util.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskController].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskControllerTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskControllerTest : SysuiTestCase() {
+
+    private val notesIntent = Intent(NOTES_ACTION)
+
+    @Mock lateinit var context: Context
+    @Mock lateinit var noteTaskIntentResolver: NoteTaskIntentResolver
+    @Mock lateinit var floatingTasks: FloatingTasks
+    @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks>
+    @Mock lateinit var keyguardManager: KeyguardManager
+    @Mock lateinit var optionalKeyguardManager: Optional<KeyguardManager>
+    @Mock lateinit var optionalUserManager: Optional<UserManager>
+    @Mock lateinit var userManager: UserManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(notesIntent)
+        whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks)
+        whenever(optionalKeyguardManager.orElse(null)).thenReturn(keyguardManager)
+        whenever(optionalUserManager.orElse(null)).thenReturn(userManager)
+        whenever(userManager.isUserUnlocked).thenReturn(true)
+    }
+
+    private fun createNoteTaskController(isEnabled: Boolean = true): NoteTaskController {
+        return NoteTaskController(
+            context = context,
+            intentResolver = noteTaskIntentResolver,
+            optionalFloatingTasks = optionalFloatingTasks,
+            optionalKeyguardManager = optionalKeyguardManager,
+            optionalUserManager = optionalUserManager,
+            isEnabled = isEnabled,
+        )
+    }
+
+    @Test
+    fun handleSystemKey_keyguardIsLocked_shouldStartActivity() {
+        whenever(keyguardManager.isKeyguardLocked).thenReturn(true)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context).startActivity(notesIntent)
+        verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_keyguardIsUnlocked_shouldStartFloatingTask() {
+        whenever(keyguardManager.isKeyguardLocked).thenReturn(false)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(floatingTasks).showOrSetStashed(notesIntent)
+        verify(context, never()).startActivity(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_receiveInvalidSystemKey_shouldDoNothing() {
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_UNKNOWN)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_floatingTasksIsNull_shouldDoNothing() {
+        whenever(optionalFloatingTasks.orElse(null)).thenReturn(null)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_keyguardManagerIsNull_shouldDoNothing() {
+        whenever(optionalKeyguardManager.orElse(null)).thenReturn(null)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_userManagerIsNull_shouldDoNothing() {
+        whenever(optionalUserManager.orElse(null)).thenReturn(null)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_intentResolverReturnsNull_shouldDoNothing() {
+        whenever(noteTaskIntentResolver.resolveIntent()).thenReturn(null)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_flagDisabled_shouldDoNothing() {
+        createNoteTaskController(isEnabled = false).handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+    }
+
+    @Test
+    fun handleSystemKey_userIsLocked_shouldDoNothing() {
+        whenever(userManager.isUserUnlocked).thenReturn(false)
+
+        createNoteTaskController().handleSystemKey(KeyEvent.KEYCODE_VIDEO_APP_1)
+
+        verify(context, never()).startActivity(notesIntent)
+        verify(floatingTasks, never()).showOrSetStashed(notesIntent)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
new file mode 100644
index 0000000..f344c8d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.notetask
+
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.wm.shell.floating.FloatingTasks
+import java.util.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskController].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskInitializerTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskInitializerTest : SysuiTestCase() {
+
+    @Mock lateinit var commandQueue: CommandQueue
+    @Mock lateinit var floatingTasks: FloatingTasks
+    @Mock lateinit var optionalFloatingTasks: Optional<FloatingTasks>
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        whenever(optionalFloatingTasks.isPresent).thenReturn(true)
+        whenever(optionalFloatingTasks.orElse(null)).thenReturn(floatingTasks)
+    }
+
+    private fun createNoteTaskInitializer(isEnabled: Boolean = true): NoteTaskInitializer {
+        return NoteTaskInitializer(
+            optionalFloatingTasks = optionalFloatingTasks,
+            lazyNoteTaskController = mock(),
+            commandQueue = commandQueue,
+            isEnabled = isEnabled,
+        )
+    }
+
+    @Test
+    fun initialize_shouldAddCallbacks() {
+        createNoteTaskInitializer().initialize()
+
+        verify(commandQueue).addCallback(any())
+    }
+
+    @Test
+    fun initialize_flagDisabled_shouldDoNothing() {
+        createNoteTaskInitializer(isEnabled = false).initialize()
+
+        verify(commandQueue, never()).addCallback(any())
+    }
+
+    @Test
+    fun initialize_floatingTasksNotPresent_shouldDoNothing() {
+        whenever(optionalFloatingTasks.isPresent).thenReturn(false)
+
+        createNoteTaskInitializer().initialize()
+
+        verify(commandQueue, never()).addCallback(any())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
new file mode 100644
index 0000000..dd2cc2f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskIntentResolverTest.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notetask
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ResolveInfo
+import android.content.pm.ServiceInfo
+import android.test.suitebuilder.annotation.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.notetask.NoteTaskIntentResolver.Companion.NOTES_ACTION
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for [NoteTaskIntentResolver].
+ *
+ * Build/Install/Run:
+ * - atest SystemUITests:NoteTaskIntentResolverTest
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+internal class NoteTaskIntentResolverTest : SysuiTestCase() {
+
+    @Mock lateinit var packageManager: PackageManager
+
+    private lateinit var resolver: NoteTaskIntentResolver
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        resolver = NoteTaskIntentResolver(packageManager)
+    }
+
+    private fun createResolveInfo(
+        packageName: String = "PackageName",
+        activityInfo: ActivityInfo? = null,
+    ): ResolveInfo {
+        return ResolveInfo().apply {
+            serviceInfo =
+                ServiceInfo().apply {
+                    applicationInfo = ApplicationInfo().apply { this.packageName = packageName }
+                }
+            this.activityInfo = activityInfo
+        }
+    }
+
+    private fun createActivityInfo(
+        name: String? = "ActivityName",
+        exported: Boolean = true,
+        enabled: Boolean = true,
+        showWhenLocked: Boolean = true,
+        turnScreenOn: Boolean = true,
+    ): ActivityInfo {
+        return ActivityInfo().apply {
+            this.name = name
+            this.exported = exported
+            this.enabled = enabled
+            if (showWhenLocked) {
+                flags = flags or ActivityInfo.FLAG_SHOW_WHEN_LOCKED
+            }
+            if (turnScreenOn) {
+                flags = flags or ActivityInfo.FLAG_TURN_SCREEN_ON
+            }
+        }
+    }
+
+    private fun givenQueryIntentActivities(block: () -> List<ResolveInfo>) {
+        whenever(packageManager.queryIntentActivities(any(), any<ResolveInfoFlags>()))
+            .thenReturn(block())
+    }
+
+    private fun givenResolveActivity(block: () -> ResolveInfo?) {
+        whenever(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(block())
+    }
+
+    @Test
+    fun resolveIntent_shouldReturnNotesIntent() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo()) }
+
+        val actual = resolver.resolveIntent()
+
+        val expected =
+            Intent(NOTES_ACTION)
+                .setComponent(ComponentName("PackageName", "ActivityName"))
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        // Compares the string representation of both intents, as they are different instances.
+        assertThat(actual.toString()).isEqualTo(expected.toString())
+    }
+
+    @Test
+    fun resolveIntent_activityInfoEnabledIsFalse_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity {
+            createResolveInfo(activityInfo = createActivityInfo(enabled = false))
+        }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoExportedIsFalse_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity {
+            createResolveInfo(activityInfo = createActivityInfo(exported = false))
+        }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoShowWhenLockedIsFalse_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity {
+            createResolveInfo(activityInfo = createActivityInfo(showWhenLocked = false))
+        }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoTurnScreenOnIsFalse_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity {
+            createResolveInfo(activityInfo = createActivityInfo(turnScreenOn = false))
+        }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoNameIsBlank_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = "")) }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoNameIsNull_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { createResolveInfo(activityInfo = createActivityInfo(name = null)) }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityInfoIsNull_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { createResolveInfo(activityInfo = null) }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_resolveActivityIsNull_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo()) }
+        givenResolveActivity { null }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_packageNameIsBlank_shouldReturnNull() {
+        givenQueryIntentActivities { listOf(createResolveInfo(packageName = "")) }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+
+    @Test
+    fun resolveIntent_activityNotFoundForAction_shouldReturnNull() {
+        givenQueryIntentActivities { emptyList() }
+
+        val actual = resolver.resolveIntent()
+
+        assertThat(actual).isNull()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
index bc27bbc..3131f60 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/UserDetailViewAdapterTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingManagerFake
 import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.qs.user.UserSwitchDialogController
 import com.android.systemui.statusbar.policy.UserSwitcherController
 import com.android.systemui.user.data.source.UserRecord
 import org.junit.Assert.assertEquals
@@ -41,20 +42,27 @@
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
-import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class UserDetailViewAdapterTest : SysuiTestCase() {
 
-    @Mock private lateinit var mUserSwitcherController: UserSwitcherController
-    @Mock private lateinit var mParent: ViewGroup
-    @Mock private lateinit var mUserDetailItemView: UserDetailItemView
-    @Mock private lateinit var mOtherView: View
-    @Mock private lateinit var mInflatedUserDetailItemView: UserDetailItemView
-    @Mock private lateinit var mLayoutInflater: LayoutInflater
+    @Mock
+    private lateinit var mUserSwitcherController: UserSwitcherController
+    @Mock
+    private lateinit var mParent: ViewGroup
+    @Mock
+    private lateinit var mUserDetailItemView: UserDetailItemView
+    @Mock
+    private lateinit var mOtherView: View
+    @Mock
+    private lateinit var mInflatedUserDetailItemView: UserDetailItemView
+    @Mock
+    private lateinit var mLayoutInflater: LayoutInflater
     private var falsingManagerFake: FalsingManagerFake = FalsingManagerFake()
     private lateinit var adapter: UserDetailView.Adapter
     private lateinit var uiEventLogger: UiEventLoggerFake
@@ -67,10 +75,12 @@
 
         mContext.addMockSystemService(Context.LAYOUT_INFLATER_SERVICE, mLayoutInflater)
         `when`(mLayoutInflater.inflate(anyInt(), any(ViewGroup::class.java), anyBoolean()))
-                .thenReturn(mInflatedUserDetailItemView)
+            .thenReturn(mInflatedUserDetailItemView)
         `when`(mParent.context).thenReturn(mContext)
-        adapter = UserDetailView.Adapter(mContext, mUserSwitcherController, uiEventLogger,
-                falsingManagerFake)
+        adapter = UserDetailView.Adapter(
+            mContext, mUserSwitcherController, uiEventLogger,
+            falsingManagerFake
+        )
         mPicture = UserIcons.convertToBitmap(mContext.getDrawable(R.drawable.ic_avatar_user))
     }
 
@@ -145,6 +155,15 @@
         assertNull(adapter.users.find { it.isManageUsers })
     }
 
+    @Test
+    fun clickDismissDialog() {
+        val shower: UserSwitchDialogController.DialogShower =
+            mock(UserSwitchDialogController.DialogShower::class.java)
+        adapter.injectDialogShower(shower)
+        adapter.onUserListItemClicked(createUserRecord(current = true, guest = false), shower)
+        verify(shower).dismiss()
+    }
+
     private fun createUserRecord(current: Boolean, guest: Boolean) =
         UserRecord(
             UserInfo(0 /* id */, "name", 0 /* flags */),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index ffb41e5..70cbc64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.graphics.drawable.Drawable
 import android.os.Handler
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -104,13 +105,14 @@
             mockContext,
             mockPluginManager,
             mockHandler,
-            fakeDefaultProvider
+            isEnabled = true,
+            userHandle = UserHandle.USER_ALL,
+            defaultClockProvider = fakeDefaultProvider
         ) {
             override var currentClockId: ClockId
                 get() = settingValue
                 set(value) { settingValue = value }
         }
-        registry.isEnabled = true
 
         verify(mockPluginManager)
             .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java), eq(true))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index 64dc956..4478039 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -25,6 +25,7 @@
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
+import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
@@ -69,10 +70,13 @@
         TransitionInfo combined = new TransitionInfoBuilder(TRANSIT_CLOSE)
                 .addChange(TRANSIT_CHANGE, FLAG_SHOW_WALLPAPER,
                         createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_STANDARD))
+                // Embedded TaskFragment should be excluded when animated with Task.
+                .addChange(TRANSIT_CLOSE, FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, null /* taskInfo */)
                 .addChange(TRANSIT_CLOSE, 0 /* flags */,
                         createTaskInfo(2 /* taskId */, ACTIVITY_TYPE_STANDARD))
                 .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */)
-                .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build();
+                .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */)
+                .build();
         // Check apps extraction
         RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
                 mock(SurfaceControl.Transaction.class), null /* leashes */);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
deleted file mode 100644
index 81b8e98..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.notification.row;
-
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.view.NotificationHeaderView;
-import android.view.View;
-import android.view.ViewPropertyAnimator;
-
-import androidx.test.annotation.UiThreadTest;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.R;
-import com.android.internal.widget.NotificationActionListLayout;
-import com.android.internal.widget.NotificationExpandButton;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.statusbar.notification.FeedbackIcon;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class NotificationContentViewTest extends SysuiTestCase {
-
-    NotificationContentView mView;
-
-    @Before
-    @UiThreadTest
-    public void setup() {
-        mDependency.injectMockDependency(MediaOutputDialogFactory.class);
-
-        mView = new NotificationContentView(mContext, null);
-        ExpandableNotificationRow row = new ExpandableNotificationRow(mContext, null);
-        ExpandableNotificationRow mockRow = spy(row);
-        doReturn(10).when(mockRow).getIntrinsicHeight();
-
-        mView.setContainingNotification(mockRow);
-        mView.setHeights(10, 20, 30);
-
-        mView.setContractedChild(createViewWithHeight(10));
-        mView.setExpandedChild(createViewWithHeight(20));
-        mView.setHeadsUpChild(createViewWithHeight(30));
-
-        mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
-        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
-    }
-
-    private View createViewWithHeight(int height) {
-        View view = new View(mContext, null);
-        view.setMinimumHeight(height);
-        return view;
-    }
-
-    @Test
-    @UiThreadTest
-    public void testSetFeedbackIcon() {
-        View mockContracted = mock(NotificationHeaderView.class);
-        when(mockContracted.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockContracted);
-        when(mockContracted.getContext()).thenReturn(mContext);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockExpanded);
-        when(mockExpanded.getContext()).thenReturn(mContext);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.feedback))
-                .thenReturn(mockHeadsUp);
-        when(mockHeadsUp.getContext()).thenReturn(mContext);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setFeedbackIcon(new FeedbackIcon(R.drawable.ic_feedback_alerted,
-                R.string.notification_feedback_indicator_alerted));
-
-        verify(mockContracted, times(1)).setVisibility(View.VISIBLE);
-        verify(mockExpanded, times(1)).setVisibility(View.VISIBLE);
-        verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE);
-    }
-
-    @Test
-    @UiThreadTest
-    public void testExpandButtonFocusIsCalled() {
-        View mockContractedEB = mock(NotificationExpandButton.class);
-        View mockContracted = mock(NotificationHeaderView.class);
-        when(mockContracted.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockContracted.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockContractedEB);
-        when(mockContracted.getContext()).thenReturn(mContext);
-
-        View mockExpandedEB = mock(NotificationExpandButton.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockExpanded.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockExpandedEB);
-        when(mockExpanded.getContext()).thenReturn(mContext);
-
-        View mockHeadsUpEB = mock(NotificationExpandButton.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.animate()).thenReturn(mock(ViewPropertyAnimator.class));
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
-                mockHeadsUpEB);
-        when(mockHeadsUp.getContext()).thenReturn(mContext);
-
-        // Set up all 3 child forms
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        // This is required to call requestAccessibilityFocus()
-        mView.setFocusOnVisibilityChange();
-
-        // The following will initialize the view and switch from not visible to expanded.
-        // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
-        mView.setHeadsUp(true);
-
-        verify(mockContractedEB, times(0)).requestAccessibilityFocus();
-        verify(mockExpandedEB, times(1)).requestAccessibilityFocus();
-        verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus();
-    }
-
-    @Test
-    @UiThreadTest
-    public void testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
-        View mockContracted = mock(NotificationHeaderView.class);
-
-        View mockExpandedActions = mock(NotificationActionListLayout.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockExpandedActions);
-
-        View mockHeadsUpActions = mock(NotificationActionListLayout.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockHeadsUpActions);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setRemoteInputVisible(true);
-
-        verify(mockContracted, times(0)).findViewById(0);
-        verify(mockExpandedActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-        verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-    }
-
-    @Test
-    @UiThreadTest
-    public void testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
-        View mockContracted = mock(NotificationHeaderView.class);
-
-        View mockExpandedActions = mock(NotificationActionListLayout.class);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockExpandedActions);
-
-        View mockHeadsUpActions = mock(NotificationActionListLayout.class);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.actions)).thenReturn(
-                mockHeadsUpActions);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        mView.setRemoteInputVisible(false);
-
-        verify(mockContracted, times(0)).findViewById(0);
-        verify(mockExpandedActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-        verify(mockHeadsUpActions, times(1)).setImportantForAccessibility(
-                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
new file mode 100644
index 0000000..562b4df
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.content.res.Resources
+import android.os.UserHandle
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import android.view.NotificationHeaderView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.internal.widget.NotificationActionListLayout
+import com.android.internal.widget.NotificationExpandButton
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.dialog.MediaOutputDialogFactory
+import com.android.systemui.statusbar.notification.FeedbackIcon
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationContentViewTest : SysuiTestCase() {
+    private lateinit var view: NotificationContentView
+
+    @Mock private lateinit var mPeopleNotificationIdentifier: PeopleNotificationIdentifier
+
+    private val notificationContentMargin =
+        mContext.resources.getDimensionPixelSize(R.dimen.notification_content_margin)
+
+    @Before
+    fun setup() {
+        initMocks(this)
+
+        mDependency.injectMockDependency(MediaOutputDialogFactory::class.java)
+
+        view = spy(NotificationContentView(mContext, /* attrs= */ null))
+        val row = ExpandableNotificationRow(mContext, /* attrs= */ null)
+        row.entry = createMockNotificationEntry(false)
+        val spyRow = spy(row)
+        doReturn(10).whenever(spyRow).intrinsicHeight
+
+        with(view) {
+            initialize(mPeopleNotificationIdentifier, mock(), mock(), mock())
+            setContainingNotification(spyRow)
+            setHeights(/* smallHeight= */ 10, /* headsUpMaxHeight= */ 20, /* maxHeight= */ 30)
+            contractedChild = createViewWithHeight(10)
+            expandedChild = createViewWithHeight(20)
+            headsUpChild = createViewWithHeight(30)
+            measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
+            layout(0, 0, view.measuredWidth, view.measuredHeight)
+        }
+    }
+
+    private fun createViewWithHeight(height: Int) =
+        View(mContext, /* attrs= */ null).apply { minimumHeight = height }
+
+    @Test
+    fun testSetFeedbackIcon() {
+        // Given: contractedChild, enpandedChild, and headsUpChild being set
+        val mockContracted = createMockNotificationHeaderView()
+        val mockExpanded = createMockNotificationHeaderView()
+        val mockHeadsUp = createMockNotificationHeaderView()
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        // When: FeedBackIcon is set
+        view.setFeedbackIcon(
+            FeedbackIcon(
+                R.drawable.ic_feedback_alerted,
+                R.string.notification_feedback_indicator_alerted
+            )
+        )
+
+        // Then: contractedChild, enpandedChild, and headsUpChild should be set to be visible
+        verify(mockContracted).visibility = View.VISIBLE
+        verify(mockExpanded).visibility = View.VISIBLE
+        verify(mockHeadsUp).visibility = View.VISIBLE
+    }
+
+    private fun createMockNotificationHeaderView() =
+        mock<NotificationHeaderView>().apply {
+            whenever(this.findViewById<View>(R.id.feedback)).thenReturn(this)
+            whenever(this.context).thenReturn(mContext)
+        }
+
+    @Test
+    fun testExpandButtonFocusIsCalled() {
+        val mockContractedEB = mock<NotificationExpandButton>()
+        val mockContracted = createMockNotificationHeaderView(mockContractedEB)
+
+        val mockExpandedEB = mock<NotificationExpandButton>()
+        val mockExpanded = createMockNotificationHeaderView(mockExpandedEB)
+
+        val mockHeadsUpEB = mock<NotificationExpandButton>()
+        val mockHeadsUp = createMockNotificationHeaderView(mockHeadsUpEB)
+
+        // Set up all 3 child forms
+        view.contractedChild = mockContracted
+        view.expandedChild = mockExpanded
+        view.headsUpChild = mockHeadsUp
+
+        // This is required to call requestAccessibilityFocus()
+        view.setFocusOnVisibilityChange()
+
+        // The following will initialize the view and switch from not visible to expanded.
+        // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
+        view.setHeadsUp(true)
+        verify(mockContractedEB, never()).requestAccessibilityFocus()
+        verify(mockExpandedEB).requestAccessibilityFocus()
+        verify(mockHeadsUpEB, never()).requestAccessibilityFocus()
+    }
+
+    private fun createMockNotificationHeaderView(mockExpandedEB: NotificationExpandButton) =
+        mock<NotificationHeaderView>().apply {
+            whenever(this.animate()).thenReturn(mock())
+            whenever(this.findViewById<View>(R.id.expand_button)).thenReturn(mockExpandedEB)
+            whenever(this.context).thenReturn(mContext)
+        }
+
+    @Test
+    fun testRemoteInputVisibleSetsActionsUnimportantHideDescendantsForAccessibility() {
+        val mockContracted = mock<NotificationHeaderView>()
+
+        val mockExpandedActions = mock<NotificationActionListLayout>()
+        val mockExpanded = mock<NotificationHeaderView>()
+        whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
+
+        val mockHeadsUpActions = mock<NotificationActionListLayout>()
+        val mockHeadsUp = mock<NotificationHeaderView>()
+        whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        view.setRemoteInputVisible(true)
+
+        verify(mockContracted, never()).findViewById<View>(0)
+        verify(mockExpandedActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+        verify(mockHeadsUpActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+    }
+
+    @Test
+    fun testRemoteInputInvisibleSetsActionsAutoImportantForAccessibility() {
+        val mockContracted = mock<NotificationHeaderView>()
+
+        val mockExpandedActions = mock<NotificationActionListLayout>()
+        val mockExpanded = mock<NotificationHeaderView>()
+        whenever(mockExpanded.findViewById<View>(R.id.actions)).thenReturn(mockExpandedActions)
+
+        val mockHeadsUpActions = mock<NotificationActionListLayout>()
+        val mockHeadsUp = mock<NotificationHeaderView>()
+        whenever(mockHeadsUp.findViewById<View>(R.id.actions)).thenReturn(mockHeadsUpActions)
+
+        with(view) {
+            contractedChild = mockContracted
+            expandedChild = mockExpanded
+            headsUpChild = mockHeadsUp
+        }
+
+        view.setRemoteInputVisible(false)
+
+        verify(mockContracted, never()).findViewById<View>(0)
+        verify(mockExpandedActions).importantForAccessibility =
+            View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+        verify(mockHeadsUpActions).importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+    }
+
+    @Test
+    fun setExpandedChild_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        // Bubble button should not be shown for the given NotificationEntry
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+
+        // When: call NotificationContentView.setExpandedChild() to set the expandedChild
+        view.expandedChild = mockExpandedChild
+
+        // Then: bottom margin of actionListMarginTarget should not change,
+        // still be notificationContentMargin
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun setExpandedChild_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        // Bubble button should be shown for the given NotificationEntry
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ true)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+
+        // When: call NotificationContentView.setExpandedChild() to set the expandedChild
+        view.expandedChild = mockExpandedChild
+
+        // Then: bottom margin of actionListMarginTarget should be set to 0
+        assertEquals(0, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun onNotificationUpdated_notShowBubbleButton_marginTargetBottomMarginShouldNotChange() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+        view.expandedChild = mockExpandedChild
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+
+        // When: call NotificationContentView.onNotificationUpdated() to update the
+        // NotificationEntry, which should not show bubble button
+        view.onNotificationUpdated(createMockNotificationEntry(/* showButton= */ false))
+
+        // Then: bottom margin of actionListMarginTarget should not change, still be 20
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+    }
+
+    @Test
+    fun onNotificationUpdated_showBubbleButton_marginTargetBottomMarginShouldChangeToZero() {
+        // Given: bottom margin of actionListMarginTarget is notificationContentMargin
+        val mockNotificationEntry = createMockNotificationEntry(/* showButton= */ false)
+        val mockContainingNotification = createMockContainingNotification(mockNotificationEntry)
+        val actionListMarginTarget =
+            spy(createLinearLayoutWithBottomMargin(notificationContentMargin))
+        val mockExpandedChild = createMockExpandedChild(mockNotificationEntry)
+        whenever(
+                mockExpandedChild.findViewById<LinearLayout>(
+                    R.id.notification_action_list_margin_target
+                )
+            )
+            .thenReturn(actionListMarginTarget)
+        view.setContainingNotification(mockContainingNotification)
+        view.expandedChild = mockExpandedChild
+        assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget))
+
+        // When: call NotificationContentView.onNotificationUpdated() to update the
+        // NotificationEntry, which should show bubble button
+        view.onNotificationUpdated(createMockNotificationEntry(true))
+
+        // Then: bottom margin of actionListMarginTarget should not change, still be 20
+        assertEquals(0, getMarginBottom(actionListMarginTarget))
+    }
+
+    private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
+        mock<ExpandableNotificationRow>().apply {
+            whenever(this.entry).thenReturn(notificationEntry)
+            whenever(this.context).thenReturn(mContext)
+            whenever(this.bubbleClickListener).thenReturn(View.OnClickListener {})
+        }
+
+    private fun createMockNotificationEntry(showButton: Boolean) =
+        mock<NotificationEntry>().apply {
+            whenever(mPeopleNotificationIdentifier.getPeopleNotificationType(this))
+                .thenReturn(PeopleNotificationIdentifier.TYPE_FULL_PERSON)
+            whenever(this.bubbleMetadata).thenReturn(mock())
+            val sbnMock: StatusBarNotification = mock()
+            val userMock: UserHandle = mock()
+            whenever(this.sbn).thenReturn(sbnMock)
+            whenever(sbnMock.user).thenReturn(userMock)
+            doReturn(showButton).whenever(view).shouldShowBubbleButton(this)
+        }
+
+    private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout {
+        val outerLayout = LinearLayout(mContext)
+        val innerLayout = LinearLayout(mContext)
+        outerLayout.addView(innerLayout)
+        val mlp = innerLayout.layoutParams as ViewGroup.MarginLayoutParams
+        mlp.setMargins(0, 0, 0, bottomMargin)
+        return innerLayout
+    }
+
+    private fun createMockExpandedChild(notificationEntry: NotificationEntry) =
+        mock<ExpandableNotificationRow>().apply {
+            whenever(this.findViewById<ImageView>(R.id.bubble_button)).thenReturn(mock())
+            whenever(this.findViewById<View>(R.id.actions_container)).thenReturn(mock())
+            whenever(this.entry).thenReturn(notificationEntry)
+            whenever(this.context).thenReturn(mContext)
+
+            val resourcesMock: Resources = mock()
+            whenever(resourcesMock.configuration).thenReturn(mock())
+            whenever(this.resources).thenReturn(resourcesMock)
+        }
+
+    private fun getMarginBottom(layout: LinearLayout): Int =
+        (layout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 6de8bd5..5755782 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -235,6 +235,7 @@
     @Mock private NavigationBarController mNavigationBarController;
     @Mock private AccessibilityFloatingMenuController mAccessibilityFloatingMenuController;
     @Mock private SysuiColorExtractor mColorExtractor;
+    private WakefulnessLifecycle mWakefulnessLifecycle;
     @Mock private ColorExtractor.GradientColors mGradientColors;
     @Mock private PulseExpansionHandler mPulseExpansionHandler;
     @Mock private NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
@@ -366,10 +367,10 @@
             return null;
         }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any());
 
-        WakefulnessLifecycle wakefulnessLifecycle =
+        mWakefulnessLifecycle =
                 new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager);
-        wakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
-        wakefulnessLifecycle.dispatchFinishedWakingUp();
+        mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
+        mWakefulnessLifecycle.dispatchFinishedWakingUp();
 
         when(mGradientColors.supportsDarkText()).thenReturn(true);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
@@ -428,7 +429,7 @@
                 mBatteryController,
                 mColorExtractor,
                 new ScreenLifecycle(mDumpManager),
-                wakefulnessLifecycle,
+                mWakefulnessLifecycle,
                 mStatusBarStateController,
                 Optional.of(mBubbles),
                 mDeviceProvisionedController,
@@ -507,6 +508,8 @@
         mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController;
         mCentralSurfaces.mBarService = mBarService;
         mCentralSurfaces.mStackScroller = mStackScroller;
+        mCentralSurfaces.mGestureWakeLock = mPowerManager.newWakeLock(
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "sysui:GestureWakeLock");
         mCentralSurfaces.startKeyguard();
         mInitController.executePostInitTasks();
         notificationLogger.setUpWithContainer(mNotificationListContainer);
@@ -1125,6 +1128,55 @@
         assertThat(onDismissActionCaptor.getValue().onDismiss()).isFalse();
     }
 
+    @Test
+    public void testKeyguardHideDelayedIfOcclusionAnimationRunning() {
+        // Show the keyguard and verify we've done so.
+        setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */);
+        verify(mStatusBarStateController).setState(StatusBarState.KEYGUARD);
+
+        // Request to hide the keyguard, but while the occlude animation is playing. We should delay
+        // this hide call, since we're playing the occlude animation over the keyguard and thus want
+        // it to remain visible.
+        when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true);
+        setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */);
+        verify(mStatusBarStateController, never()).setState(StatusBarState.SHADE);
+
+        // Once the animation ends, verify that the keyguard is actually hidden.
+        when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(false);
+        setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */);
+        verify(mStatusBarStateController).setState(StatusBarState.SHADE);
+    }
+
+    @Test
+    public void testKeyguardHideNotDelayedIfOcclusionAnimationNotRunning() {
+        // Show the keyguard and verify we've done so.
+        setKeyguardShowingAndOccluded(true /* showing */, false /* occluded */);
+        verify(mStatusBarStateController).setState(StatusBarState.KEYGUARD);
+
+        // Hide the keyguard while the occlusion animation is not running. Verify that we
+        // immediately hide the keyguard.
+        when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(false);
+        setKeyguardShowingAndOccluded(false /* showing */, true /* occluded */);
+        verify(mStatusBarStateController).setState(StatusBarState.SHADE);
+    }
+
+    /**
+     * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard}
+     * to reconfigure the keyguard to reflect the requested showing/occluded states.
+     */
+    private void setKeyguardShowingAndOccluded(boolean showing, boolean occluded) {
+        when(mStatusBarStateController.isKeyguardRequested()).thenReturn(showing);
+        when(mKeyguardStateController.isOccluded()).thenReturn(occluded);
+
+        // If we want to show the keyguard, make sure that we think we're awake and not unlocking.
+        if (showing) {
+            when(mBiometricUnlockController.isWakeAndUnlock()).thenReturn(false);
+            mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN);
+        }
+
+        mCentralSurfaces.updateIsKeyguard(false /* forceStateChange */);
+    }
+
     private void setDeviceState(int state) {
         ArgumentCaptor<DeviceStateManager.DeviceStateCallback> callbackCaptor =
                 ArgumentCaptor.forClass(DeviceStateManager.DeviceStateCallback.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 4d1a52c..a5deaa4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.statusbar.phone.ScrimController.KEYGUARD_SCRIM_ALPHA;
 import static com.android.systemui.statusbar.phone.ScrimController.OPAQUE;
 import static com.android.systemui.statusbar.phone.ScrimController.SEMI_TRANSPARENT;
 import static com.android.systemui.statusbar.phone.ScrimController.TRANSPARENT;
@@ -58,6 +59,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.scrim.ScrimView;
 import com.android.systemui.statusbar.policy.FakeConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -117,6 +119,7 @@
     // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The
     //   event-dispatch-on-registration pattern caused some of these unit tests to fail.)
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    @Mock private KeyguardViewMediator mKeyguardViewMediator;
 
     private static class AnimatorListener implements Animator.AnimatorListener {
         private int mNumStarts;
@@ -230,7 +233,8 @@
                 mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()),
                 mScreenOffAnimationController,
                 mKeyguardUnlockAnimationController,
-                mStatusBarKeyguardViewManager);
+                mStatusBarKeyguardViewManager,
+                mKeyguardViewMediator);
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
@@ -239,6 +243,8 @@
         mScrimController.setWallpaperSupportsAmbientMode(false);
         mScrimController.transitionTo(ScrimState.KEYGUARD);
         finishAnimationsImmediately();
+
+        mScrimController.setLaunchingAffordanceWithPreview(false);
     }
 
     @After
@@ -852,7 +858,8 @@
                 mDockManager, mConfigurationController, new FakeExecutor(new FakeSystemClock()),
                 mScreenOffAnimationController,
                 mKeyguardUnlockAnimationController,
-                mStatusBarKeyguardViewManager);
+                mStatusBarKeyguardViewManager,
+                mKeyguardViewMediator);
         mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible);
         mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront);
         mScrimController.setAnimatorListener(mAnimatorListener);
@@ -1592,6 +1599,30 @@
         assertScrimAlpha(mScrimBehind, 0);
     }
 
+    @Test
+    public void keyguardAlpha_whenUnlockedForOcclusion_ifPlayingOcclusionAnimation() {
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+
+        when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true);
+
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        finishAnimationsImmediately();
+
+        assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f));
+    }
+
+    @Test
+    public void keyguardAlpha_whenUnlockedForLaunch_ifLaunchingAffordance() {
+        mScrimController.transitionTo(ScrimState.KEYGUARD);
+        when(mKeyguardViewMediator.isOccludeAnimationPlaying()).thenReturn(true);
+        mScrimController.setLaunchingAffordanceWithPreview(true);
+
+        mScrimController.transitionTo(ScrimState.UNLOCKED);
+        finishAnimationsImmediately();
+
+        assertScrimAlpha(mNotificationsScrim, (int) (KEYGUARD_SCRIM_ALPHA * 255f));
+    }
+
     private void assertAlphaAfterExpansion(ScrimView scrim, float expectedAlpha, float expansion) {
         mScrimController.setRawPanelExpansionFraction(expansion);
         finishAnimationsImmediately();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt
index fa7b259..9957c2a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemBarAttributesListenerTest.kt
@@ -14,8 +14,6 @@
 import com.android.internal.view.AppearanceRegion
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import org.junit.Before
 import org.junit.Test
@@ -40,7 +38,6 @@
     @Mock private lateinit var lightBarController: LightBarController
     @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
     @Mock private lateinit var letterboxAppearanceCalculator: LetterboxAppearanceCalculator
-    @Mock private lateinit var featureFlags: FeatureFlags
     @Mock private lateinit var centralSurfaces: CentralSurfaces
 
     private lateinit var sysBarAttrsListener: SystemBarAttributesListener
@@ -57,7 +54,6 @@
         sysBarAttrsListener =
             SystemBarAttributesListener(
                 centralSurfaces,
-                featureFlags,
                 letterboxAppearanceCalculator,
                 statusBarStateController,
                 lightBarController,
@@ -74,18 +70,14 @@
     }
 
     @Test
-    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToCentralSurfaces() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
-
+    fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToCentralSurfaces() {
         changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS)
 
         verify(centralSurfaces).setAppearance(TEST_LETTERBOX_APPEARANCE.appearance)
     }
 
     @Test
-    fun onSysBarAttrsChanged_flagTrue_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
-
+    fun onSysBarAttrsChanged_noLetterbox_forwardsOriginalAppearanceToCtrlSrfcs() {
         changeSysBarAttrs(TEST_APPEARANCE, arrayOf<LetterboxDetails>())
 
         verify(centralSurfaces).setAppearance(TEST_APPEARANCE)
@@ -100,9 +92,7 @@
     }
 
     @Test
-    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToStatusBarStateCtrl() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
-
+    fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToStatusBarStateCtrl() {
         changeSysBarAttrs(TEST_APPEARANCE, TEST_LETTERBOX_DETAILS)
 
         verify(statusBarStateController)
@@ -120,9 +110,7 @@
     }
 
     @Test
-    fun onSysBarAttrsChanged_flagTrue_forwardsLetterboxAppearanceToLightBarController() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
-
+    fun onSysBarAttrsChanged_forwardsLetterboxAppearanceToLightBarController() {
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
 
         verify(lightBarController)
@@ -135,7 +123,6 @@
 
     @Test
     fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToStatusBarStateController() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
         reset(centralSurfaces, lightBarController, statusBarStateController)
 
@@ -148,7 +135,6 @@
 
     @Test
     fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToLightBarController() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
         reset(centralSurfaces, lightBarController, statusBarStateController)
 
@@ -164,7 +150,6 @@
 
     @Test
     fun onStatusBarBoundsChanged_forwardsLetterboxAppearanceToCentralSurfaces() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
         reset(centralSurfaces, lightBarController, statusBarStateController)
 
@@ -175,7 +160,6 @@
 
     @Test
     fun onStatusBarBoundsChanged_previousCallEmptyLetterbox_doesNothing() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(true)
         changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf())
         reset(centralSurfaces, lightBarController, statusBarStateController)
 
@@ -184,17 +168,6 @@
         verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController)
     }
 
-    @Test
-    fun onStatusBarBoundsChanged_flagFalse_doesNothing() {
-        whenever(featureFlags.isEnabled(Flags.STATUS_BAR_LETTERBOX_APPEARANCE)).thenReturn(false)
-        changeSysBarAttrs(TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, TEST_LETTERBOX_DETAILS)
-        reset(centralSurfaces, lightBarController, statusBarStateController)
-
-        sysBarAttrsListener.onStatusBarBoundsChanged()
-
-        verifyZeroInteractions(centralSurfaces, lightBarController, statusBarStateController)
-    }
-
     private fun changeSysBarAttrs(@Appearance appearance: Int) {
         changeSysBarAttrs(appearance, arrayOf<LetterboxDetails>())
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
new file mode 100644
index 0000000..6ff7b7c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileConnectionRepository : MobileConnectionRepository {
+    private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel())
+    override val subscriptionModelFlow: Flow<MobileSubscriptionModel> = _subscriptionsModelFlow
+
+    fun setMobileSubscriptionModel(model: MobileSubscriptionModel) {
+        _subscriptionsModelFlow.value = model
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
similarity index 66%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 0d15268..c88d468 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -18,11 +18,11 @@
 
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.settingslib.mobile.MobileMappings.Config
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 
-class FakeMobileSubscriptionRepository : MobileSubscriptionRepository {
+class FakeMobileConnectionsRepository : MobileConnectionsRepository {
     private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf())
     override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow
 
@@ -30,22 +30,27 @@
         MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
     override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
 
-    private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>()
-    override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> {
-        return subIdFlows[subId]
-            ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it }
+    private val _defaultDataSubRatConfig = MutableStateFlow(Config())
+    override val defaultDataSubRatConfig = _defaultDataSubRatConfig
+
+    private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>()
+    override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
+        return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it }
     }
 
     fun setSubscriptions(subs: List<SubscriptionInfo>) {
         _subscriptionsFlow.value = subs
     }
 
+    fun setDefaultDataSubRatConfig(config: Config) {
+        _defaultDataSubRatConfig.value = config
+    }
+
     fun setActiveMobileDataSubscriptionId(subId: Int) {
         _activeMobileDataSubscriptionId.value = subId
     }
 
-    fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) {
-        val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet")
-        subscription.value = model
+    fun setMobileConnectionRepositoryForId(subId: Int, repo: MobileConnectionRepository) {
+        subIdRepos[subId] = repo
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
new file mode 100644
index 0000000..775e6db
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.CellSignalStrengthCdma
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ServiceStateListener
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_CA
+import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: MobileConnectionRepositoryImpl
+
+    @Mock private lateinit var subscriptionManager: SubscriptionManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+
+        underTest =
+            MobileConnectionRepositoryImpl(
+                SUB_1_ID,
+                telephonyManager,
+                IMMEDIATE,
+                logger,
+                scope,
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testFlowForSubId_default() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(MobileSubscriptionModel())
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+
+            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_emergencyOnly_toggles() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<ServiceStateListener>()
+            val serviceState = ServiceState()
+            serviceState.isEmergencyOnly = true
+            callback.onServiceStateChanged(serviceState)
+            serviceState.isEmergencyOnly = false
+            callback.onServiceStateChanged(serviceState)
+
+            assertThat(latest?.isEmergencyOnly).isEqualTo(false)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_signalStrengths_levelsUpdate() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+            val strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+            callback.onSignalStrengthsChanged(strength)
+
+            assertThat(latest?.isGsm).isEqualTo(true)
+            assertThat(latest?.primaryLevel).isEqualTo(1)
+            assertThat(latest?.cdmaLevel).isEqualTo(2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataConnectionState() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback =
+                getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+            callback.onDataConnectionStateChanged(100, 200 /* unused */)
+
+            assertThat(latest?.dataConnectionState).isEqualTo(100)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_dataActivity() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DataActivityListener>()
+            callback.onDataActivity(3)
+
+            assertThat(latest?.dataActivityDirection).isEqualTo(3)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testFlowForSubId_carrierNetworkChange() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>()
+            callback.onCarrierNetworkChange(true)
+
+            assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_default() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val type = NETWORK_TYPE_UNKNOWN
+            val expected = DefaultNetworkType(type)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_updatesUsingDefault() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+            val type = NETWORK_TYPE_LTE
+            val expected = DefaultNetworkType(type)
+            val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) }
+            callback.onDisplayInfoChanged(ti)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    @Test
+    fun subscriptionFlow_networkType_updatesUsingOverride() =
+        runBlocking(IMMEDIATE) {
+            var latest: MobileSubscriptionModel? = null
+            val job = underTest.subscriptionModelFlow.onEach { latest = it }.launchIn(this)
+
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+            val type = OVERRIDE_NETWORK_TYPE_LTE_CA
+            val expected = OverrideNetworkType(type)
+            val ti =
+                mock<TelephonyDisplayInfo>().also {
+                    whenever(it.overrideNetworkType).thenReturn(type)
+                }
+            callback.onDisplayInfoChanged(ti)
+
+            assertThat(latest?.resolvedNetworkType).isEqualTo(expected)
+
+            job.cancel()
+        }
+
+    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    private inline fun <reified T> getTelephonyCallbackForType(): T {
+        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+
+    /** Convenience constructor for SignalStrength */
+    private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
+        val signalStrength = mock<SignalStrength>()
+        whenever(signalStrength.isGsm).thenReturn(isGsm)
+        whenever(signalStrength.level).thenReturn(gsmLevel)
+        val cdmaStrength =
+            mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
+        whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
+            .thenReturn(listOf(cdmaStrength))
+
+        return signalStrength
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
new file mode 100644
index 0000000..326e0d281
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.SubscriptionInfo
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyManager
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class MobileConnectionsRepositoryTest : SysuiTestCase() {
+    private lateinit var underTest: MobileConnectionsRepositoryImpl
+
+    @Mock private lateinit var subscriptionManager: SubscriptionManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+
+    private val scope = CoroutineScope(IMMEDIATE)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(
+                broadcastDispatcher.broadcastFlow(
+                    any(),
+                    nullable(),
+                    ArgumentMatchers.anyInt(),
+                    nullable(),
+                )
+            )
+            .thenReturn(flowOf(Unit))
+
+        underTest =
+            MobileConnectionsRepositoryImpl(
+                subscriptionManager,
+                telephonyManager,
+                logger,
+                broadcastDispatcher,
+                context,
+                IMMEDIATE,
+                scope,
+                mock(),
+            )
+    }
+
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    @Test
+    fun testSubscriptions_initiallyEmpty() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
+        }
+
+    @Test
+    fun testSubscriptions_listUpdates() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testSubscriptions_removingSub_updatesList() =
+        runBlocking(IMMEDIATE) {
+            var latest: List<SubscriptionInfo>? = null
+
+            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
+
+            // WHEN 2 networks show up
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // WHEN one network is removed
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // THEN the subscriptions list represents the newest change
+            assertThat(latest).isEqualTo(listOf(SUB_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
+        runBlocking(IMMEDIATE) {
+            assertThat(underTest.activeMobileDataSubscriptionId.value)
+                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
+        }
+
+    @Test
+    fun testActiveDataSubscriptionId_updates() =
+        runBlocking(IMMEDIATE) {
+            var active: Int? = null
+
+            val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
+
+            getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
+                .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+            assertThat(active).isEqualTo(SUB_2_ID)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionRepository_validSubId_isCached() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_1_ID)
+
+            assertThat(repo1).isSameInstanceAs(repo2)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionCache_clearsInvalidSubscriptions() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1, SUB_2))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            // Get repos to trigger caching
+            val repo1 = underTest.getRepoForSubId(SUB_1_ID)
+            val repo2 = underTest.getRepoForSubId(SUB_2_ID)
+
+            assertThat(underTest.getSubIdRepoCache())
+                .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2)
+
+            // SUB_2 disappears
+            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+                .thenReturn(listOf(SUB_1))
+            getSubscriptionCallback().onSubscriptionsChanged()
+
+            assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
+
+            job.cancel()
+        }
+
+    @Test
+    fun testConnectionRepository_invalidSubId_throws() =
+        runBlocking(IMMEDIATE) {
+            val job = underTest.subscriptionsFlow.launchIn(this)
+
+            assertThrows(IllegalArgumentException::class.java) {
+                underTest.getRepoForSubId(SUB_1_ID)
+            }
+
+            job.cancel()
+        }
+
+    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
+        val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+        verify(subscriptionManager)
+            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
+        return callbackCaptor.value!!
+    }
+
+    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    private inline fun <reified T> getTelephonyCallbackForType(): T {
+        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SUB_1_ID = 1
+        private val SUB_1 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
+
+        private const val SUB_2_ID = 2
+        private val SUB_2 =
+            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
deleted file mode 100644
index 316b795..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt
+++ /dev/null
@@ -1,360 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.pipeline.mobile.data.repository
-
-import android.telephony.CellSignalStrengthCdma
-import android.telephony.ServiceState
-import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
-import android.telephony.SubscriptionManager
-import android.telephony.TelephonyCallback
-import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
-import android.telephony.TelephonyCallback.CarrierNetworkListener
-import android.telephony.TelephonyCallback.DataActivityListener
-import android.telephony.TelephonyCallback.DataConnectionStateListener
-import android.telephony.TelephonyCallback.DisplayInfoListener
-import android.telephony.TelephonyCallback.ServiceStateListener
-import android.telephony.TelephonyCallback.SignalStrengthsListener
-import android.telephony.TelephonyDisplayInfo
-import android.telephony.TelephonyManager
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-class MobileSubscriptionRepositoryTest : SysuiTestCase() {
-    private lateinit var underTest: MobileSubscriptionRepositoryImpl
-
-    @Mock private lateinit var subscriptionManager: SubscriptionManager
-    @Mock private lateinit var telephonyManager: TelephonyManager
-    private val scope = CoroutineScope(IMMEDIATE)
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-
-        underTest =
-            MobileSubscriptionRepositoryImpl(
-                subscriptionManager,
-                telephonyManager,
-                IMMEDIATE,
-                scope,
-            )
-    }
-
-    @After
-    fun tearDown() {
-        scope.cancel()
-    }
-
-    @Test
-    fun testSubscriptions_initiallyEmpty() =
-        runBlocking(IMMEDIATE) {
-            assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>())
-        }
-
-    @Test
-    fun testSubscriptions_listUpdates() =
-        runBlocking(IMMEDIATE) {
-            var latest: List<SubscriptionInfo>? = null
-
-            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
-
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_1, SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
-
-            job.cancel()
-        }
-
-    @Test
-    fun testSubscriptions_removingSub_updatesList() =
-        runBlocking(IMMEDIATE) {
-            var latest: List<SubscriptionInfo>? = null
-
-            val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this)
-
-            // WHEN 2 networks show up
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_1, SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            // WHEN one network is removed
-            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
-                .thenReturn(listOf(SUB_2))
-            getSubscriptionCallback().onSubscriptionsChanged()
-
-            // THEN the subscriptions list represents the newest change
-            assertThat(latest).isEqualTo(listOf(SUB_2))
-
-            job.cancel()
-        }
-
-    @Test
-    fun testActiveDataSubscriptionId_initialValueIsInvalidId() =
-        runBlocking(IMMEDIATE) {
-            assertThat(underTest.activeMobileDataSubscriptionId.value)
-                .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
-        }
-
-    @Test
-    fun testActiveDataSubscriptionId_updates() =
-        runBlocking(IMMEDIATE) {
-            var active: Int? = null
-
-            val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this)
-
-            getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
-            assertThat(active).isEqualTo(SUB_2_ID)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_default() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            assertThat(latest).isEqualTo(MobileSubscriptionModel())
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_emergencyOnly() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val serviceState = ServiceState()
-            serviceState.isEmergencyOnly = true
-
-            getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
-
-            assertThat(latest?.isEmergencyOnly).isEqualTo(true)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_emergencyOnly_toggles() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<ServiceStateListener>()
-            val serviceState = ServiceState()
-            serviceState.isEmergencyOnly = true
-            callback.onServiceStateChanged(serviceState)
-            serviceState.isEmergencyOnly = false
-            callback.onServiceStateChanged(serviceState)
-
-            assertThat(latest?.isEmergencyOnly).isEqualTo(false)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_signalStrengths_levelsUpdate() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<SignalStrengthsListener>()
-            val strength = signalStrength(1, 2, true)
-            callback.onSignalStrengthsChanged(strength)
-
-            assertThat(latest?.isGsm).isEqualTo(true)
-            assertThat(latest?.primaryLevel).isEqualTo(1)
-            assertThat(latest?.cdmaLevel).isEqualTo(2)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_dataConnectionState() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DataConnectionStateListener>()
-            callback.onDataConnectionStateChanged(100, 200 /* unused */)
-
-            assertThat(latest?.dataConnectionState).isEqualTo(100)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_dataActivity() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DataActivityListener>()
-            callback.onDataActivity(3)
-
-            assertThat(latest?.dataActivityDirection).isEqualTo(3)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_carrierNetworkChange() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<CarrierNetworkListener>()
-            callback.onCarrierNetworkChange(true)
-
-            assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_displayInfo() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            val callback = getTelephonyCallbackForType<DisplayInfoListener>()
-            val ti = mock<TelephonyDisplayInfo>()
-            callback.onDisplayInfoChanged(ti)
-
-            assertThat(latest?.displayInfo).isEqualTo(ti)
-
-            job.cancel()
-        }
-
-    @Test
-    fun testFlowForSubId_isCached() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            val state1 = underTest.getFlowForSubId(SUB_1_ID)
-            val state2 = underTest.getFlowForSubId(SUB_1_ID)
-
-            assertThat(state1).isEqualTo(state2)
-        }
-
-    @Test
-    fun testFlowForSubId_isRemovedAfterFinish() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager)
-
-            var latest: MobileSubscriptionModel? = null
-
-            // Start collecting on some flow
-            val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this)
-
-            // There should be once cached flow now
-            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1)
-
-            // When the job is canceled, the cache should be cleared
-            job.cancel()
-
-            assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0)
-        }
-
-    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
-        val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
-        verify(subscriptionManager)
-            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
-        return callbackCaptor.value!!
-    }
-
-    private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener =
-        getTelephonyCallbackForType()
-
-    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
-        val callbackCaptor = argumentCaptor<TelephonyCallback>()
-        verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
-        return callbackCaptor.allValues
-    }
-
-    private inline fun <reified T> getTelephonyCallbackForType(): T {
-        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
-        assertThat(cbs.size).isEqualTo(1)
-        return cbs[0]
-    }
-
-    /** Convenience constructor for SignalStrength */
-    private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength {
-        val signalStrength = mock<SignalStrength>()
-        whenever(signalStrength.isGsm).thenReturn(isGsm)
-        whenever(signalStrength.level).thenReturn(gsmLevel)
-        val cdmaStrength =
-            mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) }
-        whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java))
-            .thenReturn(listOf(cdmaStrength))
-
-        return signalStrength
-    }
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-        private const val SUB_1_ID = 1
-        private val SUB_1 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-
-        private const val SUB_2_ID = 2
-        private val SUB_2 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 8ec68f3..cd4dbeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -23,7 +23,7 @@
 
 class FakeMobileIconInteractor : MobileIconInteractor {
     private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN)
-    override val iconGroup = _iconGroup
+    override val networkTypeIconGroup = _iconGroup
 
     private val _isEmergencyOnly = MutableStateFlow<Boolean>(false)
     override val isEmergencyOnly = _isEmergencyOnly
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
new file mode 100644
index 0000000..2bd2286
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+
+import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+import android.telephony.TelephonyManager.NETWORK_TYPE_GSM
+import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
+import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) :
+    MobileIconsInteractor {
+    val THREE_G_KEY = mobileMappings.toIconKey(THREE_G)
+    val LTE_KEY = mobileMappings.toIconKey(LTE)
+    val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G)
+    val FIVE_G_OVERRIDE_KEY = mobileMappings.toIconKeyOverride(FIVE_G_OVERRIDE)
+
+    /**
+     * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to
+     * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for
+     * the exhaustive set of icons
+     */
+    val TEST_MAPPING: Map<String, MobileIconGroup> =
+        mapOf(
+            THREE_G_KEY to TelephonyIcons.THREE_G,
+            LTE_KEY to TelephonyIcons.LTE,
+            FOUR_G_KEY to TelephonyIcons.FOUR_G,
+            FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G,
+        )
+
+    private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionInfo>>(listOf())
+    override val filteredSubscriptions = _filteredSubscriptions
+
+    private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
+    override val defaultMobileIconMapping = _defaultMobileIconMapping
+
+    private val _defaultMobileIconGroup = MutableStateFlow(DEFAULT_ICON)
+    override val defaultMobileIconGroup = _defaultMobileIconGroup
+
+    private val _isUserSetup = MutableStateFlow(true)
+    override val isUserSetup = _isUserSetup
+
+    /** Always returns a new fake interactor */
+    override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
+        return FakeMobileIconInteractor()
+    }
+
+    companion object {
+        val DEFAULT_ICON = TelephonyIcons.G
+
+        // Use [MobileMappings] to define some simple definitions
+        const val THREE_G = NETWORK_TYPE_GSM
+        const val LTE = NETWORK_TYPE_LTE
+        const val FOUR_G = NETWORK_TYPE_UMTS
+        const val FIVE_G_OVERRIDE = OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 2f07d9c..ff44af4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -18,10 +18,19 @@
 
 import android.telephony.CellSignalStrength
 import android.telephony.SubscriptionInfo
+import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
 import androidx.test.filters.SmallTest
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FIVE_G_OVERRIDE
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.FOUR_G
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -29,26 +38,33 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.junit.Before
 import org.junit.Test
 
 @SmallTest
 class MobileIconInteractorTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconInteractor
-    private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository()
-    private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID)
+    private val mobileMappingsProxy = FakeMobileMappingsProxy()
+    private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy)
+    private val connectionRepository = FakeMobileConnectionRepository()
 
     @Before
     fun setUp() {
-        underTest = MobileIconInteractorImpl(sub1Flow)
+        underTest =
+            MobileIconInteractorImpl(
+                mobileIconsInteractor.defaultMobileIconMapping,
+                mobileIconsInteractor.defaultMobileIconGroup,
+                mobileMappingsProxy,
+                connectionRepository,
+            )
     }
 
     @Test
     fun gsm_level_default_unknown() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(isGsm = true),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -62,13 +78,12 @@
     @Test
     fun gsm_usesGsmLevel() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(
                     isGsm = true,
                     primaryLevel = GSM_LEVEL,
                     cdmaLevel = CDMA_LEVEL
                 ),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -82,9 +97,8 @@
     @Test
     fun cdma_level_default_unknown() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(isGsm = false),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -97,13 +111,12 @@
     @Test
     fun cdma_usesCdmaLevel() =
         runBlocking(IMMEDIATE) {
-            mobileSubscriptionRepository.setMobileSubscriptionModel(
+            connectionRepository.setMobileSubscriptionModel(
                 MobileSubscriptionModel(
                     isGsm = false,
                     primaryLevel = GSM_LEVEL,
                     cdmaLevel = CDMA_LEVEL
                 ),
-                SUB_1_ID
             )
 
             var latest: Int? = null
@@ -114,6 +127,75 @@
             job.cancel()
         }
 
+    @Test
+    fun iconGroup_three_g() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(TelephonyIcons.THREE_G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_updates_on_change() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = DefaultNetworkType(THREE_G)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    resolvedNetworkType = DefaultNetworkType(FOUR_G),
+                ),
+            )
+            yield()
+
+            assertThat(latest).isEqualTo(TelephonyIcons.FOUR_G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_5g_override_type() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(resolvedNetworkType = OverrideNetworkType(FIVE_G_OVERRIDE)),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(TelephonyIcons.NR_5G)
+
+            job.cancel()
+        }
+
+    @Test
+    fun iconGroup_default_if_no_lookup() =
+        runBlocking(IMMEDIATE) {
+            connectionRepository.setMobileSubscriptionModel(
+                MobileSubscriptionModel(
+                    resolvedNetworkType = DefaultNetworkType(NETWORK_TYPE_UNKNOWN),
+                ),
+            )
+
+            var latest: MobileIconGroup? = null
+            val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(FakeMobileIconsInteractor.DEFAULT_ICON)
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
@@ -123,9 +205,5 @@
         private const val SUB_1_ID = 1
         private val SUB_1 =
             mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
-
-        private const val SUB_2_ID = 2
-        private val SUB_2 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 89ad9cb..b01efd1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -19,12 +19,14 @@
 import android.telephony.SubscriptionInfo
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.util.CarrierConfigTracker
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -39,7 +41,9 @@
 class MobileIconsInteractorTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconsInteractor
     private val userSetupRepository = FakeUserSetupRepository()
-    private val subscriptionsRepository = FakeMobileSubscriptionRepository()
+    private val subscriptionsRepository = FakeMobileConnectionsRepository()
+    private val mobileMappingsProxy = FakeMobileMappingsProxy()
+    private val scope = CoroutineScope(IMMEDIATE)
 
     @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
 
@@ -47,10 +51,12 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         underTest =
-            MobileIconsInteractor(
+            MobileIconsInteractorImpl(
                 subscriptionsRepository,
                 carrierConfigTracker,
+                mobileMappingsProxy,
                 userSetupRepository,
+                scope
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
new file mode 100644
index 0000000..6d8d902
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.util
+
+import com.android.settingslib.SignalIcon.MobileIconGroup
+import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.settingslib.mobile.TelephonyIcons
+
+class FakeMobileMappingsProxy : MobileMappingsProxy {
+    private var iconMap = mapOf<String, MobileIconGroup>()
+    private var defaultIcons = TelephonyIcons.THREE_G
+
+    fun setIconMap(map: Map<String, MobileIconGroup>) {
+        iconMap = map
+    }
+    override fun mapIconSets(config: Config): Map<String, MobileIconGroup> = iconMap
+    fun getIconMap() = iconMap
+
+    fun setDefaultIcons(group: MobileIconGroup) {
+        defaultIcons = group
+    }
+    override fun getDefaultIcons(config: Config): MobileIconGroup = defaultIcons
+    fun getDefaultIcons(): MobileIconGroup = defaultIcons
+
+    override fun toIconKey(networkType: Int): String {
+        return networkType.toString()
+    }
+
+    override fun toIconKeyOverride(networkType: Int): String {
+        return toIconKey(networkType) + "_override"
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index e18dd3a..7d5f06c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -140,6 +140,40 @@
     }
 
     @Test
+    fun testOnUnfold_hingeAngleDecreasesBeforeInnerScreenAvailable_emitsOnlyStartAndInnerScreenAvailableEvents() {
+        setFoldState(folded = true)
+        foldUpdates.clear()
+
+        setFoldState(folded = false)
+        screenOnStatusProvider.notifyScreenTurningOn()
+        sendHingeAngleEvent(10)
+        sendHingeAngleEvent(20)
+        sendHingeAngleEvent(10)
+        screenOnStatusProvider.notifyScreenTurnedOn()
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING,
+                FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE)
+    }
+
+    @Test
+    fun testOnUnfold_hingeAngleDecreasesAfterInnerScreenAvailable_emitsStartInnerScreenAvailableAndStartClosingEvents() {
+        setFoldState(folded = true)
+        foldUpdates.clear()
+
+        setFoldState(folded = false)
+        screenOnStatusProvider.notifyScreenTurningOn()
+        sendHingeAngleEvent(10)
+        sendHingeAngleEvent(20)
+        screenOnStatusProvider.notifyScreenTurnedOn()
+        sendHingeAngleEvent(30)
+        sendHingeAngleEvent(40)
+        sendHingeAngleEvent(10)
+
+        assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_OPENING,
+                FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE, FOLD_UPDATE_START_CLOSING)
+    }
+
+    @Test
     fun testOnFolded_stopsHingeAngleProvider() {
         setFoldState(folded = true)
 
@@ -237,7 +271,7 @@
     }
 
     @Test
-    fun startClosingEvent_afterTimeout_abortEmitted() {
+    fun startClosingEvent_afterTimeout_finishHalfOpenEventEmitted() {
         sendHingeAngleEvent(90)
         sendHingeAngleEvent(80)
 
@@ -269,7 +303,7 @@
     }
 
     @Test
-    fun startClosingEvent_timeoutAfterTimeoutRescheduled_abortEmitted() {
+    fun startClosingEvent_timeoutAfterTimeoutRescheduled_finishHalfOpenStateEmitted() {
         sendHingeAngleEvent(180)
         sendHingeAngleEvent(90)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java
similarity index 77%
rename from packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java
index 76bff1d..7e8ffeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperColorExtractorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/canvas/WallpaperLocalColorExtractorTest.java
@@ -54,7 +54,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
-public class WallpaperColorExtractorTest extends SysuiTestCase {
+public class WallpaperLocalColorExtractorTest extends SysuiTestCase {
     private static final int LOW_BMP_WIDTH = 128;
     private static final int LOW_BMP_HEIGHT = 128;
     private static final int HIGH_BMP_WIDTH = 3000;
@@ -105,11 +105,11 @@
         return bitmap;
     }
 
-    private WallpaperColorExtractor getSpyWallpaperColorExtractor() {
+    private WallpaperLocalColorExtractor getSpyWallpaperLocalColorExtractor() {
 
-        WallpaperColorExtractor wallpaperColorExtractor = new WallpaperColorExtractor(
+        WallpaperLocalColorExtractor colorExtractor = new WallpaperLocalColorExtractor(
                 mBackgroundExecutor,
-                new WallpaperColorExtractor.WallpaperColorExtractorCallback() {
+                new WallpaperLocalColorExtractor.WallpaperLocalColorExtractorCallback() {
                     @Override
                     public void onColorsProcessed(List<RectF> regions,
                             List<WallpaperColors> colors) {
@@ -132,25 +132,25 @@
                         mDeactivatedCount++;
                     }
                 });
-        WallpaperColorExtractor spyWallpaperColorExtractor = spy(wallpaperColorExtractor);
+        WallpaperLocalColorExtractor spyColorExtractor = spy(colorExtractor);
 
         doAnswer(invocation -> {
             mMiniBitmapWidth = invocation.getArgument(1);
             mMiniBitmapHeight = invocation.getArgument(2);
             return getMockBitmap(mMiniBitmapWidth, mMiniBitmapHeight);
-        }).when(spyWallpaperColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
+        }).when(spyColorExtractor).createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
 
 
         doAnswer(invocation -> getMockBitmap(
                         invocation.getArgument(1),
                         invocation.getArgument(2)))
-                .when(spyWallpaperColorExtractor)
+                .when(spyColorExtractor)
                 .createMiniBitmap(any(Bitmap.class), anyInt(), anyInt());
 
         doReturn(new WallpaperColors(Color.valueOf(0), Color.valueOf(0), Color.valueOf(0)))
-                .when(spyWallpaperColorExtractor).getLocalWallpaperColors(any(Rect.class));
+                .when(spyColorExtractor).getLocalWallpaperColors(any(Rect.class));
 
-        return spyWallpaperColorExtractor;
+        return spyColorExtractor;
     }
 
     private RectF randomArea() {
@@ -180,18 +180,18 @@
      */
     @Test
     public void testMiniBitmapCreation() {
-        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+        WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
         int nSimulations = 10;
         for (int i = 0; i < nSimulations; i++) {
             resetCounters();
             int width = randomBetween(LOW_BMP_WIDTH, HIGH_BMP_WIDTH);
             int height = randomBetween(LOW_BMP_HEIGHT, HIGH_BMP_HEIGHT);
             Bitmap bitmap = getMockBitmap(width, height);
-            spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+            spyColorExtractor.onBitmapChanged(bitmap);
 
             assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
             assertThat(Math.min(mMiniBitmapWidth, mMiniBitmapHeight))
-                    .isAtMost(WallpaperColorExtractor.SMALL_SIDE);
+                    .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE);
         }
     }
 
@@ -201,18 +201,18 @@
      */
     @Test
     public void testSmallMiniBitmapCreation() {
-        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+        WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
         int nSimulations = 10;
         for (int i = 0; i < nSimulations; i++) {
             resetCounters();
             int width = randomBetween(VERY_LOW_BMP_WIDTH, LOW_BMP_WIDTH);
             int height = randomBetween(VERY_LOW_BMP_HEIGHT, LOW_BMP_HEIGHT);
             Bitmap bitmap = getMockBitmap(width, height);
-            spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+            spyColorExtractor.onBitmapChanged(bitmap);
 
             assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
             assertThat(Math.max(mMiniBitmapWidth, mMiniBitmapHeight))
-                    .isAtMost(WallpaperColorExtractor.SMALL_SIDE);
+                    .isAtMost(WallpaperLocalColorExtractor.SMALL_SIDE);
         }
     }
 
@@ -228,15 +228,15 @@
         int nSimulations = 10;
         for (int i = 0; i < nSimulations; i++) {
             resetCounters();
-            WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+            WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
             List<RectF> regions = listOfRandomAreas(MIN_AREAS, MAX_AREAS);
             int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
             List<Runnable> tasks = Arrays.asList(
-                    () -> spyWallpaperColorExtractor.onPageChanged(nPages),
-                    () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
-                    () -> spyWallpaperColorExtractor.setDisplayDimensions(
+                    () -> spyColorExtractor.onPageChanged(nPages),
+                    () -> spyColorExtractor.onBitmapChanged(bitmap),
+                    () -> spyColorExtractor.setDisplayDimensions(
                             DISPLAY_WIDTH, DISPLAY_HEIGHT),
-                    () -> spyWallpaperColorExtractor.addLocalColorsAreas(
+                    () -> spyColorExtractor.addLocalColorsAreas(
                             regions));
             Collections.shuffle(tasks);
             tasks.forEach(Runnable::run);
@@ -245,7 +245,7 @@
             assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
             assertThat(mColorsProcessed).isEqualTo(regions.size());
 
-            spyWallpaperColorExtractor.removeLocalColorAreas(regions);
+            spyColorExtractor.removeLocalColorAreas(regions);
             assertThat(mDeactivatedCount).isEqualTo(1);
         }
     }
@@ -260,7 +260,7 @@
         int nSimulations = 10;
         for (int i = 0; i < nSimulations; i++) {
             resetCounters();
-            WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+            WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
             List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
             List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
             List<RectF> regions = new ArrayList<>();
@@ -268,20 +268,20 @@
             regions.addAll(regions2);
             int nPages = randomBetween(PAGES_LOW, PAGES_HIGH);
             List<Runnable> tasks = Arrays.asList(
-                    () -> spyWallpaperColorExtractor.onPageChanged(nPages),
-                    () -> spyWallpaperColorExtractor.onBitmapChanged(bitmap),
-                    () -> spyWallpaperColorExtractor.setDisplayDimensions(
+                    () -> spyColorExtractor.onPageChanged(nPages),
+                    () -> spyColorExtractor.onBitmapChanged(bitmap),
+                    () -> spyColorExtractor.setDisplayDimensions(
                             DISPLAY_WIDTH, DISPLAY_HEIGHT),
-                    () -> spyWallpaperColorExtractor.removeLocalColorAreas(regions1));
+                    () -> spyColorExtractor.removeLocalColorAreas(regions1));
 
-            spyWallpaperColorExtractor.addLocalColorsAreas(regions);
+            spyColorExtractor.addLocalColorsAreas(regions);
             assertThat(mActivatedCount).isEqualTo(1);
             Collections.shuffle(tasks);
             tasks.forEach(Runnable::run);
 
             assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
             assertThat(mDeactivatedCount).isEqualTo(0);
-            spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
+            spyColorExtractor.removeLocalColorAreas(regions2);
             assertThat(mDeactivatedCount).isEqualTo(1);
         }
     }
@@ -295,18 +295,18 @@
     @Test
     public void testRecomputeColorExtraction() {
         Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
-        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
+        WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
         List<RectF> regions1 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
         List<RectF> regions2 = listOfRandomAreas(MIN_AREAS / 2, MAX_AREAS / 2);
         List<RectF> regions = new ArrayList<>();
         regions.addAll(regions1);
         regions.addAll(regions2);
-        spyWallpaperColorExtractor.addLocalColorsAreas(regions);
+        spyColorExtractor.addLocalColorsAreas(regions);
         assertThat(mActivatedCount).isEqualTo(1);
         int nPages = PAGES_LOW;
-        spyWallpaperColorExtractor.onBitmapChanged(bitmap);
-        spyWallpaperColorExtractor.onPageChanged(nPages);
-        spyWallpaperColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT);
+        spyColorExtractor.onBitmapChanged(bitmap);
+        spyColorExtractor.onPageChanged(nPages);
+        spyColorExtractor.setDisplayDimensions(DISPLAY_WIDTH, DISPLAY_HEIGHT);
 
         int nSimulations = 20;
         for (int i = 0; i < nSimulations; i++) {
@@ -315,22 +315,22 @@
             // verify that if we remove some regions, they are not recomputed after other changes
             if (i == nSimulations / 2) {
                 regions.removeAll(regions2);
-                spyWallpaperColorExtractor.removeLocalColorAreas(regions2);
+                spyColorExtractor.removeLocalColorAreas(regions2);
             }
 
             if (Math.random() >= 0.5) {
                 int nPagesNew = randomBetween(PAGES_LOW, PAGES_HIGH);
                 if (nPagesNew == nPages) continue;
                 nPages = nPagesNew;
-                spyWallpaperColorExtractor.onPageChanged(nPagesNew);
+                spyColorExtractor.onPageChanged(nPagesNew);
             } else {
                 Bitmap newBitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
-                spyWallpaperColorExtractor.onBitmapChanged(newBitmap);
+                spyColorExtractor.onBitmapChanged(newBitmap);
                 assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
             }
             assertThat(mColorsProcessed).isEqualTo(regions.size());
         }
-        spyWallpaperColorExtractor.removeLocalColorAreas(regions);
+        spyColorExtractor.removeLocalColorAreas(regions);
         assertThat(mDeactivatedCount).isEqualTo(1);
     }
 
@@ -339,12 +339,12 @@
         resetCounters();
         Bitmap bitmap = getMockBitmap(HIGH_BMP_WIDTH, HIGH_BMP_HEIGHT);
         doNothing().when(bitmap).recycle();
-        WallpaperColorExtractor spyWallpaperColorExtractor = getSpyWallpaperColorExtractor();
-        spyWallpaperColorExtractor.onPageChanged(PAGES_LOW);
-        spyWallpaperColorExtractor.onBitmapChanged(bitmap);
+        WallpaperLocalColorExtractor spyColorExtractor = getSpyWallpaperLocalColorExtractor();
+        spyColorExtractor.onPageChanged(PAGES_LOW);
+        spyColorExtractor.onBitmapChanged(bitmap);
         assertThat(mMiniBitmapUpdatedCount).isEqualTo(1);
-        spyWallpaperColorExtractor.cleanUp();
-        spyWallpaperColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS));
+        spyColorExtractor.cleanUp();
+        spyColorExtractor.addLocalColorsAreas(listOfRandomAreas(MIN_AREAS, MAX_AREAS));
         assertThat(mColorsProcessed).isEqualTo(0);
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
index 7af66f6..7ae47b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -28,6 +28,7 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.notetask.NoteTaskInitializer;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -36,7 +37,6 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.desktopmode.DesktopMode;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
-import com.android.wm.shell.floating.FloatingTasks;
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.onehanded.OneHandedEventCallback;
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -78,18 +78,31 @@
     @Mock ProtoTracer mProtoTracer;
     @Mock UserTracker mUserTracker;
     @Mock ShellExecutor mSysUiMainExecutor;
-    @Mock FloatingTasks mFloatingTasks;
+    @Mock NoteTaskInitializer mNoteTaskInitializer;
     @Mock DesktopMode mDesktopMode;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mWMShell = new WMShell(mContext, mShellInterface, Optional.of(mPip),
-                Optional.of(mSplitScreen), Optional.of(mOneHanded), Optional.of(mFloatingTasks),
+        mWMShell = new WMShell(
+                mContext,
+                mShellInterface,
+                Optional.of(mPip),
+                Optional.of(mSplitScreen),
+                Optional.of(mOneHanded),
                 Optional.of(mDesktopMode),
-                mCommandQueue, mConfigurationController, mKeyguardStateController,
-                mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer,
-                mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor);
+                mCommandQueue,
+                mConfigurationController,
+                mKeyguardStateController,
+                mKeyguardUpdateMonitor,
+                mScreenLifecycle,
+                mSysUiState,
+                mProtoTracer,
+                mWakefulnessLifecycle,
+                mUserTracker,
+                mNoteTaskInitializer,
+                mSysUiMainExecutor
+        );
     }
 
     @Test
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
index 8d171be..69575a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
@@ -26,7 +26,9 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatcher
 import org.mockito.Mockito
+import org.mockito.Mockito.`when`
 import org.mockito.stubbing.OngoingStubbing
+import org.mockito.stubbing.Stubber
 
 /**
  * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
@@ -89,7 +91,8 @@
  *
  * @see Mockito.when
  */
-fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
+fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall)
+fun <T> Stubber.whenever(mock: T): T = `when`(mock)
 
 /**
  * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
index 043aff6..b568186 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.unfold.progress
 
+import android.os.Trace
 import android.util.Log
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -117,6 +118,7 @@
 
         if (DEBUG) {
             Log.d(TAG, "onFoldUpdate = $update")
+            Trace.traceCounter(Trace.TRACE_TAG_APP, "fold_update", update)
         }
     }
 
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index 07473b3..808128d 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -16,6 +16,7 @@
 package com.android.systemui.unfold.updates
 
 import android.os.Handler
+import android.os.Trace
 import android.util.Log
 import androidx.annotation.FloatRange
 import androidx.annotation.VisibleForTesting
@@ -108,6 +109,7 @@
     private fun onHingeAngle(angle: Float) {
         if (DEBUG) {
             Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle")
+            Trace.traceCounter(Trace.TRACE_TAG_APP, "hinge_angle", angle.toInt())
         }
 
         val isClosing = angle < lastHingeAngle
@@ -115,8 +117,16 @@
         val closingThresholdMet = closingThreshold == null || angle < closingThreshold
         val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
         val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING
+        val screenAvailableEventSent = isUnfoldHandled
 
-        if (isClosing && closingThresholdMet && !closingEventDispatched && !isFullyOpened) {
+        if (isClosing // hinge angle should be decreasing since last update
+                && closingThresholdMet // hinge angle is below certain threshold
+                && !closingEventDispatched  // we haven't sent closing event already
+                && !isFullyOpened // do not send closing event if we are in fully opened hinge
+                                  // angle range as closing threshold could overlap this range
+                && screenAvailableEventSent // do not send closing event if we are still in
+                                            // the process of turning on the inner display
+        ) {
             notifyFoldUpdate(FOLD_UPDATE_START_CLOSING)
         }
 
diff --git a/proto/src/task_snapshot.proto b/proto/src/task_snapshot.proto
deleted file mode 100644
index 1cbc17e..0000000
--- a/proto/src/task_snapshot.proto
+++ /dev/null
@@ -1,48 +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.
- */
-
- syntax = "proto3";
-
- package com.android.server.wm;
-
- option java_package = "com.android.server.wm";
- option java_outer_classname = "WindowManagerProtos";
-
- message TaskSnapshotProto {
-     int32 orientation = 1;
-     int32 inset_left = 2;
-     int32 inset_top = 3;
-     int32 inset_right = 4;
-     int32 inset_bottom = 5;
-     bool is_real_snapshot = 6;
-     int32 windowing_mode = 7;
-     int32 system_ui_visibility = 8 [deprecated=true];
-     bool is_translucent = 9;
-     string top_activity_component = 10;
-     // deprecated because original width and height are stored now instead of the scale.
-     float legacy_scale = 11 [deprecated=true];
-     int64 id = 12;
-     int32 rotation = 13;
-     // The task width when the snapshot was taken
-     int32 task_width = 14;
-     // The task height when the snapshot was taken
-     int32 task_height = 15;
-     int32 appearance = 16;
-     int32 letterbox_inset_left = 17;
-     int32 letterbox_inset_top = 18;
-     int32 letterbox_inset_right = 19;
-     int32 letterbox_inset_bottom = 20;
- }
diff --git a/proto/src/windowmanager.proto b/proto/src/windowmanager.proto
new file mode 100644
index 0000000..f26404c6
--- /dev/null
+++ b/proto/src/windowmanager.proto
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+package com.android.server.wm;
+
+option java_package = "com.android.server.wm";
+option java_outer_classname = "WindowManagerProtos";
+
+message TaskSnapshotProto {
+  int32 orientation = 1;
+  int32 inset_left = 2;
+  int32 inset_top = 3;
+  int32 inset_right = 4;
+  int32 inset_bottom = 5;
+  bool is_real_snapshot = 6;
+  int32 windowing_mode = 7;
+  int32 system_ui_visibility = 8 [deprecated=true];
+  bool is_translucent = 9;
+  string top_activity_component = 10;
+  // deprecated because original width and height are stored now instead of the scale.
+  float legacy_scale = 11 [deprecated=true];
+  int64 id = 12;
+  int32 rotation = 13;
+  // The task width when the snapshot was taken
+  int32 task_width = 14;
+  // The task height when the snapshot was taken
+  int32 task_height = 15;
+  int32 appearance = 16;
+  int32 letterbox_inset_left = 17;
+  int32 letterbox_inset_top = 18;
+  int32 letterbox_inset_right = 19;
+  int32 letterbox_inset_bottom = 20;
+}
+
+// Persistent letterboxing configurations
+message LetterboxProto {
+
+  // Possible values for the letterbox horizontal reachability
+  enum LetterboxHorizontalReachability {
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT = 0;
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER = 1;
+    LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT = 2;
+  }
+
+  // Possible values for the letterbox vertical reachability
+  enum LetterboxVerticalReachability {
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP = 0;
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER = 1;
+    LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM = 2;
+  }
+
+  // Represents the current horizontal position for the letterboxed activity
+  LetterboxHorizontalReachability letterbox_position_for_horizontal_reachability = 1;
+  // Represents the current vertical position for the letterboxed activity
+  LetterboxVerticalReachability letterbox_position_for_vertical_reachability = 2;
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 745555c..d2ba9c6 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -4018,7 +4018,7 @@
         }
     }
 
-    private void setLeAudioVolumeOnModeUpdate(int mode, int streamType, int device) {
+    private void setLeAudioVolumeOnModeUpdate(int mode, int device) {
         switch (mode) {
             case AudioSystem.MODE_IN_COMMUNICATION:
             case AudioSystem.MODE_IN_CALL:
@@ -4032,10 +4032,16 @@
                 return;
         }
 
-        // Currently, DEVICE_OUT_BLE_HEADSET is the only output type for LE_AUDIO profile.
-        // (See AudioDeviceBroker#createBtDeviceInfo())
-        int index = mStreamStates[streamType].getIndex(AudioSystem.DEVICE_OUT_BLE_HEADSET);
-        int maxIndex = mStreamStates[streamType].getMaxIndex();
+        // Forcefully set LE audio volume as a workaround, since in some cases
+        // (like the outgoing call) the value of 'device' is not DEVICE_OUT_BLE_*
+        // even when BLE is connected.
+        if (!AudioSystem.isLeAudioDeviceType(device)) {
+            device = AudioSystem.DEVICE_OUT_BLE_HEADSET;
+        }
+
+        final int streamType = getBluetoothContextualVolumeStream(mode);
+        final int index = mStreamStates[streamType].getIndex(device);
+        final int maxIndex = mStreamStates[streamType].getMaxIndex();
 
         if (DEBUG_VOL) {
             Log.d(TAG, "setLeAudioVolumeOnModeUpdate postSetLeAudioVolumeIndex index="
@@ -5421,9 +5427,7 @@
                 // change of mode may require volume to be re-applied on some devices
                 updateAbsVolumeMultiModeDevices(previousMode, mode);
 
-                // Forcefully set LE audio volume as a workaround, since the value of 'device'
-                // is not DEVICE_OUT_BLE_* even when BLE is connected.
-                setLeAudioVolumeOnModeUpdate(mode, streamType, device);
+                setLeAudioVolumeOnModeUpdate(mode, device);
 
                 // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                 // connections not started by the application changing the mode when pid changes
diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
index 1a5f31c..da43618 100644
--- a/services/core/java/com/android/server/biometrics/log/ALSProbe.java
+++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
@@ -52,16 +52,13 @@
     private boolean mDestroyed = false;
     private boolean mDestroyRequested = false;
     private boolean mDisableRequested = false;
-    private volatile NextConsumer mNextConsumer = null;
+    private NextConsumer mNextConsumer = null;
     private volatile float mLastAmbientLux = -1;
 
     private final SensorEventListener mLightSensorListener = new SensorEventListener() {
         @Override
         public void onSensorChanged(SensorEvent event) {
-            mLastAmbientLux = event.values[0];
-            if (mNextConsumer != null) {
-                completeNextConsumer(mLastAmbientLux);
-            }
+            onNext(event.values[0]);
         }
 
         @Override
@@ -133,11 +130,29 @@
 
         // if a final consumer is set it will call destroy/disable on the next value if requested
         if (!mDestroyed && mNextConsumer == null) {
-            disable();
+            disableLightSensorLoggingLocked();
             mDestroyed = true;
         }
     }
 
+    private synchronized void onNext(float value) {
+        mLastAmbientLux = value;
+
+        final NextConsumer consumer = mNextConsumer;
+        mNextConsumer = null;
+        if (consumer != null) {
+            Slog.v(TAG, "Finishing next consumer");
+
+            if (mDestroyRequested) {
+                destroy();
+            } else if (mDisableRequested) {
+                disable();
+            }
+
+            consumer.consume(value);
+        }
+    }
+
     /** The most recent lux reading. */
     public float getMostRecentLux() {
         return mLastAmbientLux;
@@ -160,7 +175,7 @@
             @Nullable Handler handler) {
         final NextConsumer nextConsumer = new NextConsumer(consumer, handler);
         final float current = mLastAmbientLux;
-        if (current > 0) {
+        if (current > -1f) {
             nextConsumer.consume(current);
         } else if (mDestroyed) {
             nextConsumer.consume(-1f);
@@ -172,23 +187,6 @@
         }
     }
 
-    private synchronized void completeNextConsumer(float value) {
-        Slog.v(TAG, "Finishing next consumer");
-
-        final NextConsumer consumer = mNextConsumer;
-        mNextConsumer = null;
-
-        if (mDestroyRequested) {
-            destroy();
-        } else if (mDisableRequested) {
-            disable();
-        }
-
-        if (consumer != null) {
-            consumer.consume(value);
-        }
-    }
-
     private void enableLightSensorLoggingLocked() {
         if (!mEnabled) {
             mEnabled = true;
@@ -219,9 +217,13 @@
         }
     }
 
-    private void onTimeout() {
+    private synchronized void onTimeout() {
         Slog.e(TAG, "Max time exceeded for ALS logger - disabling: "
                 + mLightSensorListener.hashCode());
+
+        // if consumers are waiting but there was no sensor change, complete them with the latest
+        // value before disabling
+        onNext(mLastAmbientLux);
         disable();
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index a55b118..37f980d 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1978,34 +1978,39 @@
             return (haystack & needle) != 0;
         }
 
-        public boolean isInLockDownMode() {
-            return mIsInLockDownMode;
+        // Return whether the user is in lockdown mode.
+        // If the flag is not set, we assume the user is not in lockdown.
+        public boolean isInLockDownMode(int userId) {
+            return mUserInLockDownMode.get(userId, false);
         }
 
         @Override
         public synchronized void onStrongAuthRequiredChanged(int userId) {
             boolean userInLockDownModeNext = containsFlag(getStrongAuthForUser(userId),
                     STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
-            mUserInLockDownMode.put(userId, userInLockDownModeNext);
-            boolean isInLockDownModeNext = mUserInLockDownMode.indexOfValue(true) != -1;
 
-            if (mIsInLockDownMode == isInLockDownModeNext) {
+            // Nothing happens if the lockdown mode of userId keeps the same.
+            if (userInLockDownModeNext == isInLockDownMode(userId)) {
                 return;
             }
 
-            if (isInLockDownModeNext) {
-                cancelNotificationsWhenEnterLockDownMode();
+            // When the lockdown mode is changed, we perform the following steps.
+            // If the userInLockDownModeNext is true, all the function calls to
+            // notifyPostedLocked and notifyRemovedLocked will not be executed.
+            // The cancelNotificationsWhenEnterLockDownMode calls notifyRemovedLocked
+            // and postNotificationsWhenExitLockDownMode calls notifyPostedLocked.
+            // So we shall call cancelNotificationsWhenEnterLockDownMode before
+            // we set mUserInLockDownMode as true.
+            // On the other hand, if the userInLockDownModeNext is false, we shall call
+            // postNotificationsWhenExitLockDownMode after we put false into mUserInLockDownMode
+            if (userInLockDownModeNext) {
+                cancelNotificationsWhenEnterLockDownMode(userId);
             }
 
-            // When the mIsInLockDownMode is true, both notifyPostedLocked and
-            // notifyRemovedLocked will be dismissed. So we shall call
-            // cancelNotificationsWhenEnterLockDownMode before we set mIsInLockDownMode
-            // as true and call postNotificationsWhenExitLockDownMode after we set
-            // mIsInLockDownMode as false.
-            mIsInLockDownMode = isInLockDownModeNext;
+            mUserInLockDownMode.put(userId, userInLockDownModeNext);
 
-            if (!isInLockDownModeNext) {
-                postNotificationsWhenExitLockDownMode();
+            if (!userInLockDownModeNext) {
+                postNotificationsWhenExitLockDownMode(userId);
             }
         }
     }
@@ -9679,11 +9684,14 @@
         }
     }
 
-    private void cancelNotificationsWhenEnterLockDownMode() {
+    private void cancelNotificationsWhenEnterLockDownMode(int userId) {
         synchronized (mNotificationLock) {
             int numNotifications = mNotificationList.size();
             for (int i = 0; i < numNotifications; i++) {
                 NotificationRecord rec = mNotificationList.get(i);
+                if (rec.getUser().getIdentifier() != userId) {
+                    continue;
+                }
                 mListeners.notifyRemovedLocked(rec, REASON_CANCEL_ALL,
                         rec.getStats());
             }
@@ -9691,14 +9699,23 @@
         }
     }
 
-    private void postNotificationsWhenExitLockDownMode() {
+    private void postNotificationsWhenExitLockDownMode(int userId) {
         synchronized (mNotificationLock) {
             int numNotifications = mNotificationList.size();
+            // Set the delay to spread out the burst of notifications.
+            long delay = 0;
             for (int i = 0; i < numNotifications; i++) {
                 NotificationRecord rec = mNotificationList.get(i);
-                mListeners.notifyPostedLocked(rec, rec);
+                if (rec.getUser().getIdentifier() != userId) {
+                    continue;
+                }
+                mHandler.postDelayed(() -> {
+                    synchronized (mNotificationLock) {
+                        mListeners.notifyPostedLocked(rec, rec);
+                    }
+                }, delay);
+                delay += 20;
             }
-
         }
     }
 
@@ -9877,6 +9894,9 @@
 
         for (int i = 0; i < N; i++) {
             NotificationRecord record = mNotificationList.get(i);
+            if (isInLockDownMode(record.getUser().getIdentifier())) {
+                continue;
+            }
             if (!isVisibleToListener(record.getSbn(), record.getNotificationType(), info)) {
                 continue;
             }
@@ -9918,8 +9938,8 @@
                 rankings.toArray(new NotificationListenerService.Ranking[0]));
     }
 
-    boolean isInLockDownMode() {
-        return mStrongAuthTracker.isInLockDownMode();
+    boolean isInLockDownMode(int userId) {
+        return mStrongAuthTracker.isInLockDownMode(userId);
     }
 
     boolean hasCompanionDevice(ManagedServiceInfo info) {
@@ -10989,7 +11009,7 @@
         @GuardedBy("mNotificationLock")
         void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
                 boolean notifyAllListeners) {
-            if (isInLockDownMode()) {
+            if (isInLockDownMode(r.getUser().getIdentifier())) {
                 return;
             }
 
@@ -11095,7 +11115,7 @@
         @GuardedBy("mNotificationLock")
         public void notifyRemovedLocked(NotificationRecord r, int reason,
                 NotificationStats notificationStats) {
-            if (isInLockDownMode()) {
+            if (isInLockDownMode(r.getUser().getIdentifier())) {
                 return;
             }
 
@@ -11144,10 +11164,6 @@
          */
         @GuardedBy("mNotificationLock")
         public void notifyRankingUpdateLocked(List<NotificationRecord> changedHiddenNotifications) {
-            if (isInLockDownMode()) {
-                return;
-            }
-
             boolean isHiddenRankingUpdate = changedHiddenNotifications != null
                     && changedHiddenNotifications.size() > 0;
             // TODO (b/73052211): if the ranking update changed the notification type,
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 32ef014..fa0d41c 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4052,6 +4052,9 @@
             case KeyEvent.KEYCODE_DEMO_APP_2:
             case KeyEvent.KEYCODE_DEMO_APP_3:
             case KeyEvent.KEYCODE_DEMO_APP_4: {
+                // TODO(b/254604589): Dispatch KeyEvent to System UI.
+                sendSystemKeyToStatusBarAsync(keyCode);
+
                 // Just drop if keys are not intercepted for direct key.
                 result &= ~ACTION_PASS_TO_USER;
                 break;
@@ -4125,6 +4128,10 @@
         mCameraGestureTriggered = true;
         if (mRequestedOrSleepingDefaultDisplay) {
             mCameraGestureTriggeredDuringGoingToSleep = true;
+            // Wake device up early to prevent display doing redundant turning off/on stuff.
+            wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey,
+                    PowerManager.WAKE_REASON_CAMERA_LAUNCH,
+                    "android.policy:CAMERA_GESTURE_PREVENT_LOCK");
         }
         return true;
     }
@@ -4656,11 +4663,6 @@
             }
             mDefaultDisplayRotation.updateOrientationListener();
             reportScreenStateToVrManager(false);
-            if (mCameraGestureTriggeredDuringGoingToSleep) {
-                wakeUp(SystemClock.uptimeMillis(), mAllowTheaterModeWakeFromPowerKey,
-                        PowerManager.WAKE_REASON_CAMERA_LAUNCH,
-                        "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK");
-            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index 5c1a877..27370bf 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -1460,6 +1460,12 @@
                 || transit == TRANSIT_OLD_ACTIVITY_RELAUNCH;
     }
 
+    static boolean isTaskFragmentTransitOld(@TransitionOldType int transit) {
+        return transit == TRANSIT_OLD_TASK_FRAGMENT_OPEN
+                || transit == TRANSIT_OLD_TASK_FRAGMENT_CLOSE
+                || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
+    }
+
     static boolean isChangeTransitOld(@TransitionOldType int transit) {
         return transit == TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE
                 || transit == TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index bf4b65d..3a8fbbb 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -173,6 +173,7 @@
         mWindowContainer = windowContainer;
         // TODO: remove the frame provider for non-WindowState container.
         mFrameProvider = frameProvider;
+        mOverrideFrames.clear();
         mOverrideFrameProviders = overrideFrameProviders;
         if (windowContainer == null) {
             setServerVisible(false);
@@ -234,6 +235,8 @@
         updateSourceFrameForServerVisibility();
 
         if (mOverrideFrameProviders != null) {
+            // Not necessary to clear the mOverrideFrames here. It will be cleared every time the
+            // override frame provider updates.
             for (int i = mOverrideFrameProviders.size() - 1; i >= 0; i--) {
                 final int windowType = mOverrideFrameProviders.keyAt(i);
                 final Rect overrideFrame;
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index a469c6b..c19353c 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -17,14 +17,17 @@
 package com.android.server.wm;
 
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.graphics.Color;
 
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.function.Function;
 
 /** Reads letterbox configs from resources and controls their overrides at runtime. */
 final class LetterboxConfiguration {
@@ -156,34 +159,25 @@
     // portrait device orientation.
     private boolean mIsVerticalReachabilityEnabled;
 
-
-    // Horizontal position of a center of the letterboxed app window which is global to prevent
-    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
-    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
-    // LetterboxUiController#getHorizontalPositionMultiplier which is called from
-    // ActivityRecord#updateResolvedBoundsPosition.
-    // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from
-    // Overview after changing position in another app.
-    @LetterboxHorizontalReachabilityPosition
-    private volatile int mLetterboxPositionForHorizontalReachability;
-
-    // Vertical position of a center of the letterboxed app window which is global to prevent
-    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
-    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
-    // LetterboxUiController#getVerticalPositionMultiplier which is called from
-    // ActivityRecord#updateResolvedBoundsPosition.
-    // TODO(b/199426138): Global reachability setting causes a jump when resuming an app from
-    // Overview after changing position in another app.
-    @LetterboxVerticalReachabilityPosition
-    private volatile int mLetterboxPositionForVerticalReachability;
-
     // Whether education is allowed for letterboxed fullscreen apps.
     private boolean mIsEducationEnabled;
 
     // Whether using split screen aspect ratio as a default aspect ratio for unresizable apps.
     private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled;
 
+    // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier
+    @NonNull
+    private final LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+
     LetterboxConfiguration(Context systemUiContext) {
+        this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
+                () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext),
+                () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext)));
+    }
+
+    @VisibleForTesting
+    LetterboxConfiguration(Context systemUiContext,
+            LetterboxConfigurationPersister letterboxConfigurationPersister) {
         mContext = systemUiContext;
         mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat(
                 R.dimen.config_fixedOrientationLetterboxAspectRatio);
@@ -206,14 +200,14 @@
                 readLetterboxHorizontalReachabilityPositionFromConfig(mContext);
         mDefaultPositionForVerticalReachability =
                 readLetterboxVerticalReachabilityPositionFromConfig(mContext);
-        mLetterboxPositionForHorizontalReachability = mDefaultPositionForHorizontalReachability;
-        mLetterboxPositionForVerticalReachability = mDefaultPositionForVerticalReachability;
         mIsEducationEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsEducationEnabled);
         setDefaultMinAspectRatioForUnresizableApps(mContext.getResources().getFloat(
                 R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps));
         mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean(
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
+        mLetterboxConfigurationPersister = letterboxConfigurationPersister;
+        mLetterboxConfigurationPersister.start();
     }
 
     /**
@@ -653,7 +647,9 @@
      * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
     float getHorizontalMultiplierForReachability() {
-        switch (mLetterboxPositionForHorizontalReachability) {
+        final int letterboxPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        switch (letterboxPositionForHorizontalReachability) {
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT:
                 return 0.0f;
             case LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER:
@@ -662,10 +658,11 @@
                 return 1.0f;
             default:
                 throw new AssertionError(
-                    "Unexpected letterbox position type: "
-                            + mLetterboxPositionForHorizontalReachability);
+                        "Unexpected letterbox position type: "
+                                + letterboxPositionForHorizontalReachability);
         }
     }
+
     /*
      * Gets vertical position of a center of the letterboxed app window when reachability
      * is enabled specified. 0 corresponds to the top side of the screen and 1 to the bottom side.
@@ -673,7 +670,9 @@
      * <p>The position multiplier is changed after each double tap in the letterbox area.
      */
     float getVerticalMultiplierForReachability() {
-        switch (mLetterboxPositionForVerticalReachability) {
+        final int letterboxPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        switch (letterboxPositionForVerticalReachability) {
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP:
                 return 0.0f;
             case LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER:
@@ -683,7 +682,7 @@
             default:
                 throw new AssertionError(
                         "Unexpected letterbox position type: "
-                                + mLetterboxPositionForVerticalReachability);
+                                + letterboxPositionForVerticalReachability);
         }
     }
 
@@ -693,7 +692,7 @@
      */
     @LetterboxHorizontalReachabilityPosition
     int getLetterboxPositionForHorizontalReachability() {
-        return mLetterboxPositionForHorizontalReachability;
+        return mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
     }
 
     /*
@@ -702,7 +701,7 @@
      */
     @LetterboxVerticalReachabilityPosition
     int getLetterboxPositionForVerticalReachability() {
-        return mLetterboxPositionForVerticalReachability;
+        return mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
     }
 
     /** Returns a string representing the given {@link LetterboxHorizontalReachabilityPosition}. */
@@ -742,9 +741,8 @@
      * right side.
      */
     void movePositionForHorizontalReachabilityToNextRightStop() {
-        mLetterboxPositionForHorizontalReachability = Math.min(
-                mLetterboxPositionForHorizontalReachability + 1,
-                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT);
+        updatePositionForHorizontalReachability(prev -> Math.min(
+                prev + 1, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT));
     }
 
     /**
@@ -752,8 +750,7 @@
      * side.
      */
     void movePositionForHorizontalReachabilityToNextLeftStop() {
-        mLetterboxPositionForHorizontalReachability =
-                Math.max(mLetterboxPositionForHorizontalReachability - 1, 0);
+        updatePositionForHorizontalReachability(prev -> Math.max(prev - 1, 0));
     }
 
     /**
@@ -761,9 +758,8 @@
      * side.
      */
     void movePositionForVerticalReachabilityToNextBottomStop() {
-        mLetterboxPositionForVerticalReachability = Math.min(
-                mLetterboxPositionForVerticalReachability + 1,
-                LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM);
+        updatePositionForVerticalReachability(prev -> Math.min(
+                prev + 1, LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM));
     }
 
     /**
@@ -771,8 +767,7 @@
      * side.
      */
     void movePositionForVerticalReachabilityToNextTopStop() {
-        mLetterboxPositionForVerticalReachability =
-                Math.max(mLetterboxPositionForVerticalReachability - 1, 0);
+        updatePositionForVerticalReachability(prev -> Math.max(prev - 1, 0));
     }
 
     /**
@@ -822,4 +817,26 @@
                 R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
     }
 
+    /** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */
+    private void updatePositionForHorizontalReachability(
+            Function<Integer, Integer> newHorizonalPositionFun) {
+        final int letterboxPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int nextHorizontalPosition = newHorizonalPositionFun.apply(
+                letterboxPositionForHorizontalReachability);
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                nextHorizontalPosition);
+    }
+
+    /** Calculates a new letterboxPositionForVerticalReachability value and updates the store */
+    private void updatePositionForVerticalReachability(
+            Function<Integer, Integer> newVerticalPositionFun) {
+        final int letterboxPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int nextVerticalPosition = newVerticalPositionFun.apply(
+                letterboxPositionForVerticalReachability);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                nextVerticalPosition);
+    }
+
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
new file mode 100644
index 0000000..70639b1
--- /dev/null
+++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Environment;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.wm.LetterboxConfiguration.LetterboxHorizontalReachabilityPosition;
+import com.android.server.wm.LetterboxConfiguration.LetterboxVerticalReachabilityPosition;
+import com.android.server.wm.nano.WindowManagerProtos;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Persists the values of letterboxPositionForHorizontalReachability and
+ * letterboxPositionForVerticalReachability for {@link LetterboxConfiguration}.
+ */
+class LetterboxConfigurationPersister {
+
+    private static final String TAG =
+            TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM;
+
+    @VisibleForTesting
+    static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config";
+
+    private final Context mContext;
+    private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier;
+    private final Supplier<Integer> mDefaultVerticalReachabilitySupplier;
+
+    // Horizontal position of a center of the letterboxed app window which is global to prevent
+    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
+    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
+    // LetterboxUiController#getHorizontalPositionMultiplier which is called from
+    // ActivityRecord#updateResolvedBoundsPosition.
+    @LetterboxHorizontalReachabilityPosition
+    private volatile int mLetterboxPositionForHorizontalReachability;
+
+    // Vertical position of a center of the letterboxed app window which is global to prevent
+    // "jumps" when switching between letterboxed apps. It's updated to reposition the app window
+    // in response to a double tap gesture (see LetterboxUiController#handleDoubleTap). Used in
+    // LetterboxUiController#getVerticalPositionMultiplier which is called from
+    // ActivityRecord#updateResolvedBoundsPosition.
+    @LetterboxVerticalReachabilityPosition
+    private volatile int mLetterboxPositionForVerticalReachability;
+
+    @NonNull
+    private final AtomicFile mConfigurationFile;
+
+    @Nullable
+    private final Consumer<String> mCompletionCallback;
+
+    @NonNull
+    private final PersisterQueue mPersisterQueue;
+
+    LetterboxConfigurationPersister(Context systemUiContext,
+            Supplier<Integer> defaultHorizontalReachabilitySupplier,
+            Supplier<Integer> defaultVerticalReachabilitySupplier) {
+        this(systemUiContext, defaultHorizontalReachabilitySupplier,
+                defaultVerticalReachabilitySupplier,
+                Environment.getDataSystemDirectory(), new PersisterQueue(),
+                /* completionCallback */ null);
+    }
+
+    @VisibleForTesting
+    LetterboxConfigurationPersister(Context systemUiContext,
+            Supplier<Integer> defaultHorizontalReachabilitySupplier,
+            Supplier<Integer> defaultVerticalReachabilitySupplier, File configFolder,
+            PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) {
+        mContext = systemUiContext.createDeviceProtectedStorageContext();
+        mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier;
+        mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier;
+        mCompletionCallback = completionCallback;
+        final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME);
+        mConfigurationFile = new AtomicFile(prefFiles);
+        mPersisterQueue = persisterQueue;
+        readCurrentConfiguration();
+    }
+
+    /**
+     * Startes the persistence queue
+     */
+    void start() {
+        mPersisterQueue.startPersisting();
+    }
+
+    /*
+     * Gets the horizontal position of the letterboxed app window when horizontal reachability is
+     * enabled.
+     */
+    @LetterboxHorizontalReachabilityPosition
+    int getLetterboxPositionForHorizontalReachability() {
+        return mLetterboxPositionForHorizontalReachability;
+    }
+
+    /*
+     * Gets the vertical position of the letterboxed app window when vertical reachability is
+     * enabled.
+     */
+    @LetterboxVerticalReachabilityPosition
+    int getLetterboxPositionForVerticalReachability() {
+        return mLetterboxPositionForVerticalReachability;
+    }
+
+    /**
+     * Updates letterboxPositionForVerticalReachability if different from the current value
+     */
+    void setLetterboxPositionForHorizontalReachability(
+            int letterboxPositionForHorizontalReachability) {
+        if (mLetterboxPositionForHorizontalReachability
+                != letterboxPositionForHorizontalReachability) {
+            mLetterboxPositionForHorizontalReachability =
+                    letterboxPositionForHorizontalReachability;
+            updateConfiguration();
+        }
+    }
+
+    /**
+     * Updates letterboxPositionForVerticalReachability if different from the current value
+     */
+    void setLetterboxPositionForVerticalReachability(
+            int letterboxPositionForVerticalReachability) {
+        if (mLetterboxPositionForVerticalReachability != letterboxPositionForVerticalReachability) {
+            mLetterboxPositionForVerticalReachability = letterboxPositionForVerticalReachability;
+            updateConfiguration();
+        }
+    }
+
+    @VisibleForTesting
+    void useDefaultValue() {
+        mLetterboxPositionForHorizontalReachability = mDefaultHorizontalReachabilitySupplier.get();
+        mLetterboxPositionForVerticalReachability = mDefaultVerticalReachabilitySupplier.get();
+    }
+
+    private void readCurrentConfiguration() {
+        FileInputStream fis = null;
+        try {
+            fis = mConfigurationFile.openRead();
+            byte[] protoData = readInputStream(fis);
+            final WindowManagerProtos.LetterboxProto letterboxData =
+                    WindowManagerProtos.LetterboxProto.parseFrom(protoData);
+            mLetterboxPositionForHorizontalReachability =
+                    letterboxData.letterboxPositionForHorizontalReachability;
+            mLetterboxPositionForVerticalReachability =
+                    letterboxData.letterboxPositionForVerticalReachability;
+        } catch (IOException ioe) {
+            Slog.e(TAG,
+                    "Error reading from LetterboxConfigurationPersister. "
+                            + "Using default values!", ioe);
+            useDefaultValue();
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException e) {
+                    useDefaultValue();
+                    Slog.e(TAG, "Error reading from LetterboxConfigurationPersister ", e);
+                }
+            }
+        }
+    }
+
+    private void updateConfiguration() {
+        mPersisterQueue.addItem(new UpdateValuesCommand(mConfigurationFile,
+                mLetterboxPositionForHorizontalReachability,
+                mLetterboxPositionForVerticalReachability,
+                mCompletionCallback), /* flush */ true);
+    }
+
+    private static byte[] readInputStream(InputStream in) throws IOException {
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        try {
+            byte[] buffer = new byte[1024];
+            int size = in.read(buffer);
+            while (size > 0) {
+                outputStream.write(buffer, 0, size);
+                size = in.read(buffer);
+            }
+            return outputStream.toByteArray();
+        } finally {
+            outputStream.close();
+        }
+    }
+
+    private static class UpdateValuesCommand implements
+            PersisterQueue.WriteQueueItem<UpdateValuesCommand> {
+
+        @NonNull
+        private final AtomicFile mFileToUpdate;
+        @Nullable
+        private final Consumer<String> mOnComplete;
+
+
+        private final int mHorizontalReachability;
+        private final int mVerticalReachability;
+
+        UpdateValuesCommand(@NonNull AtomicFile fileToUpdate,
+                int horizontalReachability, int verticalReachability,
+                @Nullable Consumer<String> onComplete) {
+            mFileToUpdate = fileToUpdate;
+            mHorizontalReachability = horizontalReachability;
+            mVerticalReachability = verticalReachability;
+            mOnComplete = onComplete;
+        }
+
+        @Override
+        public void process() {
+            final WindowManagerProtos.LetterboxProto letterboxData =
+                    new WindowManagerProtos.LetterboxProto();
+            letterboxData.letterboxPositionForHorizontalReachability = mHorizontalReachability;
+            letterboxData.letterboxPositionForVerticalReachability = mVerticalReachability;
+            final byte[] bytes = WindowManagerProtos.LetterboxProto.toByteArray(letterboxData);
+
+            FileOutputStream fos = null;
+            try {
+                fos = mFileToUpdate.startWrite();
+                fos.write(bytes);
+                mFileToUpdate.finishWrite(fos);
+            } catch (IOException ioe) {
+                mFileToUpdate.failWrite(fos);
+                Slog.e(TAG,
+                        "Error writing to LetterboxConfigurationPersister. "
+                                + "Using default values!", ioe);
+            } finally {
+                if (mOnComplete != null) {
+                    mOnComplete.accept("UpdateValuesCommand");
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index ac1a2b1..95371a5 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -320,11 +320,11 @@
             } finally {
                 mService.closeSurfaceTransaction("RemoteAnimationController#finished");
             }
+            // Reset input for all activities when the remote animation is finished.
+            final Consumer<ActivityRecord> updateActivities =
+                    activity -> activity.setDropInputForAnimation(false);
+            mDisplayContent.forAllActivities(updateActivities);
         }
-        // Reset input for all activities when the remote animation is finished.
-        final Consumer<ActivityRecord> updateActivities =
-                activity -> activity.setDropInputForAnimation(false);
-        mDisplayContent.forAllActivities(updateActivities);
         setRunningRemoteAnimation(false);
         ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "Finishing remote animation");
     }
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 867833a..509b1e6 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -184,19 +184,30 @@
         }
 
         void dispose() {
-            while (!mOrganizedTaskFragments.isEmpty()) {
-                final TaskFragment taskFragment = mOrganizedTaskFragments.get(0);
-                // Cleanup before remove to prevent it from sending any additional event, such as
-                // #onTaskFragmentVanished, to the removed organizer.
+            for (int i = mOrganizedTaskFragments.size() - 1; i >= 0; i--) {
+                // Cleanup the TaskFragmentOrganizer from all TaskFragments it organized before
+                // removing the windows to prevent it from adding any additional TaskFragment
+                // pending event.
+                final TaskFragment taskFragment = mOrganizedTaskFragments.get(i);
                 taskFragment.onTaskFragmentOrganizerRemoved();
-                taskFragment.removeImmediately();
-                mOrganizedTaskFragments.remove(taskFragment);
             }
+
+            // Defer to avoid unnecessary layout when there are multiple TaskFragments removal.
+            mAtmService.deferWindowLayout();
+            try {
+                while (!mOrganizedTaskFragments.isEmpty()) {
+                    final TaskFragment taskFragment = mOrganizedTaskFragments.remove(0);
+                    taskFragment.removeImmediately();
+                }
+            } finally {
+                mAtmService.continueWindowLayout();
+            }
+
             for (int i = mDeferredTransitions.size() - 1; i >= 0; i--) {
                 // Cleanup any running transaction to unblock the current transition.
                 onTransactionFinished(mDeferredTransitions.keyAt(i));
             }
-            mOrganizer.asBinder().unlinkToDeath(this, 0 /*flags*/);
+            mOrganizer.asBinder().unlinkToDeath(this, 0 /* flags */);
         }
 
         @NonNull
@@ -426,7 +437,6 @@
 
     @Override
     public void unregisterOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
-        validateAndGetState(organizer);
         final int pid = Binder.getCallingPid();
         final long uid = Binder.getCallingUid();
         final long origId = Binder.clearCallingIdentity();
@@ -607,6 +617,13 @@
             int opType, @NonNull Throwable exception) {
         validateAndGetState(organizer);
         Slog.w(TAG, "onTaskFragmentError ", exception);
+        final PendingTaskFragmentEvent vanishedEvent = taskFragment != null
+                ? getPendingTaskFragmentEvent(taskFragment, PendingTaskFragmentEvent.EVENT_VANISHED)
+                : null;
+        if (vanishedEvent != null) {
+            // No need to notify if the TaskFragment has been removed.
+            return;
+        }
         addPendingEvent(new PendingTaskFragmentEvent.Builder(
                 PendingTaskFragmentEvent.EVENT_ERROR, organizer)
                 .setErrorCallbackToken(errorCallbackToken)
@@ -690,11 +707,17 @@
     }
 
     private void removeOrganizer(@NonNull ITaskFragmentOrganizer organizer) {
-        final TaskFragmentOrganizerState state = validateAndGetState(organizer);
+        final TaskFragmentOrganizerState state = mTaskFragmentOrganizerState.get(
+                organizer.asBinder());
+        if (state == null) {
+            Slog.w(TAG, "The organizer has already been removed.");
+            return;
+        }
+        // Remove any pending event of this organizer first because state.dispose() may trigger
+        // event dispatch as result of surface placement.
+        mPendingTaskFragmentEvents.remove(organizer.asBinder());
         // remove all of the children of the organized TaskFragment
         state.dispose();
-        // Remove any pending event of this organizer.
-        mPendingTaskFragmentEvents.remove(organizer.asBinder());
         mTaskFragmentOrganizerState.remove(organizer.asBinder());
     }
 
@@ -878,23 +901,6 @@
         return null;
     }
 
-    private boolean shouldSendEventWhenTaskInvisible(@NonNull PendingTaskFragmentEvent event) {
-        if (event.mEventType == PendingTaskFragmentEvent.EVENT_ERROR
-                // Always send parent info changed to update task visibility
-                || event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) {
-            return true;
-        }
-
-        final TaskFragmentOrganizerState state =
-                mTaskFragmentOrganizerState.get(event.mTaskFragmentOrg.asBinder());
-        final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos.get(event.mTaskFragment);
-        final TaskFragmentInfo info = event.mTaskFragment.getTaskFragmentInfo();
-        // Send an info changed callback if this event is for the last activities to finish in a
-        // TaskFragment so that the {@link TaskFragmentOrganizer} can delete this TaskFragment.
-        return event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED
-                && lastInfo != null && lastInfo.hasRunningActivity() && info.isEmpty();
-    }
-
     void dispatchPendingEvents() {
         if (mAtmService.mWindowManager.mWindowPlacerLocked.isLayoutDeferred()
                 || mPendingTaskFragmentEvents.isEmpty()) {
@@ -908,37 +914,19 @@
         }
     }
 
-    void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
+    private void dispatchPendingEvents(@NonNull TaskFragmentOrganizerState state,
             @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
         if (pendingEvents.isEmpty()) {
             return;
         }
-
-        final ArrayList<Task> visibleTasks = new ArrayList<>();
-        final ArrayList<Task> invisibleTasks = new ArrayList<>();
-        final ArrayList<PendingTaskFragmentEvent> candidateEvents = new ArrayList<>();
-        for (int i = 0, n = pendingEvents.size(); i < n; i++) {
-            final PendingTaskFragmentEvent event = pendingEvents.get(i);
-            final Task task = event.mTaskFragment != null ? event.mTaskFragment.getTask() : null;
-            // TODO(b/251132298): move visibility check to the client side.
-            if (task != null && (task.lastActiveTime <= event.mDeferTime
-                    || !(isTaskVisible(task, visibleTasks, invisibleTasks)
-                    || shouldSendEventWhenTaskInvisible(event)))) {
-                // Defer sending events to the TaskFragment until the host task is active again.
-                event.mDeferTime = task.lastActiveTime;
-                continue;
-            }
-            candidateEvents.add(event);
-        }
-        final int numEvents = candidateEvents.size();
-        if (numEvents == 0) {
+        if (shouldDeferPendingEvents(state, pendingEvents)) {
             return;
         }
-
         mTmpTaskSet.clear();
+        final int numEvents = pendingEvents.size();
         final TaskFragmentTransaction transaction = new TaskFragmentTransaction();
         for (int i = 0; i < numEvents; i++) {
-            final PendingTaskFragmentEvent event = candidateEvents.get(i);
+            final PendingTaskFragmentEvent event = pendingEvents.get(i);
             if (event.mEventType == PendingTaskFragmentEvent.EVENT_APPEARED
                     || event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
                 final Task task = event.mTaskFragment.getTask();
@@ -954,7 +942,47 @@
         }
         mTmpTaskSet.clear();
         state.dispatchTransaction(transaction);
-        pendingEvents.removeAll(candidateEvents);
+        pendingEvents.clear();
+    }
+
+    /**
+     * Whether or not to defer sending the events to the organizer to avoid waking the app process
+     * when it is in background. We want to either send all events or none to avoid inconsistency.
+     */
+    private boolean shouldDeferPendingEvents(@NonNull TaskFragmentOrganizerState state,
+            @NonNull List<PendingTaskFragmentEvent> pendingEvents) {
+        final ArrayList<Task> visibleTasks = new ArrayList<>();
+        final ArrayList<Task> invisibleTasks = new ArrayList<>();
+        for (int i = 0, n = pendingEvents.size(); i < n; i++) {
+            final PendingTaskFragmentEvent event = pendingEvents.get(i);
+            if (event.mEventType != PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED
+                    && event.mEventType != PendingTaskFragmentEvent.EVENT_INFO_CHANGED
+                    && event.mEventType != PendingTaskFragmentEvent.EVENT_APPEARED) {
+                // Send events for any other types.
+                return false;
+            }
+
+            // Check if we should send the event given the Task visibility and events.
+            final Task task;
+            if (event.mEventType == PendingTaskFragmentEvent.EVENT_PARENT_INFO_CHANGED) {
+                task = event.mTask;
+            } else {
+                task = event.mTaskFragment.getTask();
+            }
+            if (task.lastActiveTime > event.mDeferTime
+                    && isTaskVisible(task, visibleTasks, invisibleTasks)) {
+                // Send events when the app has at least one visible Task.
+                return false;
+            } else if (shouldSendEventWhenTaskInvisible(task, state, event)) {
+                // Sent events even if the Task is invisible.
+                return false;
+            }
+
+            // Defer sending events to the organizer until the host task is active (visible) again.
+            event.mDeferTime = task.lastActiveTime;
+        }
+        // Defer for invisible Task.
+        return true;
     }
 
     private static boolean isTaskVisible(@NonNull Task task,
@@ -975,6 +1003,28 @@
         }
     }
 
+    private boolean shouldSendEventWhenTaskInvisible(@NonNull Task task,
+            @NonNull TaskFragmentOrganizerState state,
+            @NonNull PendingTaskFragmentEvent event) {
+        final TaskFragmentParentInfo lastParentInfo = state.mLastSentTaskFragmentParentInfos
+                .get(task.mTaskId);
+        if (lastParentInfo == null || lastParentInfo.isVisible()) {
+            // When the Task was visible, or when there was no Task info changed sent (in which case
+            // the organizer will consider it as visible by default), always send the event to
+            // update the Task visibility.
+            return true;
+        }
+        if (event.mEventType == PendingTaskFragmentEvent.EVENT_INFO_CHANGED) {
+            // Send info changed if the TaskFragment is becoming empty/non-empty so the
+            // organizer can choose whether or not to remove the TaskFragment.
+            final TaskFragmentInfo lastInfo = state.mLastSentTaskFragmentInfos
+                    .get(event.mTaskFragment);
+            final boolean isEmpty = event.mTaskFragment.getNonFinishingActivityCount() == 0;
+            return lastInfo == null || lastInfo.isEmpty() != isEmpty;
+        }
+        return false;
+    }
+
     void dispatchPendingInfoChangedEvent(@NonNull TaskFragment taskFragment) {
         final PendingTaskFragmentEvent event = getPendingTaskFragmentEvent(taskFragment,
                 PendingTaskFragmentEvent.EVENT_INFO_CHANGED);
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 46253c1..32f6197 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1602,7 +1602,11 @@
             change.setMode(info.getTransitMode(target));
             change.setStartAbsBounds(info.mAbsoluteBounds);
             change.setFlags(info.getChangeFlags(target));
+
             final Task task = target.asTask();
+            final TaskFragment taskFragment = target.asTaskFragment();
+            final ActivityRecord activityRecord = target.asActivityRecord();
+
             if (task != null) {
                 final ActivityManager.RunningTaskInfo tinfo = new ActivityManager.RunningTaskInfo();
                 task.fillTaskInfo(tinfo);
@@ -1636,12 +1640,7 @@
             change.setEndRelOffset(bounds.left - parentBounds.left,
                     bounds.top - parentBounds.top);
             int endRotation = target.getWindowConfiguration().getRotation();
-            final ActivityRecord activityRecord = target.asActivityRecord();
             if (activityRecord != null) {
-                final Task arTask = activityRecord.getTask();
-                final int backgroundColor = ColorUtils.setAlphaComponent(
-                        arTask.getTaskDescription().getBackgroundColor(), 255);
-                change.setBackgroundColor(backgroundColor);
                 // TODO(b/227427984): Shell needs to aware letterbox.
                 // Always use parent bounds of activity because letterbox area (e.g. fixed aspect
                 // ratio or size compat mode) should be included in the animation.
@@ -1654,6 +1653,18 @@
             } else {
                 change.setEndAbsBounds(bounds);
             }
+
+            if (activityRecord != null || (taskFragment != null && taskFragment.isEmbedded())) {
+                // Set background color to Task theme color for activity and embedded TaskFragment
+                // in case we want to show background during the animation.
+                final Task parentTask = activityRecord != null
+                        ? activityRecord.getTask()
+                        : taskFragment.getTask();
+                final int backgroundColor = ColorUtils.setAlphaComponent(
+                        parentTask.getTaskDescription().getBackgroundColor(), 255);
+                change.setBackgroundColor(backgroundColor);
+            }
+
             change.setRotation(info.mRotation, endRotation);
             if (info.mSnapshot != null) {
                 change.setSnapshot(info.mSnapshot, info.mSnapshotLuma);
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 07ae167..bece476 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -41,6 +41,7 @@
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
 import static com.android.server.wm.AppTransition.MAX_APP_TRANSITION_DURATION;
 import static com.android.server.wm.AppTransition.isActivityTransitOld;
+import static com.android.server.wm.AppTransition.isTaskFragmentTransitOld;
 import static com.android.server.wm.AppTransition.isTaskTransitOld;
 import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING;
 import static com.android.server.wm.IdentifierProto.HASH_CODE;
@@ -2985,10 +2986,17 @@
             // {@link Activity#overridePendingTransition(int, int, int)}.
             @ColorInt int backdropColor = 0;
             if (controller.isFromActivityEmbedding()) {
-                final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter);
-                final Animation a = animAttr != 0
-                        ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null;
-                showBackdrop = a != null && a.getShowBackdrop();
+                if (isChanging) {
+                    // When there are more than one changing containers, it may leave part of the
+                    // screen empty. Show background color to cover that.
+                    showBackdrop = getDisplayContent().mChangingContainers.size() > 1;
+                } else {
+                    // Check whether or not to show backdrop for open/close transition.
+                    final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter);
+                    final Animation a = animAttr != 0
+                            ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null;
+                    showBackdrop = a != null && a.getShowBackdrop();
+                }
                 backdropColor = appTransition.getNextAppTransitionBackgroundColor();
             }
             final Rect localBounds = new Rect(mTmpRect);
@@ -3091,9 +3099,16 @@
                 }
             }
 
+            // Check if the animation requests to show background color for Activity and embedded
+            // TaskFragment.
             final ActivityRecord activityRecord = asActivityRecord();
-            if (activityRecord != null && isActivityTransitOld(transit)
-                    && adapter.getShowBackground()) {
+            final TaskFragment taskFragment = asTaskFragment();
+            if (adapter.getShowBackground()
+                    // Check if it is Activity transition.
+                    && ((activityRecord != null && isActivityTransitOld(transit))
+                    // Check if it is embedded TaskFragment transition.
+                    || (taskFragment != null && taskFragment.isEmbedded()
+                    && isTaskFragmentTransitOld(transit)))) {
                 final @ColorInt int backgroundColorForTransition;
                 if (adapter.getBackgroundColor() != 0) {
                     // If available use the background color provided through getBackgroundColor
@@ -3103,9 +3118,11 @@
                     // Otherwise default to the window's background color if provided through
                     // the theme as the background color for the animation - the top most window
                     // with a valid background color and showBackground set takes precedence.
-                    final Task arTask = activityRecord.getTask();
+                    final Task parentTask = activityRecord != null
+                            ? activityRecord.getTask()
+                            : taskFragment.getTask();
                     backgroundColorForTransition = ColorUtils.setAlphaComponent(
-                            arTask.getTaskDescription().getBackgroundColor(), 255);
+                            parentTask.getTaskDescription().getBackgroundColor(), 255);
                 }
                 animationRunnerBuilder.setTaskBackgroundColor(backgroundColorForTransition);
             }
diff --git a/services/tests/mockingservicestests/OWNERS b/services/tests/mockingservicestests/OWNERS
index 2bb1649..4dda51f 100644
--- a/services/tests/mockingservicestests/OWNERS
+++ b/services/tests/mockingservicestests/OWNERS
@@ -1,5 +1,8 @@
 include platform/frameworks/base:/services/core/java/com/android/server/am/OWNERS
+
+# Game Platform
 per-file FakeGameClassifier.java = file:/GAME_MANAGER_OWNERS
 per-file FakeGameServiceProviderInstance = file:/GAME_MANAGER_OWNERS
 per-file FakeServiceConnector.java = file:/GAME_MANAGER_OWNERS
 per-file Game* = file:/GAME_MANAGER_OWNERS
+per-file res/xml/game_manager* = file:/GAME_MANAGER_OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
index 68c9ce4..0cff4f1 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
@@ -178,6 +178,23 @@
     }
 
     @Test
+    public void testWatchDogCompletesAwait() {
+        mProbe.enable();
+
+        AtomicInteger lux = new AtomicInteger(-9);
+        mProbe.awaitNextLux((v) -> lux.set(Math.round(v)), null /* handler */);
+
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        moveTimeBy(TIMEOUT_MS);
+
+        assertThat(lux.get()).isEqualTo(-1);
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
     public void testNextLuxWhenAlreadyEnabledAndNotAvailable() {
         testNextLuxWhenAlreadyEnabled(false /* dataIsAvailable */);
     }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
index 101a5cb..bf66dee 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenersTest.java
@@ -488,63 +488,112 @@
 
     @Test
     public void testNotifyPostedLockedInLockdownMode() {
-        NotificationRecord r = mock(NotificationRecord.class);
-        NotificationRecord old = mock(NotificationRecord.class);
+        NotificationRecord r0 = mock(NotificationRecord.class);
+        NotificationRecord old0 = mock(NotificationRecord.class);
+        UserHandle uh0 = mock(UserHandle.class);
 
-        // before the lockdown mode
-        when(mNm.isInLockDownMode()).thenReturn(false);
-        mListeners.notifyPostedLocked(r, old, true);
-        mListeners.notifyPostedLocked(r, old, false);
-        verify(r, atLeast(2)).getSbn();
+        NotificationRecord r1 = mock(NotificationRecord.class);
+        NotificationRecord old1 = mock(NotificationRecord.class);
+        UserHandle uh1 = mock(UserHandle.class);
 
-        // in the lockdown mode
-        reset(r);
-        reset(old);
-        when(mNm.isInLockDownMode()).thenReturn(true);
-        mListeners.notifyPostedLocked(r, old, true);
-        mListeners.notifyPostedLocked(r, old, false);
-        verify(r, never()).getSbn();
-    }
+        // Neither user0 and user1 is in the lockdown mode
+        when(r0.getUser()).thenReturn(uh0);
+        when(uh0.getIdentifier()).thenReturn(0);
+        when(mNm.isInLockDownMode(0)).thenReturn(false);
 
-    @Test
-    public void testnotifyRankingUpdateLockedInLockdownMode() {
-        List chn = mock(List.class);
+        when(r1.getUser()).thenReturn(uh1);
+        when(uh1.getIdentifier()).thenReturn(1);
+        when(mNm.isInLockDownMode(1)).thenReturn(false);
 
-        // before the lockdown mode
-        when(mNm.isInLockDownMode()).thenReturn(false);
-        mListeners.notifyRankingUpdateLocked(chn);
-        verify(chn, atLeast(1)).size();
+        mListeners.notifyPostedLocked(r0, old0, true);
+        mListeners.notifyPostedLocked(r0, old0, false);
+        verify(r0, atLeast(2)).getSbn();
 
-        // in the lockdown mode
-        reset(chn);
-        when(mNm.isInLockDownMode()).thenReturn(true);
-        mListeners.notifyRankingUpdateLocked(chn);
-        verify(chn, never()).size();
+        mListeners.notifyPostedLocked(r1, old1, true);
+        mListeners.notifyPostedLocked(r1, old1, false);
+        verify(r1, atLeast(2)).getSbn();
+
+        // Reset
+        reset(r0);
+        reset(old0);
+        reset(r1);
+        reset(old1);
+
+        // Only user 0 is in the lockdown mode
+        when(r0.getUser()).thenReturn(uh0);
+        when(uh0.getIdentifier()).thenReturn(0);
+        when(mNm.isInLockDownMode(0)).thenReturn(true);
+
+        when(r1.getUser()).thenReturn(uh1);
+        when(uh1.getIdentifier()).thenReturn(1);
+        when(mNm.isInLockDownMode(1)).thenReturn(false);
+
+        mListeners.notifyPostedLocked(r0, old0, true);
+        mListeners.notifyPostedLocked(r0, old0, false);
+        verify(r0, never()).getSbn();
+
+        mListeners.notifyPostedLocked(r1, old1, true);
+        mListeners.notifyPostedLocked(r1, old1, false);
+        verify(r1, atLeast(2)).getSbn();
     }
 
     @Test
     public void testNotifyRemovedLockedInLockdownMode() throws NoSuchFieldException {
-        NotificationRecord r = mock(NotificationRecord.class);
-        NotificationStats rs = mock(NotificationStats.class);
+        NotificationRecord r0 = mock(NotificationRecord.class);
+        NotificationStats rs0 = mock(NotificationStats.class);
+        UserHandle uh0 = mock(UserHandle.class);
+
+        NotificationRecord r1 = mock(NotificationRecord.class);
+        NotificationStats rs1 = mock(NotificationStats.class);
+        UserHandle uh1 = mock(UserHandle.class);
+
         StatusBarNotification sbn = mock(StatusBarNotification.class);
         FieldSetter.setField(mNm,
                 NotificationManagerService.class.getDeclaredField("mHandler"),
                 mock(NotificationManagerService.WorkerHandler.class));
 
-        // before the lockdown mode
-        when(mNm.isInLockDownMode()).thenReturn(false);
-        when(r.getSbn()).thenReturn(sbn);
-        mListeners.notifyRemovedLocked(r, 0, rs);
-        mListeners.notifyRemovedLocked(r, 0, rs);
-        verify(r, atLeast(2)).getSbn();
+        // Neither user0 and user1 is in the lockdown mode
+        when(r0.getUser()).thenReturn(uh0);
+        when(uh0.getIdentifier()).thenReturn(0);
+        when(mNm.isInLockDownMode(0)).thenReturn(false);
+        when(r0.getSbn()).thenReturn(sbn);
 
-        // in the lockdown mode
-        reset(r);
-        reset(rs);
-        when(mNm.isInLockDownMode()).thenReturn(true);
-        when(r.getSbn()).thenReturn(sbn);
-        mListeners.notifyRemovedLocked(r, 0, rs);
-        mListeners.notifyRemovedLocked(r, 0, rs);
-        verify(r, never()).getSbn();
+        when(r1.getUser()).thenReturn(uh1);
+        when(uh1.getIdentifier()).thenReturn(1);
+        when(mNm.isInLockDownMode(1)).thenReturn(false);
+        when(r1.getSbn()).thenReturn(sbn);
+
+        mListeners.notifyRemovedLocked(r0, 0, rs0);
+        mListeners.notifyRemovedLocked(r0, 0, rs0);
+        verify(r0, atLeast(2)).getSbn();
+
+        mListeners.notifyRemovedLocked(r1, 0, rs1);
+        mListeners.notifyRemovedLocked(r1, 0, rs1);
+        verify(r1, atLeast(2)).getSbn();
+
+        // Reset
+        reset(r0);
+        reset(rs0);
+        reset(r1);
+        reset(rs1);
+
+        // Only user 0 is in the lockdown mode
+        when(r0.getUser()).thenReturn(uh0);
+        when(uh0.getIdentifier()).thenReturn(0);
+        when(mNm.isInLockDownMode(0)).thenReturn(true);
+        when(r0.getSbn()).thenReturn(sbn);
+
+        when(r1.getUser()).thenReturn(uh1);
+        when(uh1.getIdentifier()).thenReturn(1);
+        when(mNm.isInLockDownMode(1)).thenReturn(false);
+        when(r1.getSbn()).thenReturn(sbn);
+
+        mListeners.notifyRemovedLocked(r0, 0, rs0);
+        mListeners.notifyRemovedLocked(r0, 0, rs0);
+        verify(r0, never()).getSbn();
+
+        mListeners.notifyRemovedLocked(r1, 0, rs1);
+        mListeners.notifyRemovedLocked(r1, 0, rs1);
+        verify(r1, atLeast(2)).getSbn();
     }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index a545c83..beaa6e0 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -174,6 +174,7 @@
 import android.service.notification.ConversationChannelWrapper;
 import android.service.notification.NotificationListenerFilter;
 import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationRankingUpdate;
 import android.service.notification.NotificationStats;
 import android.service.notification.StatusBarNotification;
 import android.service.notification.ZenPolicy;
@@ -9781,10 +9782,10 @@
         mStrongAuthTracker.setGetStrongAuthForUserReturnValue(
                 STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
         mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId());
-        assertTrue(mStrongAuthTracker.isInLockDownMode());
-        mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0);
+        assertTrue(mStrongAuthTracker.isInLockDownMode(mContext.getUserId()));
+        mStrongAuthTracker.setGetStrongAuthForUserReturnValue(mContext.getUserId());
         mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId());
-        assertFalse(mStrongAuthTracker.isInLockDownMode());
+        assertFalse(mStrongAuthTracker.isInLockDownMode(mContext.getUserId()));
     }
 
     @Test
@@ -9800,8 +9801,8 @@
         // when entering the lockdown mode, cancel the 2 notifications.
         mStrongAuthTracker.setGetStrongAuthForUserReturnValue(
                 STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
-        mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId());
-        assertTrue(mStrongAuthTracker.isInLockDownMode());
+        mStrongAuthTracker.onStrongAuthRequiredChanged(0);
+        assertTrue(mStrongAuthTracker.isInLockDownMode(0));
 
         // the notifyRemovedLocked function is called twice due to REASON_CANCEL_ALL.
         ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
@@ -9810,10 +9811,46 @@
 
         // exit lockdown mode.
         mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0);
-        mStrongAuthTracker.onStrongAuthRequiredChanged(mContext.getUserId());
+        mStrongAuthTracker.onStrongAuthRequiredChanged(0);
+        assertFalse(mStrongAuthTracker.isInLockDownMode(0));
 
         // the notifyPostedLocked function is called twice.
-        verify(mListeners, times(2)).notifyPostedLocked(any(), any());
+        verify(mWorkerHandler, times(2)).postDelayed(any(Runnable.class), anyLong());
+        //verify(mListeners, times(2)).notifyPostedLocked(any(), any());
+    }
+
+    @Test
+    public void testMakeRankingUpdateLockedInLockDownMode() {
+        // post 2 notifications from a same package
+        NotificationRecord pkgA = new NotificationRecord(mContext,
+                generateSbn("a", 1000, 9, 0), mTestNotificationChannel);
+        mService.addNotification(pkgA);
+        NotificationRecord pkgB = new NotificationRecord(mContext,
+                generateSbn("a", 1000, 9, 1), mTestNotificationChannel);
+        mService.addNotification(pkgB);
+
+        mService.setIsVisibleToListenerReturnValue(true);
+        NotificationRankingUpdate nru = mService.makeRankingUpdateLocked(null);
+        assertEquals(2, nru.getRankingMap().getOrderedKeys().length);
+
+        // when only user 0 entering the lockdown mode, its notification will be suppressed.
+        mStrongAuthTracker.setGetStrongAuthForUserReturnValue(
+                STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+        mStrongAuthTracker.onStrongAuthRequiredChanged(0);
+        assertTrue(mStrongAuthTracker.isInLockDownMode(0));
+        assertFalse(mStrongAuthTracker.isInLockDownMode(1));
+
+        nru = mService.makeRankingUpdateLocked(null);
+        assertEquals(1, nru.getRankingMap().getOrderedKeys().length);
+
+        // User 0 exits lockdown mode. Its notification will be resumed.
+        mStrongAuthTracker.setGetStrongAuthForUserReturnValue(0);
+        mStrongAuthTracker.onStrongAuthRequiredChanged(0);
+        assertFalse(mStrongAuthTracker.isInLockDownMode(0));
+        assertFalse(mStrongAuthTracker.isInLockDownMode(1));
+
+        nru = mService.makeRankingUpdateLocked(null);
+        assertEquals(2, nru.getRankingMap().getOrderedKeys().length);
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index b49e5cb..8cf74fb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -19,10 +19,12 @@
 import android.companion.ICompanionDeviceManager;
 import android.content.ComponentName;
 import android.content.Context;
+import android.service.notification.StatusBarNotification;
 
 import androidx.annotation.Nullable;
 
 import com.android.internal.logging.InstanceIdSequence;
+import com.android.server.notification.ManagedServices.ManagedServiceInfo;
 
 import java.util.HashSet;
 import java.util.Set;
@@ -37,6 +39,9 @@
     @Nullable
     NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback;
 
+    @Nullable
+    Boolean mIsVisibleToListenerReturnValue = null;
+
     TestableNotificationManagerService(Context context, NotificationRecordLogger logger,
             InstanceIdSequence notificationInstanceIdSequence) {
         super(context, logger, notificationInstanceIdSequence);
@@ -119,6 +124,19 @@
         mShowReviewPermissionsNotification = setting;
     }
 
+    protected void setIsVisibleToListenerReturnValue(boolean value) {
+        mIsVisibleToListenerReturnValue = value;
+    }
+
+    @Override
+    boolean isVisibleToListener(StatusBarNotification sbn, int notificationType,
+            ManagedServiceInfo listener) {
+        if (mIsVisibleToListenerReturnValue != null) {
+            return mIsVisibleToListenerReturnValue;
+        }
+        return super.isVisibleToListener(sbn, notificationType, listener);
+    }
+
     public class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker {
         private int mGetStrongAuthForUserReturnValue = 0;
         StrongAuthTrackerFake(Context context) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
new file mode 100644
index 0000000..1246d1e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+import static com.android.server.wm.LetterboxConfigurationPersister.LETTERBOX_CONFIGURATION_FILENAME;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+import android.util.AtomicFile;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+@SmallTest
+@Presubmit
+public class LetterboxConfigurationPersisterTest {
+
+    private static final long TIMEOUT = 2000L; // 2 secs
+
+    private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+    private Context mContext;
+    private PersisterQueue mPersisterQueue;
+    private QueueState mQueueState;
+    private PersisterQueue.Listener mQueueListener;
+    private File mConfigFolder;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = getInstrumentation().getTargetContext();
+        mConfigFolder = mContext.getFilesDir();
+        mPersisterQueue = new PersisterQueue();
+        mQueueState = new QueueState();
+        mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(mContext,
+                () -> mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability),
+                () -> mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability),
+                mConfigFolder, mPersisterQueue, mQueueState);
+        mQueueListener = queueEmpty -> mQueueState.onItemAdded();
+        mPersisterQueue.addListener(mQueueListener);
+        mLetterboxConfigurationPersister.start();
+    }
+
+    public void tearDown() throws InterruptedException {
+        deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
+        waitForCompletion(mPersisterQueue);
+        mPersisterQueue.removeListener(mQueueListener);
+        stopPersisterSafe(mPersisterQueue);
+    }
+
+    @Test
+    public void test_whenStoreIsCreated_valuesAreDefaults() {
+        final int positionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int defaultPositionForHorizontalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+        Assert.assertEquals(defaultPositionForHorizontalReachability,
+                positionForHorizontalReachability);
+        final int positionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int defaultPositionForVerticalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability);
+        Assert.assertEquals(defaultPositionForVerticalReachability,
+                positionForVerticalReachability);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValues_valuesAreWritten() {
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(mPersisterQueue);
+        final int newPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() {
+        final PersisterQueue firstPersisterQueue = new PersisterQueue();
+        final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister(
+                mContext, () -> -1, () -> -1, mContext.getFilesDir(), firstPersisterQueue,
+                mQueueState);
+        firstPersister.start();
+        firstPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        firstPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(firstPersisterQueue);
+        stopPersisterSafe(firstPersisterQueue);
+        final PersisterQueue secondPersisterQueue = new PersisterQueue();
+        final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister(
+                mContext, () -> -1, () -> -1, mContext.getFilesDir(), secondPersisterQueue,
+                mQueueState);
+        secondPersister.start();
+        final int newPositionForHorizontalReachability =
+                secondPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                secondPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+        deleteConfiguration(secondPersister, secondPersisterQueue);
+        waitForCompletion(secondPersisterQueue);
+        stopPersisterSafe(secondPersisterQueue);
+    }
+
+    @Test
+    public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() {
+        mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(
+                LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT);
+        mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(
+                LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP);
+        waitForCompletion(mPersisterQueue);
+        final int newPositionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int newPositionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                newPositionForHorizontalReachability);
+        Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                newPositionForVerticalReachability);
+        deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue);
+        waitForCompletion(mPersisterQueue);
+        final int positionForHorizontalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability();
+        final int defaultPositionForHorizontalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForHorizontalReachability);
+        Assert.assertEquals(defaultPositionForHorizontalReachability,
+                positionForHorizontalReachability);
+        final int positionForVerticalReachability =
+                mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability();
+        final int defaultPositionForVerticalReachability =
+                mContext.getResources().getInteger(
+                        R.integer.config_letterboxDefaultPositionForVerticalReachability);
+        Assert.assertEquals(defaultPositionForVerticalReachability,
+                positionForVerticalReachability);
+    }
+
+    private void stopPersisterSafe(PersisterQueue persisterQueue) {
+        try {
+            persisterQueue.stopPersisting();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void waitForCompletion(PersisterQueue persisterQueue) {
+        final long endTime = System.currentTimeMillis() + TIMEOUT;
+        // The queue could be empty but the last item still processing and not completed. For this
+        // reason the completion happens when there are not more items to process and the last one
+        // has completed.
+        while (System.currentTimeMillis() < endTime && (!isQueueEmpty(persisterQueue)
+                || !hasLastItemCompleted())) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException ie) { /* Nope */}
+        }
+    }
+
+    private boolean isQueueEmpty(PersisterQueue persisterQueue) {
+        return persisterQueue.findLastItem(
+                writeQueueItem -> true, PersisterQueue.WriteQueueItem.class) != null;
+    }
+
+    private boolean hasLastItemCompleted() {
+        return mQueueState.isEmpty();
+    }
+
+    private void deleteConfiguration(LetterboxConfigurationPersister persister,
+            PersisterQueue persisterQueue) {
+        final AtomicFile fileToDelete = new AtomicFile(
+                new File(mConfigFolder, LETTERBOX_CONFIGURATION_FILENAME));
+        persisterQueue.addItem(
+                new DeleteFileCommand(fileToDelete, mQueueState.andThen(
+                        s -> persister.useDefaultValue())), true);
+    }
+
+    private static class DeleteFileCommand implements
+            PersisterQueue.WriteQueueItem<DeleteFileCommand> {
+
+        @NonNull
+        private final AtomicFile mFileToDelete;
+        @Nullable
+        private final Consumer<String> mOnComplete;
+
+        DeleteFileCommand(@NonNull AtomicFile fileToDelete, Consumer<String> onComplete) {
+            mFileToDelete = fileToDelete;
+            mOnComplete = onComplete;
+        }
+
+        @Override
+        public void process() {
+            mFileToDelete.delete();
+            if (mOnComplete != null) {
+                mOnComplete.accept("DeleteFileCommand");
+            }
+        }
+    }
+
+    // Contains the current length of the persister queue
+    private static class QueueState implements Consumer<String> {
+
+        // The current number of commands in the queue
+        @VisibleForTesting
+        private final AtomicInteger mCounter = new AtomicInteger(0);
+
+        @Override
+        public void accept(String s) {
+            mCounter.decrementAndGet();
+        }
+
+        void onItemAdded() {
+            mCounter.incrementAndGet();
+        }
+
+        boolean isEmpty() {
+            return mCounter.get() == 0;
+        }
+
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
new file mode 100644
index 0000000..c927f9e
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER;
+import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.function.Consumer;
+
+@SmallTest
+@Presubmit
+public class LetterboxConfigurationTest {
+
+    private LetterboxConfiguration mLetterboxConfiguration;
+    private LetterboxConfigurationPersister mLetterboxConfigurationPersister;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = getInstrumentation().getTargetContext();
+        mLetterboxConfigurationPersister = mock(LetterboxConfigurationPersister.class);
+        mLetterboxConfiguration = new LetterboxConfiguration(context,
+                mLetterboxConfigurationPersister);
+    }
+
+    @Test
+    public void test_whenReadingValues_storeIsInvoked() {
+        mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability();
+        verify(mLetterboxConfigurationPersister).getLetterboxPositionForHorizontalReachability();
+        mLetterboxConfiguration.getLetterboxPositionForVerticalReachability();
+        verify(mLetterboxConfigurationPersister).getLetterboxPositionForVerticalReachability();
+    }
+
+    @Test
+    public void test_whenSettingValues_updateConfigurationIsInvoked() {
+        mLetterboxConfiguration.movePositionForHorizontalReachabilityToNextRightStop();
+        verify(mLetterboxConfigurationPersister).setLetterboxPositionForHorizontalReachability(
+                anyInt());
+        mLetterboxConfiguration.movePositionForVerticalReachabilityToNextBottomStop();
+        verify(mLetterboxConfigurationPersister).setLetterboxPositionForVerticalReachability(
+                anyInt());
+    }
+
+    @Test
+    public void test_whenMovedHorizontally_updatePositionAccordingly() {
+        // Starting from center
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        // Starting from left
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        // Starting from right
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextRightStop);
+        assertForHorizontalMove(
+                /* from */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_RIGHT,
+                /* expected */ LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForHorizontalReachabilityToNextLeftStop);
+    }
+
+    @Test
+    public void test_whenMovedVertically_updatePositionAccordingly() {
+        // Starting from center
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        // Starting from top
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 1,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        // Starting from bottom
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_CENTER,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextTopStop);
+        assertForVerticalMove(
+                /* from */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expected */ LETTERBOX_VERTICAL_REACHABILITY_POSITION_BOTTOM,
+                /* expectedTime */ 2,
+                LetterboxConfiguration::movePositionForVerticalReachabilityToNextBottomStop);
+    }
+
+    private void assertForHorizontalMove(int from, int expected, int expectedTime,
+            Consumer<LetterboxConfiguration> move) {
+        // We are in the current position
+        when(mLetterboxConfiguration.getLetterboxPositionForHorizontalReachability())
+                .thenReturn(from);
+        move.accept(mLetterboxConfiguration);
+        verify(mLetterboxConfigurationPersister,
+                times(expectedTime)).setLetterboxPositionForHorizontalReachability(
+                expected);
+    }
+
+    private void assertForVerticalMove(int from, int expected, int expectedTime,
+            Consumer<LetterboxConfiguration> move) {
+        // We are in the current position
+        when(mLetterboxConfiguration.getLetterboxPositionForVerticalReachability())
+                .thenReturn(from);
+        move.accept(mLetterboxConfiguration);
+        verify(mLetterboxConfigurationPersister,
+                times(expectedTime)).setLetterboxPositionForVerticalReachability(
+                expected);
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 0b23359..4202f46 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -56,6 +56,7 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -91,6 +92,7 @@
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
 
 import java.util.List;
 
@@ -762,6 +764,50 @@
     }
 
     @Test
+    public void testOrganizerRemovedWithPendingEvents() {
+        final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
+                .setCreateParentTask()
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(mFragmentToken)
+                .build();
+        final TaskFragment tf1 = new TaskFragmentBuilder(mAtm)
+                .setCreateParentTask()
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(new Binder())
+                .build();
+        assertTrue(tf0.isOrganizedTaskFragment());
+        assertTrue(tf1.isOrganizedTaskFragment());
+        assertTrue(tf0.isAttached());
+        assertTrue(tf0.isAttached());
+
+        // Mock the behavior that remove TaskFragment can trigger event dispatch.
+        final Answer<Void> removeImmediately = invocation -> {
+            invocation.callRealMethod();
+            mController.dispatchPendingEvents();
+            return null;
+        };
+        doAnswer(removeImmediately).when(tf0).removeImmediately();
+        doAnswer(removeImmediately).when(tf1).removeImmediately();
+
+        // Add pending events.
+        mController.onTaskFragmentAppeared(mIOrganizer, tf0);
+        mController.onTaskFragmentAppeared(mIOrganizer, tf1);
+
+        // Remove organizer.
+        mController.unregisterOrganizer(mIOrganizer);
+        mController.dispatchPendingEvents();
+
+        // Nothing should happen after the organizer is removed.
+        verify(mOrganizer, never()).onTransactionReady(any());
+
+        // TaskFragments should be removed.
+        assertFalse(tf0.isOrganizedTaskFragment());
+        assertFalse(tf1.isOrganizedTaskFragment());
+        assertFalse(tf0.isAttached());
+        assertFalse(tf0.isAttached());
+    }
+
+    @Test
     public void testTaskFragmentInPip_startActivityInTaskFragment() {
         setupTaskFragmentInPip();
         final ActivityRecord activity = mTaskFragment.getTopMostActivity();
@@ -874,29 +920,87 @@
 
     @Test
     public void testDeferPendingTaskFragmentEventsOfInvisibleTask() {
-        // Task - TaskFragment - Activity.
         final Task task = createTask(mDisplayContent);
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
                 .setParentTask(task)
                 .setOrganizer(mOrganizer)
                 .setFragmentToken(mFragmentToken)
                 .build();
-
-        // Mock the task to invisible
         doReturn(false).when(task).shouldBeVisible(any());
 
-        // Sending events
-        taskFragment.mTaskFragmentAppearedSent = true;
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+
+        // Verify that events were not sent when the Task is in background.
+        clearInvocations(mOrganizer);
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
+        mController.onTaskFragmentParentInfoChanged(mIOrganizer, task);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-
-        // Verifies that event was not sent
         verify(mOrganizer, never()).onTransactionReady(any());
+
+        // Verify that the events were sent when the Task becomes visible.
+        doReturn(true).when(task).shouldBeVisible(any());
+        task.lastActiveTime++;
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+    }
+
+    @Test
+    public void testSendAllPendingTaskFragmentEventsWhenAnyTaskIsVisible() {
+        // Invisible Task.
+        final Task invisibleTask = createTask(mDisplayContent);
+        final TaskFragment invisibleTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(invisibleTask)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(mFragmentToken)
+                .build();
+        doReturn(false).when(invisibleTask).shouldBeVisible(any());
+
+        // Visible Task.
+        final IBinder fragmentToken = new Binder();
+        final Task visibleTask = createTask(mDisplayContent);
+        final TaskFragment visibleTaskFragment = new TaskFragmentBuilder(mAtm)
+                .setParentTask(visibleTask)
+                .setOrganizer(mOrganizer)
+                .setFragmentToken(fragmentToken)
+                .build();
+        doReturn(true).when(invisibleTask).shouldBeVisible(any());
+
+        // Sending events
+        invisibleTaskFragment.mTaskFragmentAppearedSent = true;
+        visibleTaskFragment.mTaskFragmentAppearedSent = true;
+        mController.onTaskFragmentInfoChanged(mIOrganizer, invisibleTaskFragment);
+        mController.onTaskFragmentInfoChanged(mIOrganizer, visibleTaskFragment);
+        mController.dispatchPendingEvents();
+
+        // Verify that both events are sent.
+        verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture());
+        final TaskFragmentTransaction transaction = mTransactionCaptor.getValue();
+        final List<TaskFragmentTransaction.Change> changes = transaction.getChanges();
+
+        // There should be two Task info changed with two TaskFragment info changed.
+        assertEquals(4, changes.size());
+        // Invisible Task info changed
+        assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(0).getType());
+        assertEquals(invisibleTask.mTaskId, changes.get(0).getTaskId());
+        // Invisible TaskFragment info changed
+        assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(1).getType());
+        assertEquals(invisibleTaskFragment.getFragmentToken(),
+                changes.get(1).getTaskFragmentToken());
+        // Visible Task info changed
+        assertEquals(TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED, changes.get(2).getType());
+        assertEquals(visibleTask.mTaskId, changes.get(2).getTaskId());
+        // Visible TaskFragment info changed
+        assertEquals(TYPE_TASK_FRAGMENT_INFO_CHANGED, changes.get(3).getType());
+        assertEquals(visibleTaskFragment.getFragmentToken(), changes.get(3).getTaskFragmentToken());
     }
 
     @Test
     public void testCanSendPendingTaskFragmentEventsAfterActivityResumed() {
-        // Task - TaskFragment - Activity.
         final Task task = createTask(mDisplayContent);
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
                 .setParentTask(task)
@@ -905,24 +1009,26 @@
                 .createActivityCount(1)
                 .build();
         final ActivityRecord activity = taskFragment.getTopMostActivity();
-
-        // Mock the task to invisible
         doReturn(false).when(task).shouldBeVisible(any());
         taskFragment.setResumedActivity(null, "test");
 
-        // Sending events
-        taskFragment.mTaskFragmentAppearedSent = true;
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
+        mController.dispatchPendingEvents();
+        verify(mOrganizer).onTransactionReady(any());
+
+        // Verify the info changed event is not sent because the Task is invisible
+        clearInvocations(mOrganizer);
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
-
-        // Verifies that event was not sent
         verify(mOrganizer, never()).onTransactionReady(any());
 
-        // Mock the task becomes visible, and activity resumed
+        // Mock the task becomes visible, and activity resumed. Verify the info changed event is
+        // sent.
         doReturn(true).when(task).shouldBeVisible(any());
         taskFragment.setResumedActivity(activity, "test");
-
-        // Verifies that event is sent.
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
     }
@@ -977,25 +1083,24 @@
         final ActivityRecord embeddedActivity = taskFragment.getTopNonFinishingActivity();
         // Add another activity in the Task so that it always contains a non-finishing activity.
         createActivityRecord(task);
-        assertTrue(task.shouldBeVisible(null));
+        doReturn(false).when(task).shouldBeVisible(any());
 
-        // Dispatch pending info changed event from creating the activity
-        taskFragment.mTaskFragmentAppearedSent = true;
-        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
+        // Dispatch the initial event in the Task to update the Task visibility to the organizer.
+        mController.onTaskFragmentAppeared(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
 
-        // Verify the info changed callback is not called when the task is invisible
+        // Verify the info changed event is not sent because the Task is invisible
         clearInvocations(mOrganizer);
-        doReturn(false).when(task).shouldBeVisible(any());
+        final Rect bounds = new Rect(0, 0, 500, 1000);
+        task.setBoundsUnchecked(bounds);
         mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer, never()).onTransactionReady(any());
 
-        // Finish the embedded activity, and verify the info changed callback is called because the
+        // Finish the embedded activity, and verify the info changed event is sent because the
         // TaskFragment is becoming empty.
         embeddedActivity.finishing = true;
-        mController.onTaskFragmentInfoChanged(mIOrganizer, taskFragment);
         mController.dispatchPendingEvents();
         verify(mOrganizer).onTransactionReady(any());
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 9fd0850..66e46a2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -59,7 +59,9 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import android.app.ActivityManager;
 import android.content.res.Configuration;
+import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.IBinder;
@@ -79,6 +81,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.graphics.ColorUtils;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -1384,6 +1388,50 @@
     }
 
     @Test
+    public void testChangeSetBackgroundColor() {
+        final Transition transition = createTestTransition(TRANSIT_CHANGE);
+        final ArrayMap<WindowContainer, Transition.ChangeInfo> changes = transition.mChanges;
+        final ArraySet<WindowContainer> participants = transition.mParticipants;
+
+        // Test background color for Activity and embedded TaskFragment.
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(
+                ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()));
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment embeddedTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord embeddedActivity = embeddedTf.getTopMostActivity();
+        final ActivityRecord nonEmbeddedActivity = createActivityRecord(task);
+        final ActivityManager.TaskDescription taskDescription =
+                new ActivityManager.TaskDescription.Builder()
+                        .setBackgroundColor(Color.YELLOW)
+                        .build();
+        task.setTaskDescription(taskDescription);
+
+        // Start states:
+        embeddedActivity.mVisibleRequested = true;
+        nonEmbeddedActivity.mVisibleRequested = false;
+        changes.put(embeddedTf, new Transition.ChangeInfo(embeddedTf));
+        changes.put(nonEmbeddedActivity, new Transition.ChangeInfo(nonEmbeddedActivity));
+        // End states:
+        embeddedActivity.mVisibleRequested = false;
+        nonEmbeddedActivity.mVisibleRequested = true;
+
+        participants.add(embeddedTf);
+        participants.add(nonEmbeddedActivity);
+        final ArrayList<WindowContainer> targets = Transition.calculateTargets(
+                participants, changes);
+        final TransitionInfo info = Transition.calculateTransitionInfo(transition.mType,
+                0 /* flags */, targets, changes, mMockT);
+
+        // Background color should be set on both Activity and embedded TaskFragment.
+        final int expectedBackgroundColor = ColorUtils.setAlphaComponent(
+                taskDescription.getBackgroundColor(), 255);
+        assertEquals(2, info.getChanges().size());
+        assertEquals(expectedBackgroundColor, info.getChanges().get(0).getBackgroundColor());
+        assertEquals(expectedBackgroundColor, info.getChanges().get(1).getBackgroundColor());
+    }
+
+    @Test
     public void testTransitionVisibleChange() {
         registerTestTransitionPlayer();
         final ActivityRecord app = createActivityRecord(mDisplayContent);