Merge "Make new headers flag true by default" 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/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/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/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index 44a467f..cbd544c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -18,9 +18,21 @@
 
 import com.android.wm.shell.common.annotations.ExternalThread;
 
+import java.util.concurrent.Executor;
+
 /**
  * Interface to interact with desktop mode feature in shell.
  */
 @ExternalThread
 public interface DesktopMode {
+
+    /**
+     * Adds a listener to find out about changes in the visibility of freeform tasks.
+     *
+     * @param listener the listener to add.
+     * @param callbackExecutor the executor to call the listener on.
+     */
+    void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+            Executor callbackExecutor);
+
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
index b96facf..34ff6d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 
 import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
@@ -60,6 +61,7 @@
 
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.concurrent.Executor;
 
 /**
  * Handles windowing changes when desktop mode system setting changes
@@ -132,6 +134,17 @@
         return new IDesktopModeImpl(this);
     }
 
+    /**
+     * Adds a listener to find out about changes in the visibility of freeform tasks.
+     *
+     * @param listener the listener to add.
+     * @param callbackExecutor the executor to call the listener on.
+     */
+    public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+            Executor callbackExecutor) {
+        mDesktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor);
+    }
+
     @VisibleForTesting
     void updateDesktopModeActive(boolean active) {
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "updateDesktopModeActive: active=%s", active);
@@ -181,7 +194,18 @@
     /**
      * Show apps on desktop
      */
-    WindowContainerTransaction showDesktopApps() {
+    void showDesktopApps() {
+        WindowContainerTransaction wct = bringDesktopAppsToFront();
+
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            mTransitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */);
+        } else {
+            mShellTaskOrganizer.applyTransaction(wct);
+        }
+    }
+
+    @NonNull
+    private WindowContainerTransaction bringDesktopAppsToFront() {
         ArraySet<Integer> activeTasks = mDesktopModeTaskRepository.getActiveTasks();
         ProtoLog.d(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront: tasks=%s", activeTasks.size());
         ArrayList<RunningTaskInfo> taskInfos = new ArrayList<>();
@@ -197,11 +221,6 @@
         for (RunningTaskInfo task : taskInfos) {
             wct.reorder(task.token, true);
         }
-
-        if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
-            mShellTaskOrganizer.applyTransaction(wct);
-        }
-
         return wct;
     }
 
@@ -237,17 +256,29 @@
     @Override
     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
             @NonNull TransitionRequestInfo request) {
-
-        // Only do anything if we are in desktop mode and opening a task/app
-        if (!DesktopModeStatus.isActive(mContext) || request.getType() != TRANSIT_OPEN) {
+        // Only do anything if we are in desktop mode and opening a task/app in freeform
+        if (!DesktopModeStatus.isActive(mContext)) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                    "skip shell transition request: desktop mode not active");
             return null;
         }
+        if (request.getType() != TRANSIT_OPEN) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+                    "skip shell transition request: only supports TRANSIT_OPEN");
+            return null;
+        }
+        if (request.getTriggerTask() == null
+                || request.getTriggerTask().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+            ProtoLog.d(WM_SHELL_DESKTOP_MODE, "skip shell transition request: not freeform task");
+            return null;
+        }
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "handle shell transition request: %s", request);
 
         WindowContainerTransaction wct = mTransitions.dispatchRequest(transition, request, this);
         if (wct == null) {
             wct = new WindowContainerTransaction();
         }
-        wct.merge(showDesktopApps(), true /* transfer */);
+        wct.merge(bringDesktopAppsToFront(), true /* transfer */);
         wct.reorder(request.getTriggerTask().token, true /* onTop */);
 
         return wct;
@@ -293,7 +324,14 @@
      */
     @ExternalThread
     private final class DesktopModeImpl implements DesktopMode {
-        // Do nothing
+
+        @Override
+        public void addListener(DesktopModeTaskRepository.VisibleTasksListener listener,
+                Executor callbackExecutor) {
+            mMainExecutor.execute(() -> {
+                DesktopModeController.this.addListener(listener, callbackExecutor);
+            });
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 988601c..c91d54a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -16,7 +16,9 @@
 
 package com.android.wm.shell.desktopmode
 
+import android.util.ArrayMap
 import android.util.ArraySet
+import java.util.concurrent.Executor
 
 /**
  * Keeps track of task data related to desktop mode.
@@ -30,20 +32,39 @@
      * Task gets removed from this list when it vanishes. Or when desktop mode is turned off.
      */
     private val activeTasks = ArraySet<Int>()
-    private val listeners = ArraySet<Listener>()
+    private val visibleTasks = ArraySet<Int>()
+    private val activeTasksListeners = ArraySet<ActiveTasksListener>()
+    // Track visible tasks separately because a task may be part of the desktop but not visible.
+    private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
 
     /**
-     * Add a [Listener] to be notified of updates to the repository.
+     * Add a [ActiveTasksListener] to be notified of updates to active tasks in the repository.
      */
-    fun addListener(listener: Listener) {
-        listeners.add(listener)
+    fun addActiveTaskListener(activeTasksListener: ActiveTasksListener) {
+        activeTasksListeners.add(activeTasksListener)
     }
 
     /**
-     * Remove a previously registered [Listener]
+     * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not.
      */
-    fun removeListener(listener: Listener) {
-        listeners.remove(listener)
+    fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
+        visibleTasksListeners.put(visibleTasksListener, executor)
+        executor.execute(
+                Runnable { visibleTasksListener.onVisibilityChanged(visibleTasks.size > 0) })
+    }
+
+    /**
+     * Remove a previously registered [ActiveTasksListener]
+     */
+    fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
+        activeTasksListeners.remove(activeTasksListener)
+    }
+
+    /**
+     * Remove a previously registered [VisibleTasksListener]
+     */
+    fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) {
+        visibleTasksListeners.remove(visibleTasksListener)
     }
 
     /**
@@ -52,7 +73,7 @@
     fun addActiveTask(taskId: Int) {
         val added = activeTasks.add(taskId)
         if (added) {
-            listeners.onEach { it.onActiveTasksChanged() }
+            activeTasksListeners.onEach { it.onActiveTasksChanged() }
         }
     }
 
@@ -62,7 +83,7 @@
     fun removeActiveTask(taskId: Int) {
         val removed = activeTasks.remove(taskId)
         if (removed) {
-            listeners.onEach { it.onActiveTasksChanged() }
+            activeTasksListeners.onEach { it.onActiveTasksChanged() }
         }
     }
 
@@ -81,9 +102,43 @@
     }
 
     /**
-     * Defines interface for classes that can listen to changes in repository state.
+     * Updates whether a freeform task with this id is visible or not and notifies listeners.
      */
-    interface Listener {
-        fun onActiveTasksChanged()
+    fun updateVisibleFreeformTasks(taskId: Int, visible: Boolean) {
+        val prevCount: Int = visibleTasks.size
+        if (visible) {
+            visibleTasks.add(taskId)
+        } else {
+            visibleTasks.remove(taskId)
+        }
+        if (prevCount == 0 && visibleTasks.size == 1 ||
+                prevCount > 0 && visibleTasks.size == 0) {
+            for ((listener, executor) in visibleTasksListeners) {
+                executor.execute(
+                        Runnable { listener.onVisibilityChanged(visibleTasks.size > 0) })
+            }
+        }
+    }
+
+    /**
+     * Defines interface for classes that can listen to changes for active tasks in desktop mode.
+     */
+    interface ActiveTasksListener {
+        /**
+         * Called when the active tasks change in desktop mode.
+         */
+        @JvmDefault
+        fun onActiveTasksChanged() {}
+    }
+
+    /**
+     * Defines interface for classes that can listen to changes for visible tasks in desktop mode.
+     */
+    interface VisibleTasksListener {
+        /**
+         * Called when the desktop starts or stops showing freeform tasks.
+         */
+        @JvmDefault
+        fun onVisibilityChanged(hasVisibleFreeformTasks: Boolean) {}
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index f82a346..eaa7158 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -90,6 +90,8 @@
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                     "Adding active freeform task: #%d", taskInfo.taskId);
             mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
+            mDesktopModeTaskRepository.ifPresent(
+                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, true));
         }
     }
 
@@ -103,6 +105,8 @@
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                     "Removing active freeform task: #%d", taskInfo.taskId);
             mDesktopModeTaskRepository.ifPresent(it -> it.removeActiveTask(taskInfo.taskId));
+            mDesktopModeTaskRepository.ifPresent(
+                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, false));
         }
 
         if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -124,6 +128,8 @@
                         "Adding active freeform task: #%d", taskInfo.taskId);
                 mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTask(taskInfo.taskId));
             }
+            mDesktopModeTaskRepository.ifPresent(
+                    it -> it.updateVisibleFreeformTasks(taskInfo.taskId, taskInfo.isVisible));
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 08f3db6..f9172ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -68,7 +68,7 @@
  * Manages the recent task list from the system, caching it as necessary.
  */
 public class RecentTasksController implements TaskStackListenerCallback,
-        RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.Listener {
+        RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener {
     private static final String TAG = RecentTasksController.class.getSimpleName();
 
     private final Context mContext;
@@ -147,7 +147,7 @@
                 this::createExternalInterface, this);
         mShellCommandHandler.addDumpCallback(this::dump, this);
         mTaskStackListener.addListener(this);
-        mDesktopModeTaskRepository.ifPresent(it -> it.addListener(this));
+        mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
     }
 
     /**
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
index c850a3b..79b520c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeControllerTest.java
@@ -20,6 +20,8 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
@@ -35,10 +37,12 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
 import android.testing.AndroidTestingRunner;
 import android.window.DisplayAreaInfo;
+import android.window.TransitionRequestInfo;
 import android.window.WindowContainerToken;
 import android.window.WindowContainerTransaction;
 import android.window.WindowContainerTransaction.Change;
@@ -243,6 +247,44 @@
         assertThat(op2.getContainer()).isEqualTo(token2.binder());
     }
 
+    @Test
+    public void testHandleTransitionRequest_desktopModeNotActive_returnsNull() {
+        when(DesktopModeStatus.isActive(any())).thenReturn(false);
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_notTransitOpen_returnsNull() {
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_TO_FRONT, null /* trigger */, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_notFreeform_returnsNull() {
+        ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo();
+        trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        WindowContainerTransaction wct = mController.handleRequest(
+                new Binder(),
+                new TransitionRequestInfo(TRANSIT_TO_FRONT, trigger, null /* remote */));
+        assertThat(wct).isNull();
+    }
+
+    @Test
+    public void testHandleTransitionRequest_returnsWct() {
+        ActivityManager.RunningTaskInfo trigger = new ActivityManager.RunningTaskInfo();
+        trigger.token = new MockToken().mToken;
+        trigger.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        WindowContainerTransaction wct = mController.handleRequest(
+                mock(IBinder.class),
+                new TransitionRequestInfo(TRANSIT_OPEN, trigger, null /* remote */));
+        assertThat(wct).isNotNull();
+    }
+
     private static class MockToken {
         private final WindowContainerToken mToken;
         private final IBinder mBinder;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 9b28d11..aaa5c8a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -19,6 +19,7 @@
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -38,7 +39,7 @@
     @Test
     fun addActiveTask_listenerNotifiedAndTaskIsActive() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         assertThat(listener.activeTaskChangedCalls).isEqualTo(1)
@@ -48,7 +49,7 @@
     @Test
     fun addActiveTask_sameTaskDoesNotNotify() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.addActiveTask(1)
@@ -58,7 +59,7 @@
     @Test
     fun addActiveTask_multipleTasksAddedNotifiesForEach() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.addActiveTask(2)
@@ -68,7 +69,7 @@
     @Test
     fun removeActiveTask_listenerNotifiedAndTaskNotActive() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
 
         repo.addActiveTask(1)
         repo.removeActiveTask(1)
@@ -80,7 +81,7 @@
     @Test
     fun removeActiveTask_removeNotExistingTaskDoesNotNotify() {
         val listener = TestListener()
-        repo.addListener(listener)
+        repo.addActiveTaskListener(listener)
         repo.removeActiveTask(99)
         assertThat(listener.activeTaskChangedCalls).isEqualTo(0)
     }
@@ -90,10 +91,69 @@
         assertThat(repo.isActiveTask(99)).isFalse()
     }
 
-    class TestListener : DesktopModeTaskRepository.Listener {
+    @Test
+    fun addListener_notifiesVisibleFreeformTask() {
+        repo.updateVisibleFreeformTasks(1, true)
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(1)
+    }
+
+    @Test
+    fun updateVisibleFreeformTasks_addVisibleTasksNotifiesListener() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        repo.updateVisibleFreeformTasks(1, true)
+        repo.updateVisibleFreeformTasks(2, true)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        // Equal to 2 because adding the listener notifies the current state
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+    }
+
+    @Test
+    fun updateVisibleFreeformTasks_removeVisibleTasksNotifiesListener() {
+        val listener = TestVisibilityListener()
+        val executor = TestShellExecutor()
+        repo.addVisibleTasksListener(listener, executor)
+        repo.updateVisibleFreeformTasks(1, true)
+        repo.updateVisibleFreeformTasks(2, true)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isTrue()
+        repo.updateVisibleFreeformTasks(1, false)
+        executor.flushAll()
+
+        // Equal to 2 because adding the listener notifies the current state
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(2)
+
+        repo.updateVisibleFreeformTasks(2, false)
+        executor.flushAll()
+
+        assertThat(listener.hasVisibleFreeformTasks).isFalse()
+        assertThat(listener.visibleFreeformTaskChangedCalls).isEqualTo(3)
+    }
+
+    class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
         var activeTaskChangedCalls = 0
         override fun onActiveTasksChanged() {
             activeTaskChangedCalls++
         }
     }
+
+    class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener {
+        var hasVisibleFreeformTasks = false
+        var visibleFreeformTaskChangedCalls = 0
+
+        override fun onVisibilityChanged(hasVisibleTasks: Boolean) {
+            hasVisibleFreeformTasks = hasVisibleTasks
+            visibleFreeformTaskChangedCalls++
+        }
+    }
 }
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/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index dabb43b..89f5c2c 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -18,6 +18,7 @@
 import android.graphics.drawable.Drawable
 import android.view.View
 import com.android.systemui.plugins.annotations.ProvidesInterface
+import com.android.systemui.plugins.log.LogBuffer
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
@@ -70,6 +71,9 @@
 
     /** Optional method for dumping debug information */
     fun dump(pw: PrintWriter) { }
+
+    /** Optional method for debug logging */
+    fun setLogBuffer(logBuffer: LogBuffer) { }
 }
 
 /** Interface for a specific clock face version rendered by the clock */
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
index 16a1d94..647abee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_status_view.xml
@@ -27,6 +27,7 @@
     systemui:layout_constraintEnd_toEndOf="parent"
     systemui:layout_constraintTop_toTopOf="parent"
     android:layout_marginHorizontal="@dimen/status_view_margin_horizontal"
+    android:clipChildren="false"
     android:layout_width="0dp"
     android:layout_height="wrap_content">
     <LinearLayout
diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml
index b1d3375..a25ab51 100644
--- a/packages/SystemUI/res-keyguard/values/config.xml
+++ b/packages/SystemUI/res-keyguard/values/config.xml
@@ -28,11 +28,6 @@
     <!-- Will display the bouncer on one side of the display, and the current user icon and
          user switcher on the other side -->
     <bool name="config_enableBouncerUserSwitcher">false</bool>
-    <!-- Whether to show the face scanning animation on devices with face auth supported.
-         The face scanning animation renders in a SW layer in ScreenDecorations.
-         Enabling this will also render the camera protection in the SW layer
-         (instead of HW, if relevant)."=-->
-    <bool name="config_enableFaceScanningAnimation">true</bool>
     <!-- Time to be considered a consecutive fingerprint failure in ms -->
     <integer name="fp_consecutive_failure_time_ms">3500</integer>
 </resources>
diff --git a/packages/SystemUI/res/layout/status_bar_expanded.xml b/packages/SystemUI/res/layout/status_bar_expanded.xml
index f0e49d5..92ef3f8 100644
--- a/packages/SystemUI/res/layout/status_bar_expanded.xml
+++ b/packages/SystemUI/res/layout/status_bar_expanded.xml
@@ -32,41 +32,8 @@
         android:layout_height="match_parent"
         android:layout_width="match_parent" />
 
-    <include
-        layout="@layout/keyguard_bottom_area"
-        android:visibility="gone" />
-
-    <ViewStub
-        android:id="@+id/keyguard_user_switcher_stub"
-        android:layout="@layout/keyguard_user_switcher"
-        android:layout_height="match_parent"
-        android:layout_width="match_parent" />
-
     <include layout="@layout/status_bar_expanded_plugin_frame"/>
 
-    <include layout="@layout/dock_info_bottom_area_overlay" />
-
-    <com.android.keyguard.LockIconView
-        android:id="@+id/lock_icon_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content">
-        <!-- Background protection -->
-        <ImageView
-            android:id="@+id/lock_icon_bg"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:background="@drawable/fingerprint_bg"
-            android:visibility="invisible"/>
-
-        <ImageView
-            android:id="@+id/lock_icon"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_gravity="center"
-            android:scaleType="centerCrop"/>
-
-    </com.android.keyguard.LockIconView>
-
     <com.android.systemui.shade.NotificationsQuickSettingsContainer
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -145,6 +112,39 @@
         />
     </com.android.systemui.shade.NotificationsQuickSettingsContainer>
 
+    <include
+        layout="@layout/keyguard_bottom_area"
+        android:visibility="gone" />
+
+    <ViewStub
+        android:id="@+id/keyguard_user_switcher_stub"
+        android:layout="@layout/keyguard_user_switcher"
+        android:layout_height="match_parent"
+        android:layout_width="match_parent" />
+
+    <include layout="@layout/dock_info_bottom_area_overlay" />
+
+    <com.android.keyguard.LockIconView
+        android:id="@+id/lock_icon_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content">
+        <!-- Background protection -->
+        <ImageView
+            android:id="@+id/lock_icon_bg"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/fingerprint_bg"
+            android:visibility="invisible"/>
+
+        <ImageView
+            android:id="@+id/lock_icon"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_gravity="center"
+            android:scaleType="centerCrop"/>
+
+    </com.android.keyguard.LockIconView>
+
     <FrameLayout
         android:id="@+id/preview_container"
         android:layout_width="match_parent"
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index f59a320..91fd6a6 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -62,7 +62,6 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
-    java_version: "1.8",
     min_sdk_version: "current",
     plugins: ["dagger2-compiler"],
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 1cf7c50..236aa66 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -33,6 +33,8 @@
 import com.android.systemui.animation.GlyphCallback
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.TextAnimator
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
 import com.android.systemui.shared.R
 import java.io.PrintWriter
 import java.util.Calendar
@@ -52,14 +54,8 @@
     defStyleAttr: Int = 0,
     defStyleRes: Int = 0
 ) : TextView(context, attrs, defStyleAttr, defStyleRes) {
-
-    private var lastMeasureCall: CharSequence? = null
-    private var lastDraw: CharSequence? = null
-    private var lastTextUpdate: CharSequence? = null
-    private var lastOnTextChanged: CharSequence? = null
-    private var lastInvalidate: CharSequence? = null
-    private var lastTimeZoneChange: CharSequence? = null
-    private var lastAnimationCall: CharSequence? = null
+    var tag: String = "UnnamedClockView"
+    var logBuffer: LogBuffer? = null
 
     private val time = Calendar.getInstance()
 
@@ -136,6 +132,7 @@
 
     override fun onAttachedToWindow() {
         super.onAttachedToWindow()
+        logBuffer?.log(tag, DEBUG, "onAttachedToWindow")
         refreshFormat()
     }
 
@@ -151,27 +148,39 @@
         time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
         contentDescription = DateFormat.format(descFormat, time)
         val formattedText = DateFormat.format(format, time)
+        logBuffer?.log(tag, DEBUG,
+                { str1 = formattedText?.toString() },
+                { "refreshTime: new formattedText=$str1" }
+        )
         // Setting text actually triggers a layout pass (because the text view is set to
         // wrap_content width and TextView always relayouts for this). Avoid needless
         // relayout if the text didn't actually change.
         if (!TextUtils.equals(text, formattedText)) {
             text = formattedText
+            logBuffer?.log(tag, DEBUG,
+                    { str1 = formattedText?.toString() },
+                    { "refreshTime: done setting new time text to: $str1" }
+            )
             // Because the TextLayout may mutate under the hood as a result of the new text, we
             // notify the TextAnimator that it may have changed and request a measure/layout. A
             // crash will occur on the next invocation of setTextStyle if the layout is mutated
             // without being notified TextInterpolator being notified.
             if (layout != null) {
                 textAnimator?.updateLayout(layout)
+                logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout")
             }
             requestLayout()
-            lastTextUpdate = getTimestamp()
+            logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout")
         }
     }
 
     fun onTimeZoneChanged(timeZone: TimeZone?) {
         time.timeZone = timeZone
         refreshFormat()
-        lastTimeZoneChange = "${getTimestamp()} timeZone=${time.timeZone}"
+        logBuffer?.log(tag, DEBUG,
+                { str1 = timeZone?.toString() },
+                { "onTimeZoneChanged newTimeZone=$str1" }
+        )
     }
 
     @SuppressLint("DrawAllocation")
@@ -185,27 +194,24 @@
         } else {
             animator.updateLayout(layout)
         }
-        lastMeasureCall = getTimestamp()
+        logBuffer?.log(tag, DEBUG, "onMeasure")
     }
 
     override fun onDraw(canvas: Canvas) {
-        lastDraw = getTimestamp()
         // Use textAnimator to render text if animation is enabled.
         // Otherwise default to using standard draw functions.
         if (isAnimationEnabled) {
+            // intentionally doesn't call super.onDraw here or else the text will be rendered twice
             textAnimator?.draw(canvas)
         } else {
             super.onDraw(canvas)
         }
+        logBuffer?.log(tag, DEBUG, "onDraw lastDraw")
     }
 
     override fun invalidate() {
         super.invalidate()
-        lastInvalidate = getTimestamp()
-    }
-
-    private fun getTimestamp(): CharSequence {
-        return "${DateFormat.format("HH:mm:ss", System.currentTimeMillis())} text=$text"
+        logBuffer?.log(tag, DEBUG, "invalidate")
     }
 
     override fun onTextChanged(
@@ -215,7 +221,10 @@
             lengthAfter: Int
     ) {
         super.onTextChanged(text, start, lengthBefore, lengthAfter)
-        lastOnTextChanged = "${getTimestamp()}"
+        logBuffer?.log(tag, DEBUG,
+                { str1 = text.toString() },
+                { "onTextChanged text=$str1" }
+        )
     }
 
     fun setLineSpacingScale(scale: Float) {
@@ -229,7 +238,7 @@
     }
 
     fun animateAppearOnLockscreen() {
-        lastAnimationCall = "${getTimestamp()} call=animateAppearOnLockscreen"
+        logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen")
         setTextStyle(
             weight = dozingWeight,
             textSize = -1f,
@@ -254,7 +263,7 @@
         if (isAnimationEnabled && textAnimator == null) {
             return
         }
-        lastAnimationCall = "${getTimestamp()} call=animateFoldAppear"
+        logBuffer?.log(tag, DEBUG, "animateFoldAppear")
         setTextStyle(
             weight = lockScreenWeightInternal,
             textSize = -1f,
@@ -281,7 +290,7 @@
             // Skip charge animation if dozing animation is already playing.
             return
         }
-        lastAnimationCall = "${getTimestamp()} call=animateCharge"
+        logBuffer?.log(tag, DEBUG, "animateCharge")
         val startAnimPhase2 = Runnable {
             setTextStyle(
                 weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -305,7 +314,7 @@
     }
 
     fun animateDoze(isDozing: Boolean, animate: Boolean) {
-        lastAnimationCall = "${getTimestamp()} call=animateDoze"
+        logBuffer?.log(tag, DEBUG, "animateDoze")
         setTextStyle(
             weight = if (isDozing) dozingWeight else lockScreenWeight,
             textSize = -1f,
@@ -423,9 +432,12 @@
             isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
             else -> DOUBLE_LINE_FORMAT_12_HOUR
         }
+        logBuffer?.log(tag, DEBUG,
+                { str1 = format?.toString() },
+                { "refreshFormat format=$str1" }
+        )
 
         descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
-
         refreshTime()
     }
 
@@ -434,15 +446,8 @@
         pw.println("    measuredWidth=$measuredWidth")
         pw.println("    measuredHeight=$measuredHeight")
         pw.println("    singleLineInternal=$isSingleLineInternal")
-        pw.println("    lastTextUpdate=$lastTextUpdate")
-        pw.println("    lastOnTextChanged=$lastOnTextChanged")
-        pw.println("    lastInvalidate=$lastInvalidate")
-        pw.println("    lastMeasureCall=$lastMeasureCall")
-        pw.println("    lastDraw=$lastDraw")
-        pw.println("    lastTimeZoneChange=$lastTimeZoneChange")
         pw.println("    currText=$text")
         pw.println("    currTimeContextDesc=$contentDescription")
-        pw.println("    lastAnimationCall=$lastAnimationCall")
         pw.println("    dozingWeightInternal=$dozingWeightInternal")
         pw.println("    lockScreenWeightInternal=$lockScreenWeightInternal")
         pw.println("    dozingColor=$dozingColor")
@@ -591,6 +596,7 @@
             if (!clockView12Skel.contains("a")) {
                 sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
             }
+
             sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
             sCacheKey = key
         }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 6fd61da..da1d233 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.R
 import java.io.PrintWriter
 import java.util.Locale
@@ -86,9 +87,17 @@
         events.onTimeTick()
     }
 
+    override fun setLogBuffer(logBuffer: LogBuffer) {
+        smallClock.view.tag = "smallClockView"
+        largeClock.view.tag = "largeClockView"
+        smallClock.view.logBuffer = logBuffer
+        largeClock.view.logBuffer = logBuffer
+    }
+
     open inner class DefaultClockFaceController(
         override val view: AnimatableClockView,
     ) : ClockFaceController {
+
         // MAGENTA is a placeholder, and will be assigned correctly in initialize
         private var currentColor = Color.MAGENTA
         private var isRegionDark = false
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
index ce337bb..fd41cb06 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -194,12 +194,8 @@
                             Rect homeContentInsets, Rect minimizedHomeBounds) {
                         final RecentsAnimationControllerCompat controllerCompat =
                                 new RecentsAnimationControllerCompat(controller);
-                        final RemoteAnimationTargetCompat[] appsCompat =
-                                RemoteAnimationTargetCompat.wrap(apps);
-                        final RemoteAnimationTargetCompat[] wallpapersCompat =
-                                RemoteAnimationTargetCompat.wrap(wallpapers);
-                        animationHandler.onAnimationStart(controllerCompat, appsCompat,
-                                wallpapersCompat, homeContentInsets, minimizedHomeBounds);
+                        animationHandler.onAnimationStart(controllerCompat, apps,
+                                wallpapers, homeContentInsets, minimizedHomeBounds);
                     }
 
                     @Override
@@ -210,12 +206,7 @@
 
                     @Override
                     public void onTasksAppeared(RemoteAnimationTarget[] apps) {
-                        final RemoteAnimationTargetCompat[] compats =
-                                new RemoteAnimationTargetCompat[apps.length];
-                        for (int i = 0; i < apps.length; ++i) {
-                            compats[i] = new RemoteAnimationTargetCompat(apps[i]);
-                        }
-                        animationHandler.onTasksAppeared(compats);
+                        animationHandler.onTasksAppeared(apps);
                     }
                 };
             }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
index f2742b7..766266d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java
@@ -110,6 +110,9 @@
     public static final int SYSUI_STATE_IMMERSIVE_MODE = 1 << 24;
     // The voice interaction session window is showing
     public static final int SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING = 1 << 25;
+    // Freeform windows are showing in desktop mode
+    public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26;
+
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({SYSUI_STATE_SCREEN_PINNING,
@@ -137,7 +140,8 @@
             SYSUI_STATE_BACK_DISABLED,
             SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED,
             SYSUI_STATE_IMMERSIVE_MODE,
-            SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING
+            SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING,
+            SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE
     })
     public @interface SystemUiStateFlags {}
 
@@ -173,6 +177,8 @@
                 ? "bubbles_mange_menu_expanded" : "");
         str.add((flags & SYSUI_STATE_IMMERSIVE_MODE) != 0 ? "immersive_mode" : "");
         str.add((flags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 ? "vis_win_showing" : "");
+        str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0
+                ? "freeform_active_in_desktop_mode" : "");
         return str.toString();
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
index 5cca4a6..8bddf21 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java
@@ -17,6 +17,7 @@
 package com.android.systemui.shared.system;
 
 import android.graphics.Rect;
+import android.view.RemoteAnimationTarget;
 
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
@@ -27,7 +28,7 @@
      * Called when the animation into Recents can start. This call is made on the binder thread.
      */
     void onAnimationStart(RecentsAnimationControllerCompat controller,
-            RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers,
+            RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
             Rect homeContentInsets, Rect minimizedHomeBounds);
 
     /**
@@ -39,7 +40,7 @@
      * Called when the task of an activity that has been started while the recents animation
      * was running becomes ready for control.
      */
-    void onTasksAppeared(RemoteAnimationTargetCompat[] app);
+    void onTasksAppeared(RemoteAnimationTarget[] app);
 
     /**
      * Called to request that the current task tile be switched out for a screenshot (if not
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
index 09cf7c5..37e706a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -83,12 +83,6 @@
                     RemoteAnimationTarget[] wallpapers,
                     RemoteAnimationTarget[] nonApps,
                     final IRemoteAnimationFinishedCallback finishedCallback) {
-                final RemoteAnimationTargetCompat[] appsCompat =
-                        RemoteAnimationTargetCompat.wrap(apps);
-                final RemoteAnimationTargetCompat[] wallpapersCompat =
-                        RemoteAnimationTargetCompat.wrap(wallpapers);
-                final RemoteAnimationTargetCompat[] nonAppsCompat =
-                        RemoteAnimationTargetCompat.wrap(nonApps);
                 final Runnable animationFinishedCallback = new Runnable() {
                     @Override
                     public void run() {
@@ -100,8 +94,8 @@
                         }
                     }
                 };
-                remoteAnimationAdapter.onAnimationStart(transit, appsCompat, wallpapersCompat,
-                        nonAppsCompat, animationFinishedCallback);
+                remoteAnimationAdapter.onAnimationStart(transit, apps, wallpapers,
+                        nonApps, animationFinishedCallback);
             }
 
             @Override
@@ -121,12 +115,12 @@
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishCallback) {
                 final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
-                final RemoteAnimationTargetCompat[] appsCompat =
+                final RemoteAnimationTarget[] apps =
                         RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
-                final RemoteAnimationTargetCompat[] wallpapersCompat =
+                final RemoteAnimationTarget[] wallpapers =
                         RemoteAnimationTargetCompat.wrapNonApps(
                                 info, true /* wallpapers */, t, leashMap);
-                final RemoteAnimationTargetCompat[] nonAppsCompat =
+                final RemoteAnimationTarget[] nonApps =
                         RemoteAnimationTargetCompat.wrapNonApps(
                                 info, false /* wallpapers */, t, leashMap);
 
@@ -189,9 +183,9 @@
                         }
                     }
                     // Make wallpaper visible immediately since launcher apparently won't do this.
-                    for (int i = wallpapersCompat.length - 1; i >= 0; --i) {
-                        t.show(wallpapersCompat[i].leash);
-                        t.setAlpha(wallpapersCompat[i].leash, 1.f);
+                    for (int i = wallpapers.length - 1; i >= 0; --i) {
+                        t.show(wallpapers[i].leash);
+                        t.setAlpha(wallpapers[i].leash, 1.f);
                     }
                 } else {
                     if (launcherTask != null) {
@@ -237,7 +231,7 @@
                 }
                 // TODO(bc-unlcok): Pass correct transit type.
                 remoteAnimationAdapter.onAnimationStart(TRANSIT_OLD_NONE,
-                        appsCompat, wallpapersCompat, nonAppsCompat, () -> {
+                        apps, wallpapers, nonApps, () -> {
                             synchronized (mFinishRunnables) {
                                 if (mFinishRunnables.remove(token) == null) return;
                             }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
index 0076292..5809c81 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
@@ -16,11 +16,12 @@
 
 package com.android.systemui.shared.system;
 
+import android.view.RemoteAnimationTarget;
 import android.view.WindowManager;
 
 public interface RemoteAnimationRunnerCompat {
     void onAnimationStart(@WindowManager.TransitionOldType int transit,
-            RemoteAnimationTargetCompat[] apps, RemoteAnimationTargetCompat[] wallpapers,
-            RemoteAnimationTargetCompat[] nonApps, Runnable finishedCallback);
+            RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
+            RemoteAnimationTarget[] nonApps, Runnable finishedCallback);
     void onAnimationCancelled();
 }
\ No newline at end of file
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 2d6bef5..8d1768c 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
@@ -11,12 +11,15 @@
  * 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
+ * limitations under the License.
  */
 
 package com.android.systemui.shared.system;
 
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_CLOSE;
@@ -29,88 +32,28 @@
 import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
+import android.app.TaskInfo;
 import android.app.WindowConfiguration;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.ArrayMap;
-import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
+import android.window.TransitionInfo.Change;
 
 import java.util.ArrayList;
+import java.util.function.BiPredicate;
 
 /**
- * @see RemoteAnimationTarget
+ * Some utility methods for creating {@link RemoteAnimationTarget} instances.
  */
 public class RemoteAnimationTargetCompat {
 
-    public static final int MODE_OPENING = RemoteAnimationTarget.MODE_OPENING;
-    public static final int MODE_CLOSING = RemoteAnimationTarget.MODE_CLOSING;
-    public static final int MODE_CHANGING = RemoteAnimationTarget.MODE_CHANGING;
-    public final int mode;
-
-    public static final int ACTIVITY_TYPE_UNDEFINED = WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-    public static final int ACTIVITY_TYPE_STANDARD = WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-    public static final int ACTIVITY_TYPE_HOME = WindowConfiguration.ACTIVITY_TYPE_HOME;
-    public static final int ACTIVITY_TYPE_RECENTS = WindowConfiguration.ACTIVITY_TYPE_RECENTS;
-    public static final int ACTIVITY_TYPE_ASSISTANT = WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
-    public final int activityType;
-
-    public final int taskId;
-    public final SurfaceControl leash;
-    public final boolean isTranslucent;
-    public final Rect clipRect;
-    public final int prefixOrderIndex;
-    public final Point position;
-    public final Rect localBounds;
-    public final Rect sourceContainerBounds;
-    public final Rect screenSpaceBounds;
-    public final Rect startScreenSpaceBounds;
-    public final boolean isNotInRecents;
-    public final Rect contentInsets;
-    public final ActivityManager.RunningTaskInfo taskInfo;
-    public final boolean allowEnterPip;
-    public final int rotationChange;
-    public final int windowType;
-    public final WindowConfiguration windowConfiguration;
-
-    private final SurfaceControl mStartLeash;
-
-    // Fields used only to unwrap into RemoteAnimationTarget
-    private final Rect startBounds;
-
-    public final boolean willShowImeOnTarget;
-
-    public RemoteAnimationTargetCompat(RemoteAnimationTarget app) {
-        taskId = app.taskId;
-        mode = app.mode;
-        leash = app.leash;
-        isTranslucent = app.isTranslucent;
-        clipRect = app.clipRect;
-        position = app.position;
-        localBounds = app.localBounds;
-        sourceContainerBounds = app.sourceContainerBounds;
-        screenSpaceBounds = app.screenSpaceBounds;
-        startScreenSpaceBounds = screenSpaceBounds;
-        prefixOrderIndex = app.prefixOrderIndex;
-        isNotInRecents = app.isNotInRecents;
-        contentInsets = app.contentInsets;
-        activityType = app.windowConfiguration.getActivityType();
-        taskInfo = app.taskInfo;
-        allowEnterPip = app.allowEnterPip;
-        rotationChange = app.rotationChange;
-
-        mStartLeash = app.startLeash;
-        windowType = app.windowType;
-        windowConfiguration = app.windowConfiguration;
-        startBounds = app.startBounds;
-        willShowImeOnTarget = app.willShowImeOnTarget;
-    }
-
     private static int newModeToLegacyMode(int newMode) {
         switch (newMode) {
             case WindowManager.TRANSIT_OPEN:
@@ -120,21 +63,10 @@
             case WindowManager.TRANSIT_TO_BACK:
                 return MODE_CLOSING;
             default:
-                return 2; // MODE_CHANGING
+                return MODE_CHANGING;
         }
     }
 
-    public RemoteAnimationTarget unwrap() {
-        final RemoteAnimationTarget target = new RemoteAnimationTarget(
-                taskId, mode, leash, isTranslucent, clipRect, contentInsets,
-                prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration,
-                isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType
-        );
-        target.setWillShowImeOnTarget(willShowImeOnTarget);
-        target.setRotationChange(rotationChange);
-        return target;
-    }
-
     /**
      * Almost a copy of Transitions#setupStartState.
      * TODO: remove when there is proper cross-process transaction sync.
@@ -206,54 +138,61 @@
         return leashSurface;
     }
 
-    public RemoteAnimationTargetCompat(TransitionInfo.Change change, int order,
-            TransitionInfo info, SurfaceControl.Transaction t) {
-        mode = newModeToLegacyMode(change.getMode());
+    /**
+     * Creates a new RemoteAnimationTarget from the provided change info
+     */
+    public static RemoteAnimationTarget newTarget(TransitionInfo.Change change, int order,
+            TransitionInfo info, SurfaceControl.Transaction t,
+            @Nullable ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+        int taskId;
+        boolean isNotInRecents;
+        ActivityManager.RunningTaskInfo taskInfo;
+        WindowConfiguration windowConfiguration;
+
         taskInfo = change.getTaskInfo();
         if (taskInfo != null) {
             taskId = taskInfo.taskId;
             isNotInRecents = !taskInfo.isRunning;
-            activityType = taskInfo.getActivityType();
             windowConfiguration = taskInfo.configuration.windowConfiguration;
         } else {
             taskId = INVALID_TASK_ID;
             isNotInRecents = true;
-            activityType = ACTIVITY_TYPE_UNDEFINED;
             windowConfiguration = new WindowConfiguration();
         }
 
-        // TODO: once we can properly sync transactions across process, then get rid of this leash.
-        leash = createLeash(info, change, order, t);
-
-        isTranslucent = (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0;
-        clipRect = null;
-        position = null;
-        localBounds = new Rect(change.getEndAbsBounds());
+        Rect localBounds = new Rect(change.getEndAbsBounds());
         localBounds.offsetTo(change.getEndRelOffset().x, change.getEndRelOffset().y);
-        sourceContainerBounds = null;
-        screenSpaceBounds = new Rect(change.getEndAbsBounds());
-        startScreenSpaceBounds = new Rect(change.getStartAbsBounds());
 
-        prefixOrderIndex = order;
-        // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used.
-        contentInsets = new Rect(0, 0, 0, 0);
-        allowEnterPip = change.getAllowEnterPip();
-        mStartLeash = null;
-        rotationChange = change.getEndRotation() - change.getStartRotation();
-        windowType = (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
-                ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE;
-
-        startBounds = change.getStartAbsBounds();
-        willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0;
-    }
-
-    public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) {
-        final int length = apps != null ? apps.length : 0;
-        final RemoteAnimationTargetCompat[] appsCompat = new RemoteAnimationTargetCompat[length];
-        for (int i = 0; i < length; i++) {
-            appsCompat[i] = new RemoteAnimationTargetCompat(apps[i]);
+        RemoteAnimationTarget target = new RemoteAnimationTarget(
+                taskId,
+                newModeToLegacyMode(change.getMode()),
+                // TODO: once we can properly sync transactions across process,
+                // then get rid of this leash.
+                createLeash(info, change, order, t),
+                (change.getFlags() & TransitionInfo.FLAG_TRANSLUCENT) != 0,
+                null,
+                // TODO(shell-transitions): we need to send content insets? evaluate how its used.
+                new Rect(0, 0, 0, 0),
+                order,
+                null,
+                localBounds,
+                new Rect(change.getEndAbsBounds()),
+                windowConfiguration,
+                isNotInRecents,
+                null,
+                new Rect(change.getStartAbsBounds()),
+                taskInfo,
+                change.getAllowEnterPip(),
+                (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
+                        ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE
+        );
+        target.setWillShowImeOnTarget(
+                (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0);
+        target.setRotationChange(change.getEndRotation() - change.getStartRotation());
+        if (leashMap != null) {
+            leashMap.put(change.getLeash(), target.leash);
         }
-        return appsCompat;
+        return target;
     }
 
     /**
@@ -262,35 +201,20 @@
      * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
      *                 populated by this function. If null, it is ignored.
      */
-    public static RemoteAnimationTargetCompat[] wrapApps(TransitionInfo info,
+    public static RemoteAnimationTarget[] wrapApps(TransitionInfo info,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
-        final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
-        final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>();
-        for (int i = 0; i < info.getChanges().size(); i++) {
-            final TransitionInfo.Change change = info.getChanges().get(i);
-            if (change.getTaskInfo() == null) continue;
-
-            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+        SparseBooleanArray childTaskTargets = new SparseBooleanArray();
+        return wrap(info, t, leashMap, (change, taskInfo) -> {
             // Children always come before parent since changes are in top-to-bottom z-order.
-            if (taskInfo != null) {
-                if (childTaskTargets.contains(taskInfo.taskId)) {
-                    // has children, so not a leaf. Skip.
-                    continue;
-                }
-                if (taskInfo.hasParentTask()) {
-                    childTaskTargets.put(taskInfo.parentTaskId, change);
-                }
+            if ((taskInfo == null) || childTaskTargets.get(taskInfo.taskId)) {
+                // has children, so not a leaf. Skip.
+                return false;
             }
-
-            final RemoteAnimationTargetCompat targetCompat =
-                    new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
-            if (leashMap != null) {
-                leashMap.put(change.getLeash(), targetCompat.leash);
+            if (taskInfo.hasParentTask()) {
+                childTaskTargets.put(taskInfo.parentTaskId, true);
             }
-            out.add(targetCompat);
-        }
-
-        return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+            return true;
+        });
     }
 
     /**
@@ -301,38 +225,22 @@
      * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
      *                 populated by this function. If null, it is ignored.
      */
-    public static RemoteAnimationTargetCompat[] wrapNonApps(TransitionInfo info, boolean wallpapers,
+    public static RemoteAnimationTarget[] wrapNonApps(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
-        final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
-
-        for (int i = 0; i < info.getChanges().size(); i++) {
-            final TransitionInfo.Change change = info.getChanges().get(i);
-            if (change.getTaskInfo() != null) continue;
-
-            final boolean changeIsWallpaper =
-                    (change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0;
-            if (wallpapers != changeIsWallpaper) continue;
-
-            final RemoteAnimationTargetCompat targetCompat =
-                    new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
-            if (leashMap != null) {
-                leashMap.put(change.getLeash(), targetCompat.leash);
-            }
-            out.add(targetCompat);
-        }
-
-        return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+        return wrap(info, t, leashMap, (change, taskInfo) -> (taskInfo == null)
+                && wallpapers == ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0));
     }
 
-    /**
-     * @see SurfaceControl#release()
-     */
-    public void release() {
-        if (leash != null) {
-            leash.release();
+    private static RemoteAnimationTarget[] wrap(TransitionInfo info,
+            SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
+            BiPredicate<Change, TaskInfo> filter) {
+        final ArrayList<RemoteAnimationTarget> out = new ArrayList<>();
+        for (int i = 0; i < info.getChanges().size(); i++) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (filter.test(change, change.getTaskInfo())) {
+                out.add(newTarget(change, info.getChanges().size() - i, info, t, leashMap));
+            }
         }
-        if (mStartLeash != null) {
-            mStartLeash.release();
-        }
+        return out.toArray(new RemoteAnimationTarget[out.size()]);
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index f679225..d6655a7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -17,7 +17,9 @@
 package com.android.systemui.shared.system;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
@@ -27,8 +29,7 @@
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.window.TransitionFilter.CONTAINER_ORDER_TOP;
 
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_RECENTS;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.newTarget;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -45,6 +46,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.IRecentsAnimationController;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.window.IRemoteTransition;
 import android.window.IRemoteTransitionFinishedCallback;
@@ -127,9 +129,9 @@
                     SurfaceControl.Transaction t,
                     IRemoteTransitionFinishedCallback finishedCallback) {
                 final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
-                final RemoteAnimationTargetCompat[] apps =
+                final RemoteAnimationTarget[] apps =
                         RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
-                final RemoteAnimationTargetCompat[] wallpapers =
+                final RemoteAnimationTarget[] wallpapers =
                         RemoteAnimationTargetCompat.wrapNonApps(
                                 info, true /* wallpapers */, t, leashMap);
                 // TODO(b/177438007): Move this set-up logic into launcher's animation impl.
@@ -230,7 +232,7 @@
         private PictureInPictureSurfaceTransaction mPipTransaction = null;
         private IBinder mTransition = null;
         private boolean mKeyguardLocked = false;
-        private RemoteAnimationTargetCompat[] mAppearedTargets;
+        private RemoteAnimationTarget[] mAppearedTargets;
         private boolean mWillFinishToHome = false;
 
         void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info,
@@ -325,18 +327,15 @@
             final int layer = mInfo.getChanges().size() * 3;
             mOpeningLeashes = new ArrayList<>();
             mOpeningHome = cancelRecents;
-            final RemoteAnimationTargetCompat[] targets =
-                    new RemoteAnimationTargetCompat[openingTasks.size()];
+            final RemoteAnimationTarget[] targets =
+                    new RemoteAnimationTarget[openingTasks.size()];
             for (int i = 0; i < openingTasks.size(); ++i) {
                 final TransitionInfo.Change change = openingTasks.valueAt(i);
                 mOpeningLeashes.add(change.getLeash());
                 // We are receiving new opening tasks, so convert to onTasksAppeared.
-                final RemoteAnimationTargetCompat target = new RemoteAnimationTargetCompat(
-                        change, layer, info, t);
-                mLeashMap.put(mOpeningLeashes.get(i), target.leash);
-                t.reparent(target.leash, mInfo.getRootLeash());
-                t.setLayer(target.leash, layer);
-                targets[i] = target;
+                targets[i] = newTarget(change, layer, info, t, mLeashMap);
+                t.reparent(targets[i].leash, mInfo.getRootLeash());
+                t.setLayer(targets[i].leash, layer);
             }
             t.apply();
             mAppearedTargets = targets;
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 910955a..40a96b0 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -30,11 +30,14 @@
 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
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.log.dagger.KeyguardClockLog
 import com.android.systemui.plugins.ClockController
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.shared.regionsampling.RegionSamplingInstance
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
@@ -66,12 +69,14 @@
     private val context: Context,
     @Main private val mainExecutor: Executor,
     @Background private val bgExecutor: Executor,
+    @KeyguardClockLog private val logBuffer: LogBuffer,
     private val featureFlags: FeatureFlags
 ) {
     var clock: ClockController? = null
         set(value) {
             field = value
             if (value != null) {
+                value.setLogBuffer(logBuffer)
                 value.initialize(resources, dozeAmount, 0f)
                 updateRegionSamplers(value)
             }
@@ -217,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)
+                }
             }
         }
     }
@@ -261,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/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index d03ef98..8ebad6c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -127,7 +127,7 @@
         if (useLargeClock) {
             out = mSmallClockFrame;
             in = mLargeClockFrame;
-            if (indexOfChild(in) == -1) addView(in);
+            if (indexOfChild(in) == -1) addView(in, 0);
             direction = -1;
             statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
                     + mSmartspaceTopOffset;
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/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index e74d810..80c6c48 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -127,7 +127,7 @@
     private final ScrollView mBiometricScrollView;
     private final View mPanelView;
     private final float mTranslationY;
-    @ContainerState private int mContainerState = STATE_UNKNOWN;
+    @VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
     private final Set<Integer> mFailedModalities = new HashSet<Integer>();
     private final OnBackInvokedCallback mBackCallback = this::onBackInvoked;
 
@@ -485,45 +485,18 @@
             mContainerState = STATE_SHOWING;
         } else {
             mContainerState = STATE_ANIMATING_IN;
-            // The background panel and content are different views since we need to be able to
-            // animate them separately in other places.
-            mPanelView.setY(mTranslationY);
-            mBiometricScrollView.setY(mTranslationY);
-
+            setY(mTranslationY);
             setAlpha(0f);
             final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_SHOW_MS;
             postOnAnimation(() -> {
-                mPanelView.animate()
-                        .translationY(0)
-                        .setDuration(animateDuration)
-                        .setInterpolator(mLinearOutSlowIn)
-                        .setListener(getJankListener(mPanelView, SHOW, animateDuration))
-                        .withLayer()
-                        .withEndAction(this::onDialogAnimatedIn)
-                        .start();
-                mBiometricScrollView.animate()
-                        .translationY(0)
-                        .setDuration(animateDuration)
-                        .setInterpolator(mLinearOutSlowIn)
-                        .setListener(getJankListener(mBiometricScrollView, SHOW, animateDuration))
-                        .withLayer()
-                        .start();
-                if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
-                    mCredentialView.setY(mTranslationY);
-                    mCredentialView.animate()
-                            .translationY(0)
-                            .setDuration(animateDuration)
-                            .setInterpolator(mLinearOutSlowIn)
-                            .setListener(getJankListener(mCredentialView, SHOW, animateDuration))
-                            .withLayer()
-                            .start();
-                }
                 animate()
                         .alpha(1f)
+                        .translationY(0)
                         .setDuration(animateDuration)
                         .setInterpolator(mLinearOutSlowIn)
                         .withLayer()
                         .setListener(getJankListener(this, SHOW, animateDuration))
+                        .withEndAction(this::onDialogAnimatedIn)
                         .start();
             });
         }
@@ -657,11 +630,25 @@
         wm.addView(this, getLayoutParams(mWindowToken, mConfig.mPromptInfo.getTitle()));
     }
 
+    private void forceExecuteAnimatedIn() {
+        if (mContainerState == STATE_ANIMATING_IN) {
+            //clear all animators
+            if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
+                mCredentialView.animate().cancel();
+            }
+            mPanelView.animate().cancel();
+            mBiometricView.animate().cancel();
+            animate().cancel();
+            onDialogAnimatedIn();
+        }
+    }
+
     @Override
     public void dismissWithoutCallback(boolean animate) {
         if (animate) {
             animateAway(false /* sendReason */, 0 /* reason */);
         } else {
+            forceExecuteAnimatedIn();
             removeWindowIfAttached();
         }
     }
@@ -788,32 +775,9 @@
 
         final long animateDuration = mConfig.mSkipAnimation ? 0 : ANIMATION_DURATION_AWAY_MS;
         postOnAnimation(() -> {
-            mPanelView.animate()
-                    .translationY(mTranslationY)
-                    .setDuration(animateDuration)
-                    .setInterpolator(mLinearOutSlowIn)
-                    .setListener(getJankListener(mPanelView, DISMISS, animateDuration))
-                    .withLayer()
-                    .withEndAction(endActionRunnable)
-                    .start();
-            mBiometricScrollView.animate()
-                    .translationY(mTranslationY)
-                    .setDuration(animateDuration)
-                    .setInterpolator(mLinearOutSlowIn)
-                    .setListener(getJankListener(mBiometricScrollView, DISMISS, animateDuration))
-                    .withLayer()
-                    .start();
-            if (mCredentialView != null && mCredentialView.isAttachedToWindow()) {
-                mCredentialView.animate()
-                        .translationY(mTranslationY)
-                        .setDuration(animateDuration)
-                        .setInterpolator(mLinearOutSlowIn)
-                        .setListener(getJankListener(mCredentialView, DISMISS, animateDuration))
-                        .withLayer()
-                        .start();
-            }
             animate()
                     .alpha(0f)
+                    .translationY(mTranslationY)
                     .setDuration(animateDuration)
                     .setInterpolator(mLinearOutSlowIn)
                     .setListener(getJankListener(this, DISMISS, animateDuration))
@@ -828,6 +792,7 @@
                         mWindowManager.updateViewLayout(this, lp);
                     })
                     .withLayer()
+                    .withEndAction(endActionRunnable)
                     .start();
         });
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index 3b41a2d..c015a21 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -294,6 +294,8 @@
                 }
             });
             mUdfpsController.setAuthControllerUpdateUdfpsLocation(this::updateUdfpsLocation);
+            mUdfpsController.setUdfpsDisplayMode(new UdfpsDisplayMode(mContext, mExecution,
+                    this));
             mUdfpsBounds = mUdfpsProps.get(0).getLocation().getRect();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index d17f787..b49d452 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -123,7 +123,6 @@
     @NonNull private final PowerManager mPowerManager;
     @NonNull private final AccessibilityManager mAccessibilityManager;
     @NonNull private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
-    @Nullable private final UdfpsDisplayModeProvider mUdfpsDisplayMode;
     @NonNull private final ConfigurationController mConfigurationController;
     @NonNull private final SystemClock mSystemClock;
     @NonNull private final UnlockedScreenOffAnimationController
@@ -139,6 +138,7 @@
     // TODO(b/229290039): UDFPS controller should manage its dimensions on its own. Remove this.
     @Nullable private Runnable mAuthControllerUpdateUdfpsLocation;
     @Nullable private final AlternateUdfpsTouchProvider mAlternateTouchProvider;
+    @Nullable private UdfpsDisplayMode mUdfpsDisplayMode;
 
     // Tracks the velocity of a touch to help filter out the touches that move too fast.
     @Nullable private VelocityTracker mVelocityTracker;
@@ -319,6 +319,10 @@
         mAuthControllerUpdateUdfpsLocation = r;
     }
 
+    public void setUdfpsDisplayMode(UdfpsDisplayMode udfpsDisplayMode) {
+        mUdfpsDisplayMode = udfpsDisplayMode;
+    }
+
     /**
      * Calculate the pointer speed given a velocity tracker and the pointer id.
      * This assumes that the velocity tracker has already been passed all relevant motion events.
@@ -594,7 +598,6 @@
             @NonNull VibratorHelper vibrator,
             @NonNull UdfpsHapticsSimulator udfpsHapticsSimulator,
             @NonNull UdfpsShell udfpsShell,
-            @NonNull Optional<UdfpsDisplayModeProvider> udfpsDisplayMode,
             @NonNull KeyguardStateController keyguardStateController,
             @NonNull DisplayManager displayManager,
             @Main Handler mainHandler,
@@ -626,7 +629,6 @@
         mPowerManager = powerManager;
         mAccessibilityManager = accessibilityManager;
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
-        mUdfpsDisplayMode = udfpsDisplayMode.orElse(null);
         screenLifecycle.addObserver(mScreenObserver);
         mScreenOn = screenLifecycle.getScreenState() == ScreenLifecycle.SCREEN_ON;
         mConfigurationController = configurationController;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt
new file mode 100644
index 0000000..e9de7cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDisplayMode.kt
@@ -0,0 +1,88 @@
+package com.android.systemui.biometrics
+
+import android.content.Context
+import android.os.RemoteException
+import android.os.Trace
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.util.concurrency.Execution
+import javax.inject.Inject
+
+private const val TAG = "UdfpsDisplayMode"
+
+/**
+ * UdfpsDisplayMode that encapsulates pixel-specific code, such as enabling the high-brightness mode
+ * (HBM) in a display-specific way and freezing the display's refresh rate.
+ */
+@SysUISingleton
+class UdfpsDisplayMode
+@Inject
+constructor(
+    private val context: Context,
+    private val execution: Execution,
+    private val authController: AuthController
+) : UdfpsDisplayModeProvider {
+
+    // The request is reset to null after it's processed.
+    private var currentRequest: Request? = null
+
+    override fun enable(onEnabled: Runnable?) {
+        execution.isMainThread()
+        Log.v(TAG, "enable")
+
+        if (currentRequest != null) {
+            Log.e(TAG, "enable | already requested")
+            return
+        }
+        if (authController.udfpsHbmListener == null) {
+            Log.e(TAG, "enable | mDisplayManagerCallback is null")
+            return
+        }
+
+        Trace.beginSection("UdfpsDisplayMode.enable")
+
+        // Track this request in one object.
+        val request = Request(context.displayId)
+        currentRequest = request
+
+        try {
+            // This method is a misnomer. It has nothing to do with HBM, its purpose is to set
+            // the appropriate display refresh rate.
+            authController.udfpsHbmListener!!.onHbmEnabled(request.displayId)
+            Log.v(TAG, "enable | requested optimal refresh rate for UDFPS")
+        } catch (e: RemoteException) {
+            Log.e(TAG, "enable", e)
+        }
+
+        onEnabled?.run() ?: Log.w(TAG, "enable | onEnabled is null")
+        Trace.endSection()
+    }
+
+    override fun disable(onDisabled: Runnable?) {
+        execution.isMainThread()
+        Log.v(TAG, "disable")
+
+        val request = currentRequest
+        if (request == null) {
+            Log.w(TAG, "disable | already disabled")
+            return
+        }
+
+        Trace.beginSection("UdfpsDisplayMode.disable")
+
+        try {
+            // Allow DisplayManager to unset the UDFPS refresh rate.
+            authController.udfpsHbmListener!!.onHbmDisabled(request.displayId)
+            Log.v(TAG, "disable | removed the UDFPS refresh rate request")
+        } catch (e: RemoteException) {
+            Log.e(TAG, "disable", e)
+        }
+
+        currentRequest = null
+        onDisabled?.run() ?: Log.w(TAG, "disable | onDisabled is null")
+        Trace.endSection()
+    }
+}
+
+/** Tracks a request to enable the UDFPS mode. */
+private data class Request(val displayId: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
index bfb27a4..9f338d1 100644
--- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayController.java
@@ -459,7 +459,7 @@
         anim.start();
     }
 
-    private void hideImmediate() {
+    void hideImmediate() {
         // Note this may be called multiple times if multiple dismissal events happen at the same
         // time.
         mTimeoutHandler.cancelTimeout();
diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
index c256e44..976afd4 100644
--- a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt
@@ -34,8 +34,6 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import java.util.concurrent.Executor
 import javax.inject.Inject
@@ -47,15 +45,13 @@
     private val statusBarStateController: StatusBarStateController,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     @Main private val mainExecutor: Executor,
-    private val featureFlags: FeatureFlags
 ) : DecorProviderFactory() {
     private val display = context.display
     private val displayInfo = DisplayInfo()
 
     override val hasProviders: Boolean
         get() {
-            if (!featureFlags.isEnabled(Flags.FACE_SCANNING_ANIM) ||
-                    authController.faceSensorLocation == null) {
+            if (authController.faceSensorLocation == null) {
                 return false
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e84a564..06b7614 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -63,7 +63,8 @@
     @JvmField val NOTIFICATION_DISMISSAL_FADE = UnreleasedFlag(113, teamfood = true)
     val STABILITY_INDEX_FIX = UnreleasedFlag(114, teamfood = true)
     val SEMI_STABLE_SORT = UnreleasedFlag(115, teamfood = true)
-    // next id: 116
+    @JvmField val NOTIFICATION_GROUP_CORNER = UnreleasedFlag(116, true)
+    // next id: 117
 
     // 200 - keyguard/lockscreen
     // ** Flag retired **
@@ -80,9 +81,6 @@
     @JvmField
     val BOUNCER_USER_SWITCHER = ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher)
 
-    // TODO(b/254512694): Tracking Bug
-    val FACE_SCANNING_ANIM = ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation)
-
     // TODO(b/254512676): Tracking Bug
     @JvmField val LOCKSCREEN_CUSTOM_CLOCKS = UnreleasedFlag(207, teamfood = true)
 
@@ -120,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)
@@ -192,7 +196,7 @@
 
     // 802 - wallpaper rendering
     // TODO(b/254512923): Tracking Bug
-    @JvmField val USE_CANVAS_RENDERER = UnreleasedFlag(802, teamfood = true)
+    @JvmField val USE_CANVAS_RENDERER = ReleasedFlag(802)
 
     // 803 - screen contents translation
     // TODO(b/254513187): Tracking Bug
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/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
new file mode 100644
index 0000000..0645236
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.plugins.log.LogBuffer] for keyguard clock logs. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class KeyguardClockLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index e3311db..9adb855 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -316,6 +316,16 @@
     }
 
     /**
+     * Provides a {@link LogBuffer} for keyguard clock logs.
+     */
+    @Provides
+    @SysUISingleton
+    @KeyguardClockLog
+    public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) {
+        return factory.create("KeyguardClockLog", 500);
+    }
+
+    /**
      * Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}.
      */
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index 26cbcbf..1b9cdd4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -767,7 +767,9 @@
         mShareChip.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
-                mActionExecutor.launchIntentAsync(ActionIntentCreator.INSTANCE.createShareIntent(
+                prepareSharedTransition();
+                mActionExecutor.launchIntentAsync(
+                        ActionIntentCreator.INSTANCE.createShareIntent(
                                 imageData.uri, imageData.subject),
                         imageData.shareTransition.get().bundle,
                         imageData.owner.getIdentifier(), false);
@@ -778,6 +780,7 @@
         mEditChip.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+                prepareSharedTransition();
                 mActionExecutor.launchIntentAsync(
                         ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
                         imageData.editTransition.get().bundle,
@@ -789,6 +792,7 @@
         mScreenshotPreview.setOnClickListener(v -> {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
             if (mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)) {
+                prepareSharedTransition();
                 mActionExecutor.launchIntentAsync(
                         ActionIntentCreator.INSTANCE.createEditIntent(imageData.uri, mContext),
                         imageData.editTransition.get().bundle,
@@ -1064,6 +1068,12 @@
         }
     }
 
+    private void prepareSharedTransition() {
+        mPendingSharedTransition = true;
+        // fade out non-preview UI
+        createScreenshotFadeDismissAnimation().start();
+    }
+
     ValueAnimator createScreenshotFadeDismissAnimation() {
         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
         alphaAnim.addUpdateListener(animation -> {
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index 6e9f859..d5a3954 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -20,6 +20,7 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import android.app.Activity;
+import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Handler;
 import android.view.Gravity;
@@ -36,6 +37,8 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 
+import java.util.List;
+
 import javax.inject.Inject;
 
 /** A dialog that provides controls for adjusting the screen brightness. */
@@ -83,6 +86,15 @@
         lp.leftMargin = horizontalMargin;
         lp.rightMargin = horizontalMargin;
         frame.setLayoutParams(lp);
+        Rect bounds = new Rect();
+        frame.addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+                    // Exclude this view (and its horizontal margins) from triggering gestures.
+                    // This prevents back gesture from being triggered by dragging close to the
+                    // edge of the slider (0% or 100%).
+                    bounds.set(-horizontalMargin, 0, right - left + horizontalMargin, bottom - top);
+                    v.setSystemGestureExclusionRects(List.of(bounds));
+                });
 
         BrightnessSliderController controller = mToggleSliderFactory.create(this, frame);
         controller.init();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index f961984..87ef92a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -40,6 +40,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -110,8 +111,8 @@
         setClipChildren(false);
         setClipToPadding(false);
         mShelfIcons.setIsStaticLayout(false);
-        setBottomRoundness(1.0f, false /* animate */);
-        setTopRoundness(1f, false /* animate */);
+        requestBottomRoundness(1.0f, /* animate = */ false, SourceType.DefaultValue);
+        requestTopRoundness(1f, false, SourceType.DefaultValue);
 
         // Setting this to first in section to get the clipping to the top roundness correct. This
         // value determines the way we are clipping to the top roundness of the overall shade
@@ -413,7 +414,7 @@
                     if (iconState != null && iconState.clampedAppearAmount == 1.0f) {
                         // only if the first icon is fully in the shelf we want to clip to it!
                         backgroundTop = (int) (child.getTranslationY() - getTranslationY());
-                        firstElementRoundness = expandableRow.getCurrentTopRoundness();
+                        firstElementRoundness = expandableRow.getTopRoundness();
                     }
                 }
 
@@ -507,28 +508,36 @@
             // Round bottom corners within animation bounds
             final float changeFraction = MathUtils.saturate(
                     (viewEnd - cornerAnimationTop) / cornerAnimationDistance);
-            anv.setBottomRoundness(anv.isLastInSection() ? 1f : changeFraction,
-                    false /* animate */);
+            anv.requestBottomRoundness(
+                    anv.isLastInSection() ? 1f : changeFraction,
+                    /* animate = */ false,
+                    SourceType.OnScroll);
 
         } else if (viewEnd < cornerAnimationTop) {
             // Fast scroll skips frames and leaves corners with unfinished rounding.
             // Reset top and bottom corners outside of animation bounds.
-            anv.setBottomRoundness(anv.isLastInSection() ? 1f : smallCornerRadius,
-                    false /* animate */);
+            anv.requestBottomRoundness(
+                    anv.isLastInSection() ? 1f : smallCornerRadius,
+                    /* animate = */ false,
+                    SourceType.OnScroll);
         }
 
         if (viewStart >= cornerAnimationTop) {
             // Round top corners within animation bounds
             final float changeFraction = MathUtils.saturate(
                     (viewStart - cornerAnimationTop) / cornerAnimationDistance);
-            anv.setTopRoundness(anv.isFirstInSection() ? 1f : changeFraction,
-                    false /* animate */);
+            anv.requestTopRoundness(
+                    anv.isFirstInSection() ? 1f : changeFraction,
+                    false,
+                    SourceType.OnScroll);
 
         } else if (viewStart < cornerAnimationTop) {
             // Fast scroll skips frames and leaves corners with unfinished rounding.
             // Reset top and bottom corners outside of animation bounds.
-            anv.setTopRoundness(anv.isFirstInSection() ? 1f : smallCornerRadius,
-                    false /* animate */);
+            anv.requestTopRoundness(
+                    anv.isFirstInSection() ? 1f : smallCornerRadius,
+                    false,
+                    SourceType.OnScroll);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
index 553826d..0d35fdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorController.kt
@@ -70,8 +70,8 @@
         val height = max(0, notification.actualHeight - notification.clipBottomAmount)
         val location = notification.locationOnScreen
 
-        val clipStartLocation = notificationListContainer.getTopClippingStartLocation()
-        val roundedTopClipping = Math.max(clipStartLocation - location[1], 0)
+        val clipStartLocation = notificationListContainer.topClippingStartLocation
+        val roundedTopClipping = (clipStartLocation - location[1]).coerceAtLeast(0)
         val windowTop = location[1] + roundedTopClipping
         val topCornerRadius = if (roundedTopClipping > 0) {
             // Because the rounded Rect clipping is complex, we start the top rounding at
@@ -80,7 +80,7 @@
             // if we'd like to have this perfect, but this is close enough.
             0f
         } else {
-            notification.currentBackgroundRadiusTop
+            notification.topCornerRadius
         }
         val params = LaunchAnimationParameters(
             top = windowTop,
@@ -88,7 +88,7 @@
             left = location[0],
             right = location[0] + notification.width,
             topCornerRadius = topCornerRadius,
-            bottomCornerRadius = notification.currentBackgroundRadiusBottom
+            bottomCornerRadius = notification.bottomCornerRadius
         )
 
         params.startTranslationZ = notification.translationZ
@@ -97,8 +97,8 @@
         params.startClipTopAmount = notification.clipTopAmount
         if (notification.isChildInGroup) {
             params.startNotificationTop += notification.notificationParent.translationY
-            val parentRoundedClip = Math.max(
-                clipStartLocation - notification.notificationParent.locationOnScreen[1], 0)
+            val locationOnScreen = notification.notificationParent.locationOnScreen[1]
+            val parentRoundedClip = (clipStartLocation - locationOnScreen).coerceAtLeast(0)
             params.parentStartRoundedTopClipping = parentRoundedClip
 
             val parentClip = notification.notificationParent.clipTopAmount
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
new file mode 100644
index 0000000..ed7f648
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -0,0 +1,284 @@
+package com.android.systemui.statusbar.notification
+
+import android.util.FloatProperty
+import android.view.View
+import androidx.annotation.FloatRange
+import com.android.systemui.R
+import com.android.systemui.statusbar.notification.stack.AnimationProperties
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import kotlin.math.abs
+
+/**
+ * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f).
+ *
+ * To request a roundness value, an [SourceType] must be specified. In case more origins require
+ * different roundness, for the same property, the maximum value will always be chosen.
+ *
+ * It also returns the current radius for all corners ([updatedRadii]).
+ */
+interface Roundable {
+    /** Properties required for a Roundable */
+    val roundableState: RoundableState
+
+    /** Current top roundness */
+    @get:FloatRange(from = 0.0, to = 1.0)
+    @JvmDefault
+    val topRoundness: Float
+        get() = roundableState.topRoundness
+
+    /** Current bottom roundness */
+    @get:FloatRange(from = 0.0, to = 1.0)
+    @JvmDefault
+    val bottomRoundness: Float
+        get() = roundableState.bottomRoundness
+
+    /** Max radius in pixel */
+    @JvmDefault
+    val maxRadius: Float
+        get() = roundableState.maxRadius
+
+    /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
+    @JvmDefault
+    val topCornerRadius: Float
+        get() = topRoundness * maxRadius
+
+    /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
+    @JvmDefault
+    val bottomCornerRadius: Float
+        get() = bottomRoundness * maxRadius
+
+    /** Get and update the current radii */
+    @JvmDefault
+    val updatedRadii: FloatArray
+        get() =
+            roundableState.radiiBuffer.also { radii ->
+                updateRadii(
+                    topCornerRadius = topCornerRadius,
+                    bottomCornerRadius = bottomCornerRadius,
+                    radii = radii,
+                )
+            }
+
+    /**
+     * Request the top roundness [value] for a specific [sourceType].
+     *
+     * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
+     * origins require different roundness, for the same property, the maximum value will always be
+     * chosen.
+     *
+     * @param value a value between 0f and 1f.
+     * @param animate true if it should animate to that value.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestTopRoundness(
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        animate: Boolean,
+        sourceType: SourceType,
+    ): Boolean {
+        val roundnessMap = roundableState.topRoundnessMap
+        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
+        if (value == 0f) {
+            // we should only take the largest value, and since the smallest value is 0f, we can
+            // remove this value from the list. In the worst case, the list is empty and the
+            // default value is 0f.
+            roundnessMap.remove(sourceType)
+        } else {
+            roundnessMap[sourceType] = value
+        }
+        val newValue = roundnessMap.values.maxOrNull() ?: 0f
+
+        if (lastValue != newValue) {
+            val wasAnimating = roundableState.isTopAnimating()
+
+            // Fail safe:
+            // when we've been animating previously and we're now getting an update in the
+            // other direction, make sure to animate it too, otherwise, the localized updating
+            // may make the start larger than 1.0.
+            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
+
+            roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate)
+            return true
+        }
+        return false
+    }
+
+    /**
+     * Request the bottom roundness [value] for a specific [sourceType].
+     *
+     * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
+     * origins require different roundness, for the same property, the maximum value will always be
+     * chosen.
+     *
+     * @param value value between 0f and 1f.
+     * @param animate true if it should animate to that value.
+     * @param sourceType the source from which the request for roundness comes.
+     * @return Whether the roundness was changed.
+     */
+    @JvmDefault
+    fun requestBottomRoundness(
+        @FloatRange(from = 0.0, to = 1.0) value: Float,
+        animate: Boolean,
+        sourceType: SourceType,
+    ): Boolean {
+        val roundnessMap = roundableState.bottomRoundnessMap
+        val lastValue = roundnessMap.values.maxOrNull() ?: 0f
+        if (value == 0f) {
+            // we should only take the largest value, and since the smallest value is 0f, we can
+            // remove this value from the list. In the worst case, the list is empty and the
+            // default value is 0f.
+            roundnessMap.remove(sourceType)
+        } else {
+            roundnessMap[sourceType] = value
+        }
+        val newValue = roundnessMap.values.maxOrNull() ?: 0f
+
+        if (lastValue != newValue) {
+            val wasAnimating = roundableState.isBottomAnimating()
+
+            // Fail safe:
+            // when we've been animating previously and we're now getting an update in the
+            // other direction, make sure to animate it too, otherwise, the localized updating
+            // may make the start larger than 1.0.
+            val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
+
+            roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate)
+            return true
+        }
+        return false
+    }
+
+    /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
+    @JvmDefault
+    fun applyRoundness() {
+        roundableState.targetView.invalidate()
+    }
+
+    /** @return true if top or bottom roundness is not zero. */
+    @JvmDefault
+    fun hasRoundedCorner(): Boolean {
+        return topRoundness != 0f || bottomRoundness != 0f
+    }
+
+    /**
+     * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of
+     * [android.graphics.Path.addRoundRect].
+     *
+     * This method reuses the previous [radii] for performance reasons.
+     */
+    @JvmDefault
+    fun updateRadii(
+        topCornerRadius: Float,
+        bottomCornerRadius: Float,
+        radii: FloatArray,
+    ) {
+        if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}")
+
+        if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) {
+            (0..3).forEach { radii[it] = topCornerRadius }
+            (4..7).forEach { radii[it] = bottomCornerRadius }
+        }
+    }
+}
+
+/**
+ * State object for a `Roundable` class.
+ * @param targetView Will handle the [AnimatableProperty]
+ * @param roundable Target of the radius animation
+ * @param maxRadius Max corner radius in pixels
+ */
+class RoundableState(
+    internal val targetView: View,
+    roundable: Roundable,
+    internal val maxRadius: Float,
+) {
+    /** Animatable for top roundness */
+    private val topAnimatable = topAnimatable(roundable)
+
+    /** Animatable for bottom roundness */
+    private val bottomAnimatable = bottomAnimatable(roundable)
+
+    /** Current top roundness. Use [setTopRoundness] to update this value */
+    @set:FloatRange(from = 0.0, to = 1.0)
+    internal var topRoundness = 0f
+        private set
+
+    /** Current bottom roundness. Use [setBottomRoundness] to update this value */
+    @set:FloatRange(from = 0.0, to = 1.0)
+    internal var bottomRoundness = 0f
+        private set
+
+    /** Last requested top roundness associated by [SourceType] */
+    internal val topRoundnessMap = mutableMapOf<SourceType, Float>()
+
+    /** Last requested bottom roundness associated by [SourceType] */
+    internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>()
+
+    /** Last cached radii */
+    internal val radiiBuffer = FloatArray(8)
+
+    /** Is top roundness animation in progress? */
+    internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable)
+
+    /** Is bottom roundness animation in progress? */
+    internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable)
+
+    /** Set the current top roundness */
+    internal fun setTopRoundness(
+        value: Float,
+        animated: Boolean = targetView.isShown,
+    ) {
+        PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
+    }
+
+    /** Set the current bottom roundness */
+    internal fun setBottomRoundness(
+        value: Float,
+        animated: Boolean = targetView.isShown,
+    ) {
+        PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
+    }
+
+    companion object {
+        private val DURATION: AnimationProperties =
+            AnimationProperties()
+                .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong())
+
+        private fun topAnimatable(roundable: Roundable): AnimatableProperty =
+            AnimatableProperty.from(
+                object : FloatProperty<View>("topRoundness") {
+                    override fun get(view: View): Float = roundable.topRoundness
+
+                    override fun setValue(view: View, value: Float) {
+                        roundable.roundableState.topRoundness = value
+                        roundable.applyRoundness()
+                    }
+                },
+                R.id.top_roundess_animator_tag,
+                R.id.top_roundess_animator_end_tag,
+                R.id.top_roundess_animator_start_tag,
+            )
+
+        private fun bottomAnimatable(roundable: Roundable): AnimatableProperty =
+            AnimatableProperty.from(
+                object : FloatProperty<View>("bottomRoundness") {
+                    override fun get(view: View): Float = roundable.bottomRoundness
+
+                    override fun setValue(view: View, value: Float) {
+                        roundable.roundableState.bottomRoundness = value
+                        roundable.applyRoundness()
+                    }
+                },
+                R.id.bottom_roundess_animator_tag,
+                R.id.bottom_roundess_animator_end_tag,
+                R.id.bottom_roundess_animator_start_tag,
+            )
+    }
+}
+
+enum class SourceType {
+    DefaultValue,
+    OnDismissAnimation,
+    OnScroll,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 755e3e1..d29298a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -613,22 +613,21 @@
     protected void resetAllContentAlphas() {}
 
     @Override
-    protected void applyRoundness() {
+    public void applyRoundness() {
         super.applyRoundness();
-        applyBackgroundRoundness(getCurrentBackgroundRadiusTop(),
-                getCurrentBackgroundRadiusBottom());
+        applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius());
     }
 
     @Override
-    public float getCurrentBackgroundRadiusTop() {
+    public float getTopCornerRadius() {
         float fraction = getInterpolatedAppearAnimationFraction();
-        return MathUtils.lerp(0, super.getCurrentBackgroundRadiusTop(), fraction);
+        return MathUtils.lerp(0, super.getTopCornerRadius(), fraction);
     }
 
     @Override
-    public float getCurrentBackgroundRadiusBottom() {
+    public float getBottomCornerRadius() {
         float fraction = getInterpolatedAppearAnimationFraction();
-        return MathUtils.lerp(0, super.getCurrentBackgroundRadiusBottom(), fraction);
+        return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction);
     }
 
     private void applyBackgroundRoundness(float topRadius, float bottomRadius) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 087dc71..9e7717c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -93,6 +93,7 @@
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorController;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
@@ -154,7 +155,9 @@
         void onLayout();
     }
 
-    /** Listens for changes to the expansion state of this row. */
+    /**
+     * Listens for changes to the expansion state of this row.
+     */
     public interface OnExpansionChangedListener {
         void onExpansionChanged(boolean isExpanded);
     }
@@ -183,22 +186,34 @@
     private int mNotificationLaunchHeight;
     private boolean mMustStayOnScreen;
 
-    /** Does this row contain layouts that can adapt to row expansion */
+    /**
+     * Does this row contain layouts that can adapt to row expansion
+     */
     private boolean mExpandable;
-    /** Has the user actively changed the expansion state of this row */
+    /**
+     * Has the user actively changed the expansion state of this row
+     */
     private boolean mHasUserChangedExpansion;
-    /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
+    /**
+     * If {@link #mHasUserChangedExpansion}, has the user expanded this row
+     */
     private boolean mUserExpanded;
-    /** Whether the blocking helper is showing on this notification (even if dismissed) */
+    /**
+     * Whether the blocking helper is showing on this notification (even if dismissed)
+     */
     private boolean mIsBlockingHelperShowing;
 
     /**
      * Has this notification been expanded while it was pinned
      */
     private boolean mExpandedWhenPinned;
-    /** Is the user touching this row */
+    /**
+     * Is the user touching this row
+     */
     private boolean mUserLocked;
-    /** Are we showing the "public" version */
+    /**
+     * Are we showing the "public" version
+     */
     private boolean mShowingPublic;
     private boolean mSensitive;
     private boolean mSensitiveHiddenInGeneral;
@@ -351,11 +366,14 @@
     private boolean mWasChildInGroupWhenRemoved;
     private NotificationInlineImageResolver mImageResolver;
     private NotificationMediaManager mMediaManager;
-    @Nullable private OnExpansionChangedListener mExpansionChangedListener;
-    @Nullable private Runnable mOnIntrinsicHeightReachedRunnable;
+    @Nullable
+    private OnExpansionChangedListener mExpansionChangedListener;
+    @Nullable
+    private Runnable mOnIntrinsicHeightReachedRunnable;
 
     private float mTopRoundnessDuringLaunchAnimation;
     private float mBottomRoundnessDuringLaunchAnimation;
+    private boolean mIsNotificationGroupCornerEnabled;
 
     /**
      * Returns whether the given {@code statusBarNotification} is a system notification.
@@ -574,14 +592,18 @@
         }
     }
 
-    /** Called when the notification's ranking was changed (but nothing else changed). */
+    /**
+     * Called when the notification's ranking was changed (but nothing else changed).
+     */
     public void onNotificationRankingUpdated() {
         if (mMenuRow != null) {
             mMenuRow.onNotificationUpdated(mEntry.getSbn());
         }
     }
 
-    /** Call when bubble state has changed and the button on the notification should be updated. */
+    /**
+     * Call when bubble state has changed and the button on the notification should be updated.
+     */
     public void updateBubbleButton() {
         for (NotificationContentView l : mLayouts) {
             l.updateBubbleButton(mEntry);
@@ -620,6 +642,7 @@
 
     /**
      * Sets a supplier that can determine whether the keyguard is secure or not.
+     *
      * @param secureStateProvider A function that returns true if keyguard is secure.
      */
     public void setSecureStateProvider(BooleanSupplier secureStateProvider) {
@@ -781,7 +804,9 @@
         mChildrenContainer.setUntruncatedChildCount(childCount);
     }
 
-    /** Called after children have been attached to set the expansion states */
+    /**
+     * Called after children have been attached to set the expansion states
+     */
     public void resetChildSystemExpandedStates() {
         if (isSummaryWithChildren()) {
             mChildrenContainer.updateExpansionStates();
@@ -791,7 +816,7 @@
     /**
      * Add a child notification to this view.
      *
-     * @param row the row to add
+     * @param row        the row to add
      * @param childIndex the index to add it at, if -1 it will be added at the end
      */
     public void addChildNotification(ExpandableNotificationRow row, int childIndex) {
@@ -809,10 +834,12 @@
         }
         onAttachedChildrenCountChanged();
         row.setIsChildInGroup(false, null);
-        row.setBottomRoundness(0.0f, false /* animate */);
+        row.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
     }
 
-    /** Returns the child notification at [index], or null if no such child. */
+    /**
+     * Returns the child notification at [index], or null if no such child.
+     */
     @Nullable
     public ExpandableNotificationRow getChildNotificationAt(int index) {
         if (mChildrenContainer == null
@@ -834,7 +861,7 @@
 
     /**
      * @param isChildInGroup Is this notification now in a group
-     * @param parent the new parent notification
+     * @param parent         the new parent notification
      */
     public void setIsChildInGroup(boolean isChildInGroup, ExpandableNotificationRow parent) {
         if (mExpandAnimationRunning && !isChildInGroup && mNotificationParent != null) {
@@ -898,7 +925,9 @@
         return mChildrenContainer == null ? null : mChildrenContainer.getAttachedChildren();
     }
 
-    /** Updates states of all children. */
+    /**
+     * Updates states of all children.
+     */
     public void updateChildrenStates(AmbientState ambientState) {
         if (mIsSummaryWithChildren) {
             ExpandableViewState parentState = getViewState();
@@ -906,21 +935,27 @@
         }
     }
 
-    /** Applies children states. */
+    /**
+     * Applies children states.
+     */
     public void applyChildrenState() {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.applyState();
         }
     }
 
-    /** Prepares expansion changed. */
+    /**
+     * Prepares expansion changed.
+     */
     public void prepareExpansionChanged() {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.prepareExpansionChanged();
         }
     }
 
-    /** Starts child animations. */
+    /**
+     * Starts child animations.
+     */
     public void startChildAnimation(AnimationProperties properties) {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.startAnimationToState(properties);
@@ -984,7 +1019,7 @@
         if (mIsSummaryWithChildren) {
             return mChildrenContainer.getIntrinsicHeight();
         }
-        if(mExpandedWhenPinned) {
+        if (mExpandedWhenPinned) {
             return Math.max(getMaxExpandHeight(), getHeadsUpHeight());
         } else if (atLeastMinHeight) {
             return Math.max(getCollapsedHeight(), getHeadsUpHeight());
@@ -1079,18 +1114,22 @@
         updateClickAndFocus();
     }
 
-    /** The click listener for the bubble button. */
+    /**
+     * The click listener for the bubble button.
+     */
     public View.OnClickListener getBubbleClickListener() {
         return v -> {
             if (mBubblesManagerOptional.isPresent()) {
                 mBubblesManagerOptional.get()
-                    .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
+                        .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
             }
             mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */);
         };
     }
 
-    /** The click listener for the snooze button. */
+    /**
+     * The click listener for the snooze button.
+     */
     public View.OnClickListener getSnoozeClickListener(MenuItem item) {
         return v -> {
             // Dismiss a snoozed notification if one is still left behind
@@ -1252,7 +1291,7 @@
     }
 
     public void setContentBackground(int customBackgroundColor, boolean animate,
-            NotificationContentView notificationContentView) {
+                                     NotificationContentView notificationContentView) {
         if (getShowingLayout() == notificationContentView) {
             setTintColor(customBackgroundColor, animate);
         }
@@ -1637,7 +1676,9 @@
         setTargetPoint(null);
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.setFeedbackIcon(icon);
@@ -1646,7 +1687,9 @@
         mPublicLayout.setFeedbackIcon(icon);
     }
 
-    /** Sets the last time the notification being displayed audibly alerted the user. */
+    /**
+     * Sets the last time the notification being displayed audibly alerted the user.
+     */
     public void setLastAudiblyAlertedMs(long lastAudiblyAlertedMs) {
         long timeSinceAlertedAudibly = System.currentTimeMillis() - lastAudiblyAlertedMs;
         boolean alertedRecently = timeSinceAlertedAudibly < RECENTLY_ALERTED_THRESHOLD_MS;
@@ -1700,7 +1743,9 @@
         Trace.endSection();
     }
 
-    /** Generates and appends "(MessagingStyle)" type tag to passed string for tracing. */
+    /**
+     * Generates and appends "(MessagingStyle)" type tag to passed string for tracing.
+     */
     @NonNull
     private String appendTraceStyleTag(@NonNull String traceTag) {
         if (!Trace.isEnabled()) {
@@ -1721,7 +1766,7 @@
         super.onFinishInflate();
         mPublicLayout = findViewById(R.id.expandedPublic);
         mPrivateLayout = findViewById(R.id.expanded);
-        mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};
+        mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
 
         for (NotificationContentView l : mLayouts) {
             l.setExpandClickListener(mExpandClickListener);
@@ -1740,6 +1785,7 @@
             mChildrenContainer.setIsLowPriority(mIsLowPriority);
             mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this);
             mChildrenContainer.onNotificationUpdated();
+            mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
 
             mTranslateableViews.add(mChildrenContainer);
         });
@@ -1796,6 +1842,7 @@
     /**
      * Perform a smart action which triggers a longpress (expose guts).
      * Based on the semanticAction passed, may update the state of the guts view.
+     *
      * @param semanticAction associated with this smart action click
      */
     public void doSmartActionClick(int x, int y, int semanticAction) {
@@ -1939,9 +1986,10 @@
 
     /**
      * Set the dismiss behavior of the view.
+     *
      * @param usingRowTranslationX {@code true} if the view should translate using regular
-     *                                          translationX, otherwise the contents will be
-     *                                          translated.
+     *                             translationX, otherwise the contents will be
+     *                             translated.
      */
     @Override
     public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
@@ -1955,6 +2003,14 @@
             if (previousTranslation != 0) {
                 setTranslation(previousTranslation);
             }
+            if (mChildrenContainer != null) {
+                List<ExpandableNotificationRow> notificationChildren =
+                        mChildrenContainer.getAttachedChildren();
+                for (int i = 0; i < notificationChildren.size(); i++) {
+                    ExpandableNotificationRow child = notificationChildren.get(i);
+                    child.setDismissUsingRowTranslationX(usingRowTranslationX);
+                }
+            }
         }
     }
 
@@ -2009,7 +2065,7 @@
     }
 
     public Animator getTranslateViewAnimator(final float leftTarget,
-            AnimatorUpdateListener listener) {
+                                             AnimatorUpdateListener listener) {
         if (mTranslateAnim != null) {
             mTranslateAnim.cancel();
         }
@@ -2115,7 +2171,7 @@
                             NotificationLaunchAnimatorController.ANIMATION_DURATION_TOP_ROUNDING));
             float startTop = params.getStartNotificationTop();
             top = (int) Math.min(MathUtils.lerp(startTop,
-                    params.getTop(), expandProgress),
+                            params.getTop(), expandProgress),
                     startTop);
         } else {
             top = params.getTop();
@@ -2151,29 +2207,30 @@
         }
         setTranslationY(top);
 
-        mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / mOutlineRadius;
-        mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / mOutlineRadius;
+        final float maxRadius = getMaxRadius();
+        mTopRoundnessDuringLaunchAnimation = params.getTopCornerRadius() / maxRadius;
+        mBottomRoundnessDuringLaunchAnimation = params.getBottomCornerRadius() / maxRadius;
         invalidateOutline();
 
         mBackgroundNormal.setExpandAnimationSize(params.getWidth(), actualHeight);
     }
 
     @Override
-    public float getCurrentTopRoundness() {
+    public float getTopRoundness() {
         if (mExpandAnimationRunning) {
             return mTopRoundnessDuringLaunchAnimation;
         }
 
-        return super.getCurrentTopRoundness();
+        return super.getTopRoundness();
     }
 
     @Override
-    public float getCurrentBottomRoundness() {
+    public float getBottomRoundness() {
         if (mExpandAnimationRunning) {
             return mBottomRoundnessDuringLaunchAnimation;
         }
 
-        return super.getCurrentBottomRoundness();
+        return super.getBottomRoundness();
     }
 
     public void setExpandAnimationRunning(boolean expandAnimationRunning) {
@@ -2284,7 +2341,7 @@
     /**
      * Set this notification to be expanded by the user
      *
-     * @param userExpanded whether the user wants this notification to be expanded
+     * @param userExpanded        whether the user wants this notification to be expanded
      * @param allowChildExpansion whether a call to this method allows expanding children
      */
     public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
@@ -2434,7 +2491,7 @@
 
     /**
      * @return {@code true} if the notification can show it's heads up layout. This is mostly true
-     *         except for legacy use cases.
+     * except for legacy use cases.
      */
     public boolean canShowHeadsUp() {
         if (mOnKeyguard && !isDozing() && !isBypassEnabled()) {
@@ -2625,7 +2682,7 @@
 
     @Override
     public void setHideSensitive(boolean hideSensitive, boolean animated, long delay,
-            long duration) {
+                                 long duration) {
         if (getVisibility() == GONE) {
             // If we are GONE, the hideSensitive parameter will not be calculated and always be
             // false, which is incorrect, let's wait until a real call comes in later.
@@ -2658,9 +2715,9 @@
 
     private void animateShowingPublic(long delay, long duration, boolean showingPublic) {
         View[] privateViews = mIsSummaryWithChildren
-                ? new View[] {mChildrenContainer}
-                : new View[] {mPrivateLayout};
-        View[] publicViews = new View[] {mPublicLayout};
+                ? new View[]{mChildrenContainer}
+                : new View[]{mPrivateLayout};
+        View[] publicViews = new View[]{mPublicLayout};
         View[] hiddenChildren = showingPublic ? privateViews : publicViews;
         View[] shownChildren = showingPublic ? publicViews : privateViews;
         for (final View hiddenView : hiddenChildren) {
@@ -2693,8 +2750,8 @@
 
     /**
      * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as
-     *         otherwise some state might not be updated. To request about the general clearability
-     *         see {@link NotificationEntry#isDismissable()}.
+     * otherwise some state might not be updated. To request about the general clearability
+     * see {@link NotificationEntry#isDismissable()}.
      */
     public boolean canViewBeDismissed() {
         return mEntry.isDismissable() && (!shouldShowPublic() || !mSensitiveHiddenInGeneral);
@@ -2777,8 +2834,13 @@
     }
 
     @Override
-    public long performRemoveAnimation(long duration, long delay, float translationDirection,
-            boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable,
+    public long performRemoveAnimation(
+            long duration,
+            long delay,
+            float translationDirection,
+            boolean isHeadsUpAnimation,
+            float endLocation,
+            Runnable onFinishedRunnable,
             AnimatorListenerAdapter animationListener) {
         if (mMenuRow != null && mMenuRow.isMenuVisible()) {
             Animator anim = getTranslateViewAnimator(0f, null /* listener */);
@@ -2828,7 +2890,9 @@
         }
     }
 
-    /** Gets the last value set with {@link #setNotificationFaded(boolean)} */
+    /**
+     * Gets the last value set with {@link #setNotificationFaded(boolean)}
+     */
     @Override
     public boolean isNotificationFaded() {
         return mIsFaded;
@@ -2843,7 +2907,7 @@
      * notifications return false from {@link #hasOverlappingRendering()} and delegate the
      * layerType to child views which really need it in order to render correctly, such as icon
      * views or the conversation face pile.
-     *
+     * <p>
      * Another compounding factor for notifications is that we change clipping on each frame of the
      * animation, so the hardware layer isn't able to do any caching at the top level, but the
      * individual elements we render with hardware layers (e.g. icons) cache wonderfully because we
@@ -2869,7 +2933,9 @@
         }
     }
 
-    /** Private helper for iterating over the layouts and children containers to set faded state */
+    /**
+     * Private helper for iterating over the layouts and children containers to set faded state
+     */
     private void setNotificationFadedOnChildren(boolean faded) {
         delegateNotificationFaded(mChildrenContainer, faded);
         for (NotificationContentView layout : mLayouts) {
@@ -2897,7 +2963,7 @@
      * Because RemoteInputView is designed to be an opaque view that overlaps the Actions row, the
      * row should require overlapping rendering to ensure that the overlapped view doesn't bleed
      * through when alpha fading.
-     *
+     * <p>
      * Note that this currently works for top-level notifications which squish their height down
      * while collapsing the shade, but does not work for children inside groups, because the
      * accordion affect does not apply to those views, so super.hasOverlappingRendering() will
@@ -2976,7 +3042,7 @@
             return mGuts.getIntrinsicHeight();
         } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp
                 && mHeadsUpManager.isTrackingHeadsUp()) {
-                return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
+            return getPinnedHeadsUpHeight(false /* atLeastMinHeight */);
         } else if (mIsSummaryWithChildren && !isGroupExpanded() && !shouldShowPublic()) {
             return mChildrenContainer.getMinHeight();
         } else if (!ignoreTemporaryStates && canShowHeadsUp() && mIsHeadsUp) {
@@ -3218,8 +3284,8 @@
             MenuItem snoozeMenu = provider.getSnoozeMenuItem(getContext());
             if (snoozeMenu != null) {
                 AccessibilityAction action = new AccessibilityAction(R.id.action_snooze,
-                    getContext().getResources()
-                        .getString(R.string.notification_menu_snooze_action));
+                        getContext().getResources()
+                                .getString(R.string.notification_menu_snooze_action));
                 info.addAction(action);
             }
         }
@@ -3280,17 +3346,17 @@
             NotificationContentView contentView = (NotificationContentView) child;
             if (isClippingNeeded()) {
                 return true;
-            } else if (!hasNoRounding()
-                    && contentView.shouldClipToRounding(getCurrentTopRoundness() != 0.0f,
-                    getCurrentBottomRoundness() != 0.0f)) {
+            } else if (hasRoundedCorner()
+                    && contentView.shouldClipToRounding(getTopRoundness() != 0.0f,
+                    getBottomRoundness() != 0.0f)) {
                 return true;
             }
         } else if (child == mChildrenContainer) {
-            if (isClippingNeeded() || !hasNoRounding()) {
+            if (isClippingNeeded() || hasRoundedCorner()) {
                 return true;
             }
         } else if (child instanceof NotificationGuts) {
-            return !hasNoRounding();
+            return hasRoundedCorner();
         }
         return super.childNeedsClipping(child);
     }
@@ -3316,14 +3382,17 @@
     }
 
     @Override
-    protected void applyRoundness() {
+    public void applyRoundness() {
         super.applyRoundness();
         applyChildrenRoundness();
     }
 
     private void applyChildrenRoundness() {
         if (mIsSummaryWithChildren) {
-            mChildrenContainer.setCurrentBottomRoundness(getCurrentBottomRoundness());
+            mChildrenContainer.requestBottomRoundness(
+                    getBottomRoundness(),
+                    /* animate = */ false,
+                    SourceType.DefaultValue);
         }
     }
 
@@ -3335,10 +3404,6 @@
         return super.getCustomClipPath(child);
     }
 
-    private boolean hasNoRounding() {
-        return getCurrentBottomRoundness() == 0.0f && getCurrentTopRoundness() == 0.0f;
-    }
-
     public boolean isMediaRow() {
         return mEntry.getSbn().getNotification().isMediaNotification();
     }
@@ -3434,6 +3499,7 @@
     public interface LongPressListener {
         /**
          * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
+         *
          * @return whether the longpress was handled
          */
         boolean onLongPress(View v, int x, int y, MenuItem item);
@@ -3455,6 +3521,7 @@
     public interface CoordinateOnClickListener {
         /**
          * Equivalent to {@link View.OnClickListener#onClick(View)} with coordinates
+         *
          * @return whether the click was handled
          */
         boolean onClick(View v, int x, int y, MenuItem item);
@@ -3511,7 +3578,19 @@
     private void setTargetPoint(Point p) {
         mTargetPoint = p;
     }
+
     public Point getTargetPoint() {
         return mTargetPoint;
     }
+
+    /**
+     * Enable the support for rounded corner in notification group
+     * @param enabled true if is supported
+     */
+    public void enableNotificationGroupCorner(boolean enabled) {
+        mIsNotificationGroupCornerEnabled = enabled;
+        if (mChildrenContainer != null) {
+            mChildrenContainer.enableNotificationGroupCorner(mIsNotificationGroupCornerEnabled);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index a493a67..842526e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -231,6 +231,8 @@
                 mStatusBarStateController.removeCallback(mStatusBarStateListener);
             }
         });
+        mView.enableNotificationGroupCorner(
+                mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER));
     }
 
     private final StatusBarStateController.StateListener mStatusBarStateListener =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index d58fe3b..4fde5d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,46 +28,21 @@
 import android.view.ViewOutlineProvider;
 
 import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.AnimatableProperty;
-import com.android.systemui.statusbar.notification.PropertyAnimator;
-import com.android.systemui.statusbar.notification.stack.AnimationProperties;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
+import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 
 /**
  * Like {@link ExpandableView}, but setting an outline for the height and clipping.
  */
 public abstract class ExpandableOutlineView extends ExpandableView {
 
-    private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from(
-            "topRoundness",
-            ExpandableOutlineView::setTopRoundnessInternal,
-            ExpandableOutlineView::getCurrentTopRoundness,
-            R.id.top_roundess_animator_tag,
-            R.id.top_roundess_animator_end_tag,
-            R.id.top_roundess_animator_start_tag);
-    private static final AnimatableProperty BOTTOM_ROUNDNESS = AnimatableProperty.from(
-            "bottomRoundness",
-            ExpandableOutlineView::setBottomRoundnessInternal,
-            ExpandableOutlineView::getCurrentBottomRoundness,
-            R.id.bottom_roundess_animator_tag,
-            R.id.bottom_roundess_animator_end_tag,
-            R.id.bottom_roundess_animator_start_tag);
-    private static final AnimationProperties ROUNDNESS_PROPERTIES =
-            new AnimationProperties().setDuration(
-                    StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS);
+    private RoundableState mRoundableState;
     private static final Path EMPTY_PATH = new Path();
-
     private final Rect mOutlineRect = new Rect();
-    private final Path mClipPath = new Path();
     private boolean mCustomOutline;
     private float mOutlineAlpha = -1f;
-    protected float mOutlineRadius;
     private boolean mAlwaysRoundBothCorners;
     private Path mTmpPath = new Path();
-    private float mCurrentBottomRoundness;
-    private float mCurrentTopRoundness;
-    private float mBottomRoundness;
-    private float mTopRoundness;
     private int mBackgroundTop;
 
     /**
@@ -80,8 +55,7 @@
     private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
         @Override
         public void getOutline(View view, Outline outline) {
-            if (!mCustomOutline && getCurrentTopRoundness() == 0.0f
-                    && getCurrentBottomRoundness() == 0.0f && !mAlwaysRoundBothCorners) {
+            if (!mCustomOutline && !hasRoundedCorner() && !mAlwaysRoundBothCorners) {
                 // Only when translating just the contents, does the outline need to be shifted.
                 int translation = !mDismissUsingRowTranslationX ? (int) getTranslation() : 0;
                 int left = Math.max(translation, 0);
@@ -99,14 +73,18 @@
         }
     };
 
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     protected Path getClipPath(boolean ignoreTranslation) {
         int left;
         int top;
         int right;
         int bottom;
         int height;
-        float topRoundness = mAlwaysRoundBothCorners
-                ? mOutlineRadius : getCurrentBackgroundRadiusTop();
+        float topRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getTopCornerRadius();
         if (!mCustomOutline) {
             // The outline just needs to be shifted if we're translating the contents. Otherwise
             // it's already in the right place.
@@ -130,12 +108,11 @@
         if (height == 0) {
             return EMPTY_PATH;
         }
-        float bottomRoundness = mAlwaysRoundBothCorners
-                ? mOutlineRadius : getCurrentBackgroundRadiusBottom();
+        float bottomRoundness = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
         if (topRoundness + bottomRoundness > height) {
             float overShoot = topRoundness + bottomRoundness - height;
-            float currentTopRoundness = getCurrentTopRoundness();
-            float currentBottomRoundness = getCurrentBottomRoundness();
+            float currentTopRoundness = getTopRoundness();
+            float currentBottomRoundness = getBottomRoundness();
             topRoundness -= overShoot * currentTopRoundness
                     / (currentTopRoundness + currentBottomRoundness);
             bottomRoundness -= overShoot * currentBottomRoundness
@@ -145,8 +122,18 @@
         return mTmpPath;
     }
 
-    public void getRoundedRectPath(int left, int top, int right, int bottom,
-            float topRoundness, float bottomRoundness, Path outPath) {
+    /**
+     * Add a round rect in {@code outPath}
+     * @param outPath destination path
+     */
+    public void getRoundedRectPath(
+            int left,
+            int top,
+            int right,
+            int bottom,
+            float topRoundness,
+            float bottomRoundness,
+            Path outPath) {
         outPath.reset();
         mTmpCornerRadii[0] = topRoundness;
         mTmpCornerRadii[1] = topRoundness;
@@ -168,15 +155,28 @@
     @Override
     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
         canvas.save();
+        Path clipPath = null;
+        Path childClipPath = null;
         if (childNeedsClipping(child)) {
-            Path clipPath = getCustomClipPath(child);
+            clipPath = getCustomClipPath(child);
             if (clipPath == null) {
                 clipPath = getClipPath(false /* ignoreTranslation */);
             }
-            if (clipPath != null) {
-                canvas.clipPath(clipPath);
+            // If the notification uses "RowTranslationX" as dismiss behavior, we should clip the
+            // children instead.
+            if (mDismissUsingRowTranslationX && child instanceof NotificationChildrenContainer) {
+                childClipPath = clipPath;
+                clipPath = null;
             }
         }
+
+        if (child instanceof NotificationChildrenContainer) {
+            ((NotificationChildrenContainer) child).setChildClipPath(childClipPath);
+        }
+        if (clipPath != null) {
+            canvas.clipPath(clipPath);
+        }
+
         boolean result = super.drawChild(canvas, child, drawingTime);
         canvas.restore();
         return result;
@@ -207,73 +207,21 @@
 
     private void initDimens() {
         Resources res = getResources();
-        mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius);
         mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline);
-        if (!mAlwaysRoundBothCorners) {
-            mOutlineRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
+        float maxRadius;
+        if (mAlwaysRoundBothCorners) {
+            maxRadius = res.getDimension(R.dimen.notification_shadow_radius);
+        } else {
+            maxRadius = res.getDimensionPixelSize(R.dimen.notification_corner_radius);
         }
+        mRoundableState = new RoundableState(this, this, maxRadius);
         setClipToOutline(mAlwaysRoundBothCorners);
     }
 
     @Override
-    public boolean setTopRoundness(float topRoundness, boolean animate) {
-        if (mTopRoundness != topRoundness) {
-            float diff = Math.abs(topRoundness - mTopRoundness);
-            mTopRoundness = topRoundness;
-            boolean shouldAnimate = animate;
-            if (PropertyAnimator.isAnimating(this, TOP_ROUNDNESS) && diff > 0.5f) {
-                // Fail safe:
-                // when we've been animating previously and we're now getting an update in the
-                // other direction, make sure to animate it too, otherwise, the localized updating
-                // may make the start larger than 1.0.
-                shouldAnimate = true;
-            }
-            PropertyAnimator.setProperty(this, TOP_ROUNDNESS, topRoundness,
-                    ROUNDNESS_PROPERTIES, shouldAnimate);
-            return true;
-        }
-        return false;
-    }
-
-    protected void applyRoundness() {
+    public void applyRoundness() {
         invalidateOutline();
-        invalidate();
-    }
-
-    public float getCurrentBackgroundRadiusTop() {
-        return getCurrentTopRoundness() * mOutlineRadius;
-    }
-
-    public float getCurrentTopRoundness() {
-        return mCurrentTopRoundness;
-    }
-
-    public float getCurrentBottomRoundness() {
-        return mCurrentBottomRoundness;
-    }
-
-    public float getCurrentBackgroundRadiusBottom() {
-        return getCurrentBottomRoundness() * mOutlineRadius;
-    }
-
-    @Override
-    public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
-        if (mBottomRoundness != bottomRoundness) {
-            float diff = Math.abs(bottomRoundness - mBottomRoundness);
-            mBottomRoundness = bottomRoundness;
-            boolean shouldAnimate = animate;
-            if (PropertyAnimator.isAnimating(this, BOTTOM_ROUNDNESS) && diff > 0.5f) {
-                // Fail safe:
-                // when we've been animating previously and we're now getting an update in the
-                // other direction, make sure to animate it too, otherwise, the localized updating
-                // may make the start larger than 1.0.
-                shouldAnimate = true;
-            }
-            PropertyAnimator.setProperty(this, BOTTOM_ROUNDNESS, bottomRoundness,
-                    ROUNDNESS_PROPERTIES, shouldAnimate);
-            return true;
-        }
-        return false;
+        super.applyRoundness();
     }
 
     protected void setBackgroundTop(int backgroundTop) {
@@ -283,16 +231,6 @@
         }
     }
 
-    private void setTopRoundnessInternal(float topRoundness) {
-        mCurrentTopRoundness = topRoundness;
-        applyRoundness();
-    }
-
-    private void setBottomRoundnessInternal(float bottomRoundness) {
-        mCurrentBottomRoundness = bottomRoundness;
-        applyRoundness();
-    }
-
     public void onDensityOrFontScaleChanged() {
         initDimens();
         applyRoundness();
@@ -348,9 +286,10 @@
 
     /**
      * Set the dismiss behavior of the view.
+     *
      * @param usingRowTranslationX {@code true} if the view should translate using regular
-     *                                          translationX, otherwise the contents will be
-     *                                          translated.
+     *                             translationX, otherwise the contents will be
+     *                             translated.
      */
     public void setDismissUsingRowTranslationX(boolean usingRowTranslationX) {
         mDismissUsingRowTranslationX = usingRowTranslationX;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 38f0c55..955d7c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -36,6 +36,8 @@
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
 import com.android.systemui.util.DumpUtilsKt;
@@ -47,9 +49,10 @@
 /**
  * An abstract view for expandable views.
  */
-public abstract class ExpandableView extends FrameLayout implements Dumpable {
+public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable {
     private static final String TAG = "ExpandableView";
 
+    private RoundableState mRoundableState = null;
     protected OnHeightChangedListener mOnHeightChangedListener;
     private int mActualHeight;
     protected int mClipTopAmount;
@@ -78,6 +81,14 @@
         initDimens();
     }
 
+    @Override
+    public RoundableState getRoundableState() {
+        if (mRoundableState == null) {
+            mRoundableState = new RoundableState(this, this, 0f);
+        }
+        return mRoundableState;
+    }
+
     private void initDimens() {
         mContentShift = getResources().getDimensionPixelSize(
                 R.dimen.shelf_transform_content_shift);
@@ -440,8 +451,7 @@
             int top = getClipTopAmount();
             int bottom = Math.max(Math.max(getActualHeight() + getExtraBottomPadding()
                     - mClipBottomAmount, top), mMinimumHeightForClipping);
-            int halfExtraWidth = (int) (mExtraWidthForClipping / 2.0f);
-            mClipRect.set(-halfExtraWidth, top, getWidth() + halfExtraWidth, bottom);
+            mClipRect.set(Integer.MIN_VALUE, top, Integer.MAX_VALUE, bottom);
             setClipBounds(mClipRect);
         } else {
             setClipBounds(null);
@@ -455,7 +465,6 @@
 
     public void setExtraWidthForClipping(float extraWidthForClipping) {
         mExtraWidthForClipping = extraWidthForClipping;
-        updateClipping();
     }
 
     public float getHeaderVisibleAmount() {
@@ -844,22 +853,6 @@
         return mFirstInSection;
     }
 
-    /**
-     * Set the topRoundness of this view.
-     * @return Whether the roundness was changed.
-     */
-    public boolean setTopRoundness(float topRoundness, boolean animate) {
-        return false;
-    }
-
-    /**
-     * Set the bottom roundness of this view.
-     * @return Whether the roundness was changed.
-     */
-    public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
-        return false;
-    }
-
     public int getHeadsUpHeightWithoutHeader() {
         return getHeight();
     }
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/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index 7a65436..f13e48d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -35,12 +35,15 @@
 
 import com.android.internal.widget.CachingIconView;
 import com.android.internal.widget.NotificationExpandButton;
+import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.statusbar.TransformableView;
 import com.android.systemui.statusbar.ViewTransformationHelper;
 import com.android.systemui.statusbar.notification.CustomInterpolatorTransformation;
 import com.android.systemui.statusbar.notification.FeedbackIcon;
 import com.android.systemui.statusbar.notification.ImageTransformState;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.TransformState;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 
@@ -49,13 +52,12 @@
 /**
  * Wraps a notification view which may or may not include a header.
  */
-public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
+public class NotificationHeaderViewWrapper extends NotificationViewWrapper implements Roundable {
 
+    private final RoundableState mRoundableState;
     private static final Interpolator LOW_PRIORITY_HEADER_CLOSE
             = new PathInterpolator(0.4f, 0f, 0.7f, 1f);
-
     protected final ViewTransformationHelper mTransformationHelper;
-
     private CachingIconView mIcon;
     private NotificationExpandButton mExpandButton;
     private View mAltExpandTarget;
@@ -67,12 +69,16 @@
     private ImageView mWorkProfileImage;
     private View mAudiblyAlertedIcon;
     private View mFeedbackIcon;
-
     private boolean mIsLowPriority;
     private boolean mTransformLowPriorityTitle;
 
     protected NotificationHeaderViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
         super(ctx, view, row);
+        mRoundableState = new RoundableState(
+                mView,
+                this,
+                ctx.getResources().getDimension(R.dimen.notification_corner_radius)
+        );
         mTransformationHelper = new ViewTransformationHelper();
 
         // we want to avoid that the header clashes with the other text when transforming
@@ -81,7 +87,8 @@
                 new CustomInterpolatorTransformation(TRANSFORMING_VIEW_TITLE) {
 
                     @Override
-                    public Interpolator getCustomInterpolator(int interpolationType,
+                    public Interpolator getCustomInterpolator(
+                            int interpolationType,
                             boolean isFrom) {
                         boolean isLowPriority = mView instanceof NotificationHeaderView;
                         if (interpolationType == TRANSFORM_Y) {
@@ -99,11 +106,17 @@
                     protected boolean hasCustomTransformation() {
                         return mIsLowPriority && mTransformLowPriorityTitle;
                     }
-                }, TRANSFORMING_VIEW_TITLE);
+                },
+                TRANSFORMING_VIEW_TITLE);
         resolveHeaderViews();
         addFeedbackOnClickListener(row);
     }
 
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     protected void resolveHeaderViews() {
         mIcon = mView.findViewById(com.android.internal.R.id.icon);
         mHeaderText = mView.findViewById(com.android.internal.R.id.header_text);
@@ -128,7 +141,9 @@
         }
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     @Override
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mFeedbackIcon != null) {
@@ -193,7 +208,7 @@
                     // its animation
                     && child.getId() != com.android.internal.R.id.conversation_icon_badge_ring) {
                 ((ImageView) child).setCropToPadding(true);
-            } else if (child instanceof ViewGroup){
+            } else if (child instanceof ViewGroup) {
                 ViewGroup group = (ViewGroup) child;
                 for (int i = 0; i < group.getChildCount(); i++) {
                     stack.push(group.getChildAt(i));
@@ -215,7 +230,9 @@
     }
 
     @Override
-    public void updateExpandability(boolean expandable, View.OnClickListener onClickListener,
+    public void updateExpandability(
+            boolean expandable,
+            View.OnClickListener onClickListener,
             boolean requestLayout) {
         mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
         mExpandButton.setOnClickListener(expandable ? onClickListener : null);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 7b23a56..26f0ad9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -21,6 +21,9 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.Path.Direction;
 import android.graphics.drawable.ColorDrawable;
 import android.service.notification.StatusBarNotification;
 import android.util.AttributeSet;
@@ -33,6 +36,7 @@
 import android.widget.RemoteViews;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -43,10 +47,14 @@
 import com.android.systemui.statusbar.notification.FeedbackIcon;
 import com.android.systemui.statusbar.notification.NotificationFadeAware;
 import com.android.systemui.statusbar.notification.NotificationUtils;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.RoundableState;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.HybridGroupManager;
 import com.android.systemui.statusbar.notification.row.HybridNotificationView;
+import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
 
 import java.util.ArrayList;
@@ -56,7 +64,7 @@
  * A container containing child notifications
  */
 public class NotificationChildrenContainer extends ViewGroup
-        implements NotificationFadeAware {
+        implements NotificationFadeAware, Roundable {
 
     private static final String TAG = "NotificationChildrenContainer";
 
@@ -100,9 +108,9 @@
     private boolean mEnableShadowOnChildNotifications;
 
     private NotificationHeaderView mNotificationHeader;
-    private NotificationViewWrapper mNotificationHeaderWrapper;
+    private NotificationHeaderViewWrapper mNotificationHeaderWrapper;
     private NotificationHeaderView mNotificationHeaderLowPriority;
-    private NotificationViewWrapper mNotificationHeaderWrapperLowPriority;
+    private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority;
     private NotificationGroupingUtil mGroupingUtil;
     private ViewState mHeaderViewState;
     private int mClipBottomAmount;
@@ -110,7 +118,8 @@
     private OnClickListener mHeaderClickListener;
     private ViewGroup mCurrentHeader;
     private boolean mIsConversation;
-
+    private Path mChildClipPath = null;
+    private final Path mHeaderPath = new Path();
     private boolean mShowGroupCountInExpander;
     private boolean mShowDividersWhenExpanded;
     private boolean mHideDividersDuringExpand;
@@ -119,6 +128,8 @@
     private float mHeaderVisibleAmount = 1.0f;
     private int mUntruncatedChildCount;
     private boolean mContainingNotificationIsFaded = false;
+    private RoundableState mRoundableState;
+    private boolean mIsNotificationGroupCornerEnabled;
 
     public NotificationChildrenContainer(Context context) {
         this(context, null);
@@ -132,10 +143,14 @@
         this(context, attrs, defStyleAttr, 0);
     }
 
-    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
+    public NotificationChildrenContainer(
+            Context context,
+            AttributeSet attrs,
+            int defStyleAttr,
             int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
         mHybridGroupManager = new HybridGroupManager(getContext());
+        mRoundableState = new RoundableState(this, this, 0f);
         initDimens();
         setClipChildren(false);
     }
@@ -167,6 +182,12 @@
         mHybridGroupManager.initDimens();
     }
 
+    @NonNull
+    @Override
+    public RoundableState getRoundableState() {
+        return mRoundableState;
+    }
+
     @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         int childCount =
@@ -271,7 +292,7 @@
     /**
      * Add a child notification to this view.
      *
-     * @param row the row to add
+     * @param row        the row to add
      * @param childIndex the index to add it at, if -1 it will be added at the end
      */
     public void addNotification(ExpandableNotificationRow row, int childIndex) {
@@ -347,8 +368,11 @@
             mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
                     .setVisibility(VISIBLE);
             mNotificationHeader.setOnClickListener(mHeaderClickListener);
-            mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
-                    mNotificationHeader, mContainingNotification);
+            mNotificationHeaderWrapper =
+                    (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
+                            getContext(),
+                            mNotificationHeader,
+                            mContainingNotification);
             addView(mNotificationHeader, 0);
             invalidate();
         } else {
@@ -381,8 +405,11 @@
                 mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
                         .setVisibility(VISIBLE);
                 mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
-                mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(),
-                        mNotificationHeaderLowPriority, mContainingNotification);
+                mNotificationHeaderWrapperLowPriority =
+                        (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
+                                getContext(),
+                                mNotificationHeaderLowPriority,
+                                mContainingNotification);
                 addView(mNotificationHeaderLowPriority, 0);
                 invalidate();
             } else {
@@ -461,7 +488,9 @@
         return mAttachedChildren;
     }
 
-    /** To be called any time the rows have been updated */
+    /**
+     * To be called any time the rows have been updated
+     */
     public void updateExpansionStates() {
         if (mChildrenExpanded || mUserLocked) {
             // we don't modify it the group is expanded or if we are expanding it
@@ -475,7 +504,6 @@
     }
 
     /**
-     *
      * @return the intrinsic size of this children container, i.e the natural fully expanded state
      */
     public int getIntrinsicHeight() {
@@ -485,7 +513,7 @@
 
     /**
      * @return the intrinsic height with a number of children given
-     *         in @param maxAllowedVisibleChildren
+     * in @param maxAllowedVisibleChildren
      */
     private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
         if (showingAsLowPriority()) {
@@ -539,7 +567,8 @@
 
     /**
      * Update the state of all its children based on a linear layout algorithm.
-     * @param parentState the state of the parent
+     *
+     * @param parentState  the state of the parent
      * @param ambientState the ambient state containing ambient information
      */
     public void updateState(ExpandableViewState parentState, AmbientState ambientState) {
@@ -655,14 +684,17 @@
      * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
      * height, children in the group after this are gone.
      *
-     * @param child the child who's height to adjust.
+     * @param child        the child who's height to adjust.
      * @param parentHeight the height of the parent.
-     * @param childState the state to update.
-     * @param yPosition the yPosition of the view.
+     * @param childState   the state to update.
+     * @param yPosition    the yPosition of the view.
      * @return true if children after this one should be hidden.
      */
-    private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
-            int parentHeight, ExpandableViewState childState, int yPosition) {
+    private boolean updateChildStateForExpandedGroup(
+            ExpandableNotificationRow child,
+            int parentHeight,
+            ExpandableViewState childState,
+            int yPosition) {
         final int top = yPosition + child.getClipTopAmount();
         final int intrinsicHeight = child.getIntrinsicHeight();
         final int bottom = top + intrinsicHeight;
@@ -690,13 +722,15 @@
         if (mIsLowPriority
                 || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
                 || (mContainingNotification.isHeadsUpState()
-                        && mContainingNotification.canShowHeadsUp())) {
+                && mContainingNotification.canShowHeadsUp())) {
             return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
         }
         return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
     }
 
-    /** Applies state to children. */
+    /**
+     * Applies state to children.
+     */
     public void applyState() {
         int childCount = mAttachedChildren.size();
         ViewState tmpState = new ViewState();
@@ -768,17 +802,73 @@
         }
     }
 
+    @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        boolean isCanvasChanged = false;
+
+        Path clipPath = mChildClipPath;
+        if (clipPath != null) {
+            final float translation;
+            if (child instanceof ExpandableNotificationRow) {
+                ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child;
+                translation = notificationRow.getTranslation();
+            } else {
+                translation = child.getTranslationX();
+            }
+
+            isCanvasChanged = true;
+            canvas.save();
+            if (mIsNotificationGroupCornerEnabled && translation != 0f) {
+                clipPath.offset(translation, 0f);
+                canvas.clipPath(clipPath);
+                clipPath.offset(-translation, 0f);
+            } else {
+                canvas.clipPath(clipPath);
+            }
+        }
+
+        if (child instanceof NotificationHeaderView
+                && mNotificationHeaderWrapper.hasRoundedCorner()) {
+            float[] radii = mNotificationHeaderWrapper.getUpdatedRadii();
+            mHeaderPath.reset();
+            mHeaderPath.addRoundRect(
+                    child.getLeft(),
+                    child.getTop(),
+                    child.getRight(),
+                    child.getBottom(),
+                    radii,
+                    Direction.CW
+            );
+            if (!isCanvasChanged) {
+                isCanvasChanged = true;
+                canvas.save();
+            }
+            canvas.clipPath(mHeaderPath);
+        }
+
+        if (isCanvasChanged) {
+            boolean result = super.drawChild(canvas, child, drawingTime);
+            canvas.restore();
+            return result;
+        } else {
+            // If there have been no changes to the canvas we can proceed as usual
+            return super.drawChild(canvas, child, drawingTime);
+        }
+    }
+
+
     /**
      * This is called when the children expansion has changed and positions the children properly
      * for an appear animation.
-     *
      */
     public void prepareExpansionChanged() {
         // TODO: do something that makes sense, like placing the invisible views correctly
         return;
     }
 
-    /** Animate to a given state. */
+    /**
+     * Animate to a given state.
+     */
     public void startAnimationToState(AnimationProperties properties) {
         int childCount = mAttachedChildren.size();
         ViewState tmpState = new ViewState();
@@ -1102,7 +1192,8 @@
      * Get the minimum Height for this group.
      *
      * @param maxAllowedVisibleChildren the number of children that should be visible
-     * @param likeHighPriority if the height should be calculated as if it were not low priority
+     * @param likeHighPriority          if the height should be calculated as if it were not low
+     *                                  priority
      */
     private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
         return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation);
@@ -1112,10 +1203,13 @@
      * Get the minimum Height for this group.
      *
      * @param maxAllowedVisibleChildren the number of children that should be visible
-     * @param likeHighPriority if the height should be calculated as if it were not low priority
-     * @param headerTranslation the translation amount of the header
+     * @param likeHighPriority          if the height should be calculated as if it were not low
+     *                                  priority
+     * @param headerTranslation         the translation amount of the header
      */
-    private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority,
+    private int getMinHeight(
+            int maxAllowedVisibleChildren,
+            boolean likeHighPriority,
             int headerTranslation) {
         if (!likeHighPriority && showingAsLowPriority()) {
             if (mNotificationHeaderLowPriority == null) {
@@ -1274,16 +1368,19 @@
         return mUserLocked;
     }
 
-    public void setCurrentBottomRoundness(float currentBottomRoundness) {
+    @Override
+    public void applyRoundness() {
+        Roundable.super.applyRoundness();
         boolean last = true;
         for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
             ExpandableNotificationRow child = mAttachedChildren.get(i);
             if (child.getVisibility() == View.GONE) {
                 continue;
             }
-            float bottomRoundness = last ? currentBottomRoundness : 0.0f;
-            child.setBottomRoundness(bottomRoundness, isShown() /* animate */);
-            child.setTopRoundness(0.0f, false /* animate */);
+            child.requestBottomRoundness(
+                    last ? getBottomRoundness() : 0f,
+                    /* animate = */ isShown(),
+                    SourceType.DefaultValue);
             last = false;
         }
     }
@@ -1293,7 +1390,9 @@
         mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader);
     }
 
-    /** Shows the given feedback icon, or hides the icon if null. */
+    /**
+     * Shows the given feedback icon, or hides the icon if null.
+     */
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         if (mNotificationHeaderWrapper != null) {
             mNotificationHeaderWrapper.setFeedbackIcon(icon);
@@ -1325,4 +1424,26 @@
             child.setNotificationFaded(faded);
         }
     }
+
+    /**
+     * Allow to define a path the clip the children in #drawChild()
+     *
+     * @param childClipPath path used to clip the children
+     */
+    public void setChildClipPath(@Nullable Path childClipPath) {
+        mChildClipPath = childClipPath;
+        invalidate();
+    }
+
+    public NotificationHeaderViewWrapper getNotificationHeaderWrapper() {
+        return mNotificationHeaderWrapper;
+    }
+
+    /**
+     * Enable the support for rounded corner in notification group
+     * @param enabled true if is supported
+     */
+    public void enableNotificationGroupCorner(boolean enabled) {
+        mIsNotificationGroupCornerEnabled = enabled;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
index 2015c87..6810055 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManager.java
@@ -26,6 +26,8 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
+import com.android.systemui.statusbar.notification.Roundable;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.logging.NotificationRoundnessLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -59,8 +61,8 @@
     private boolean mIsClearAllInProgress;
 
     private ExpandableView mSwipedView = null;
-    private ExpandableView mViewBeforeSwipedView = null;
-    private ExpandableView mViewAfterSwipedView = null;
+    private Roundable mViewBeforeSwipedView = null;
+    private Roundable mViewAfterSwipedView = null;
 
     @Inject
     NotificationRoundnessManager(
@@ -101,11 +103,12 @@
     public boolean isViewAffectedBySwipe(ExpandableView expandableView) {
         return expandableView != null
                 && (expandableView == mSwipedView
-                    || expandableView == mViewBeforeSwipedView
-                    || expandableView == mViewAfterSwipedView);
+                || expandableView == mViewBeforeSwipedView
+                || expandableView == mViewAfterSwipedView);
     }
 
-    boolean updateViewWithoutCallback(ExpandableView view,
+    boolean updateViewWithoutCallback(
+            ExpandableView view,
             boolean animate) {
         if (view == null
                 || view == mViewBeforeSwipedView
@@ -113,11 +116,15 @@
             return false;
         }
 
-        final float topRoundness = getRoundnessFraction(view, true /* top */);
-        final float bottomRoundness = getRoundnessFraction(view, false /* top */);
+        final boolean isTopChanged = view.requestTopRoundness(
+                getRoundnessDefaultValue(view, true /* top */),
+                animate,
+                SourceType.DefaultValue);
 
-        final boolean topChanged = view.setTopRoundness(topRoundness, animate);
-        final boolean bottomChanged = view.setBottomRoundness(bottomRoundness, animate);
+        final boolean isBottomChanged = view.requestBottomRoundness(
+                getRoundnessDefaultValue(view, /* top = */ false),
+                animate,
+                SourceType.DefaultValue);
 
         final boolean isFirstInSection = isFirstInSection(view);
         final boolean isLastInSection = isLastInSection(view);
@@ -126,9 +133,9 @@
         view.setLastInSection(isLastInSection);
 
         mNotifLogger.onCornersUpdated(view, isFirstInSection,
-                isLastInSection, topChanged, bottomChanged);
+                isLastInSection, isTopChanged, isBottomChanged);
 
-        return (isFirstInSection || isLastInSection) && (topChanged || bottomChanged);
+        return (isFirstInSection || isLastInSection) && (isTopChanged || isBottomChanged);
     }
 
     private boolean isFirstInSection(ExpandableView view) {
@@ -150,42 +157,46 @@
     }
 
     void setViewsAffectedBySwipe(
-            ExpandableView viewBefore,
+            Roundable viewBefore,
             ExpandableView viewSwiped,
-            ExpandableView viewAfter) {
+            Roundable viewAfter) {
         final boolean animate = true;
+        final SourceType source = SourceType.OnDismissAnimation;
 
-        ExpandableView oldViewBefore = mViewBeforeSwipedView;
+        // This method requires you to change the roundness of the current View targets and reset
+        // the roundness of the old View targets (if any) to 0f.
+        // To avoid conflicts, it generates a set of old Views and removes the current Views
+        // from this set.
+        HashSet<Roundable> oldViews = new HashSet<>();
+        if (mViewBeforeSwipedView != null) oldViews.add(mViewBeforeSwipedView);
+        if (mSwipedView != null) oldViews.add(mSwipedView);
+        if (mViewAfterSwipedView != null) oldViews.add(mViewAfterSwipedView);
+
         mViewBeforeSwipedView = viewBefore;
-        if (oldViewBefore != null) {
-            final float bottomRoundness = getRoundnessFraction(oldViewBefore, false /* top */);
-            oldViewBefore.setBottomRoundness(bottomRoundness,  animate);
-        }
         if (viewBefore != null) {
-            viewBefore.setBottomRoundness(1f, animate);
+            oldViews.remove(viewBefore);
+            viewBefore.requestTopRoundness(0f, animate, source);
+            viewBefore.requestBottomRoundness(1f, animate, source);
         }
 
-        ExpandableView oldSwipedview = mSwipedView;
         mSwipedView = viewSwiped;
-        if (oldSwipedview != null) {
-            final float bottomRoundness = getRoundnessFraction(oldSwipedview, false /* top */);
-            final float topRoundness = getRoundnessFraction(oldSwipedview, true /* top */);
-            oldSwipedview.setTopRoundness(topRoundness, animate);
-            oldSwipedview.setBottomRoundness(bottomRoundness, animate);
-        }
         if (viewSwiped != null) {
-            viewSwiped.setTopRoundness(1f, animate);
-            viewSwiped.setBottomRoundness(1f, animate);
+            oldViews.remove(viewSwiped);
+            viewSwiped.requestTopRoundness(1f, animate, source);
+            viewSwiped.requestBottomRoundness(1f, animate, source);
         }
 
-        ExpandableView oldViewAfter = mViewAfterSwipedView;
         mViewAfterSwipedView = viewAfter;
-        if (oldViewAfter != null) {
-            final float topRoundness = getRoundnessFraction(oldViewAfter, true /* top */);
-            oldViewAfter.setTopRoundness(topRoundness, animate);
-        }
         if (viewAfter != null) {
-            viewAfter.setTopRoundness(1f, animate);
+            oldViews.remove(viewAfter);
+            viewAfter.requestTopRoundness(1f, animate, source);
+            viewAfter.requestBottomRoundness(0f, animate, source);
+        }
+
+        // After setting the current Views, reset the views that are still present in the set.
+        for (Roundable oldView : oldViews) {
+            oldView.requestTopRoundness(0f, animate, source);
+            oldView.requestBottomRoundness(0f, animate, source);
         }
     }
 
@@ -193,7 +204,7 @@
         mIsClearAllInProgress = isClearingAll;
     }
 
-    private float getRoundnessFraction(ExpandableView view, boolean top) {
+    private float getRoundnessDefaultValue(Roundable view, boolean top) {
         if (view == null) {
             return 0f;
         }
@@ -207,28 +218,35 @@
                 && mIsClearAllInProgress) {
             return 1.0f;
         }
-        if ((view.isPinned()
-                || (view.isHeadsUpAnimatingAway()) && !mExpanded)) {
-            return 1.0f;
-        }
-        if (isFirstInSection(view) && top) {
-            return 1.0f;
-        }
-        if (isLastInSection(view) && !top) {
-            return 1.0f;
-        }
+        if (view instanceof ExpandableView) {
+            ExpandableView expandableView = (ExpandableView) view;
+            if ((expandableView.isPinned()
+                    || (expandableView.isHeadsUpAnimatingAway()) && !mExpanded)) {
+                return 1.0f;
+            }
+            if (isFirstInSection(expandableView) && top) {
+                return 1.0f;
+            }
+            if (isLastInSection(expandableView) && !top) {
+                return 1.0f;
+            }
 
-        if (view == mTrackedHeadsUp) {
-            // If we're pushing up on a headsup the appear fraction is < 0 and it needs to still be
-            // rounded.
-            return MathUtils.saturate(1.0f - mAppearFraction);
+            if (view == mTrackedHeadsUp) {
+                // If we're pushing up on a headsup the appear fraction is < 0 and it needs to
+                // still be rounded.
+                return MathUtils.saturate(1.0f - mAppearFraction);
+            }
+            if (expandableView.showingPulsing() && mRoundForPulsingViews) {
+                return 1.0f;
+            }
+            if (expandableView.isChildInGroup()) {
+                return 0f;
+            }
+            final Resources resources = expandableView.getResources();
+            return resources.getDimension(R.dimen.notification_corner_radius_small)
+                    / resources.getDimension(R.dimen.notification_corner_radius);
         }
-        if (view.showingPulsing() && mRoundForPulsingViews) {
-            return 1.0f;
-        }
-        final Resources resources = view.getResources();
-        return resources.getDimension(R.dimen.notification_corner_radius_small)
-                / resources.getDimension(R.dimen.notification_corner_radius);
+        return 0f;
     }
 
     public void setExpanded(float expandedHeight, float appearFraction) {
@@ -258,8 +276,10 @@
         mNotifLogger.onSectionCornersUpdated(sections, anyChanged);
     }
 
-    private boolean handleRemovedOldViews(NotificationSection[] sections,
-            ExpandableView[] oldViews, boolean first) {
+    private boolean handleRemovedOldViews(
+            NotificationSection[] sections,
+            ExpandableView[] oldViews,
+            boolean first) {
         boolean anyChanged = false;
         for (ExpandableView oldView : oldViews) {
             if (oldView != null) {
@@ -289,8 +309,10 @@
         return anyChanged;
     }
 
-    private boolean handleAddedNewViews(NotificationSection[] sections,
-            ExpandableView[] oldViews, boolean first) {
+    private boolean handleAddedNewViews(
+            NotificationSection[] sections,
+            ExpandableView[] oldViews,
+            boolean first) {
         boolean anyChanged = false;
         for (NotificationSection section : sections) {
             ExpandableView newView =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 2272411..df705c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1188,7 +1188,7 @@
             return;
         }
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (mChildrenToAddAnimated.contains(child)) {
                 final int startingPosition = getPositionInLinearLayout(child);
                 final int childHeight = getIntrinsicHeight(child) + mPaddingBetweenElements;
@@ -1658,7 +1658,7 @@
         // find the view under the pointer, accounting for GONE views
         final int count = getChildCount();
         for (int childIdx = 0; childIdx < count; childIdx++) {
-            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
+            ExpandableView slidingChild = getChildAtIndex(childIdx);
             if (slidingChild.getVisibility() != VISIBLE
                     || (ignoreDecors && slidingChild instanceof StackScrollerDecorView)) {
                 continue;
@@ -1691,6 +1691,10 @@
         return null;
     }
 
+    private ExpandableView getChildAtIndex(int index) {
+        return (ExpandableView) getChildAt(index);
+    }
+
     public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
         getLocationOnScreen(mTempInt2);
         return getChildAtPosition(touchX - mTempInt2[0], touchY - mTempInt2[1]);
@@ -2276,7 +2280,7 @@
         int childCount = getChildCount();
         int count = 0;
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !child.willBeGone() && child != mShelf) {
                 count++;
             }
@@ -2496,7 +2500,7 @@
     private ExpandableView getLastChildWithBackground() {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
                 return child;
@@ -2509,7 +2513,7 @@
     private ExpandableView getFirstChildWithBackground() {
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
                 return child;
@@ -2523,7 +2527,7 @@
         ArrayList<ExpandableView> children = new ArrayList<>();
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE
                     && !(child instanceof StackScrollerDecorView)
                     && child != mShelf) {
@@ -2882,7 +2886,7 @@
         }
         int position = 0;
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             boolean notGone = child.getVisibility() != View.GONE;
             if (notGone && !child.hasNoContentHeight()) {
                 if (position != 0) {
@@ -2936,7 +2940,7 @@
         }
         mAmbientState.setLastVisibleBackgroundChild(lastChild);
         // TODO: Refactor SectionManager and put the RoundnessManager there.
-        mController.getNoticationRoundessManager().updateRoundedChildren(mSections);
+        mController.getNotificationRoundnessManager().updateRoundedChildren(mSections);
         mAnimateBottomOnLayout = false;
         invalidate();
     }
@@ -3968,7 +3972,7 @@
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     private void clearUserLockedViews() {
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child instanceof ExpandableNotificationRow) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
                 row.setUserLocked(false);
@@ -3981,7 +3985,7 @@
         // lets make sure nothing is transient anymore
         clearTemporaryViewsInGroup(this);
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child instanceof ExpandableNotificationRow) {
                 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
                 clearTemporaryViewsInGroup(row.getChildrenContainer());
@@ -4230,7 +4234,7 @@
         if (hideSensitive != mAmbientState.isHideSensitive()) {
             int childCount = getChildCount();
             for (int i = 0; i < childCount; i++) {
-                ExpandableView v = (ExpandableView) getChildAt(i);
+                ExpandableView v = getChildAtIndex(i);
                 v.setHideSensitiveForIntrinsicHeight(hideSensitive);
             }
             mAmbientState.setHideSensitive(hideSensitive);
@@ -4265,7 +4269,7 @@
     private void applyCurrentState() {
         int numChildren = getChildCount();
         for (int i = 0; i < numChildren; i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             child.applyViewState();
         }
 
@@ -4285,7 +4289,7 @@
 
         // Lefts first sort by Z difference
         for (int i = 0; i < getChildCount(); i++) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != GONE) {
                 mTmpSortedChildren.add(child);
             }
@@ -4512,7 +4516,7 @@
     public void setClearAllInProgress(boolean clearAllInProgress) {
         mClearAllInProgress = clearAllInProgress;
         mAmbientState.setClearAllInProgress(clearAllInProgress);
-        mController.getNoticationRoundessManager().setClearAllInProgress(clearAllInProgress);
+        mController.getNotificationRoundnessManager().setClearAllInProgress(clearAllInProgress);
     }
 
     boolean getClearAllInProgress() {
@@ -4555,7 +4559,7 @@
         final int count = getChildCount();
         float max = 0;
         for (int childIdx = 0; childIdx < count; childIdx++) {
-            ExpandableView child = (ExpandableView) getChildAt(childIdx);
+            ExpandableView child = getChildAtIndex(childIdx);
             if (child.getVisibility() == GONE) {
                 continue;
             }
@@ -4586,7 +4590,7 @@
     public boolean isBelowLastNotification(float touchX, float touchY) {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
-            ExpandableView child = (ExpandableView) getChildAt(i);
+            ExpandableView child = getChildAtIndex(i);
             if (child.getVisibility() != View.GONE) {
                 float childTop = child.getY();
                 if (childTop > touchY) {
@@ -5052,7 +5056,7 @@
             pw.println();
 
             for (int i = 0; i < childCount; i++) {
-                ExpandableView child = (ExpandableView) getChildAt(i);
+                ExpandableView child = getChildAtIndex(i);
                 child.dump(pw, args);
                 pw.println();
             }
@@ -5341,7 +5345,7 @@
         float wakeUplocation = -1f;
         int childCount = getChildCount();
         for (int i = 0; i < childCount; i++) {
-            ExpandableView view = (ExpandableView) getChildAt(i);
+            ExpandableView view = getChildAtIndex(i);
             if (view.getVisibility() == View.GONE) {
                 continue;
             }
@@ -5380,7 +5384,7 @@
     public void setController(
             NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
         mController = notificationStackScrollLayoutController;
-        mController.getNoticationRoundessManager().setAnimatedChildren(mChildrenToAddAnimated);
+        mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated);
     }
 
     void addSwipedOutView(View v) {
@@ -5391,31 +5395,22 @@
         if (!(viewSwiped instanceof ExpandableNotificationRow)) {
             return;
         }
-        final int indexOfSwipedView = indexOfChild(viewSwiped);
-        if (indexOfSwipedView < 0) {
-            return;
-        }
         mSectionsManager.updateFirstAndLastViewsForAllSections(
-                mSections, getChildrenWithBackground());
-        View viewBefore = null;
-        if (indexOfSwipedView > 0) {
-            viewBefore = getChildAt(indexOfSwipedView - 1);
-            if (mSectionsManager.beginsSection(viewSwiped, viewBefore)) {
-                viewBefore = null;
-            }
-        }
-        View viewAfter = null;
-        if (indexOfSwipedView < getChildCount()) {
-            viewAfter = getChildAt(indexOfSwipedView + 1);
-            if (mSectionsManager.beginsSection(viewAfter, viewSwiped)) {
-                viewAfter = null;
-            }
-        }
-        mController.getNoticationRoundessManager()
+                mSections,
+                getChildrenWithBackground()
+        );
+
+        RoundableTargets targets = mController.getNotificationTargetsHelper().findRoundableTargets(
+                (ExpandableNotificationRow) viewSwiped,
+                this,
+                mSectionsManager
+        );
+
+        mController.getNotificationRoundnessManager()
                 .setViewsAffectedBySwipe(
-                        (ExpandableView) viewBefore,
-                        (ExpandableView) viewSwiped,
-                        (ExpandableView) viewAfter);
+                        targets.getBefore(),
+                        targets.getSwiped(),
+                        targets.getAfter());
 
         updateFirstAndLastBackgroundViews();
         requestDisallowInterceptTouchEvent(true);
@@ -5426,7 +5421,7 @@
 
     void onSwipeEnd() {
         updateFirstAndLastBackgroundViews();
-        mController.getNoticationRoundessManager()
+        mController.getNotificationRoundnessManager()
                 .setViewsAffectedBySwipe(null, null, null);
         // Round bottom corners for notification right before shelf.
         mShelf.updateAppearance();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index bde98ff..e1337826 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -180,6 +180,7 @@
     private int mBarState;
     private HeadsUpAppearanceController mHeadsUpAppearanceController;
     private final FeatureFlags mFeatureFlags;
+    private final NotificationTargetsHelper mNotificationTargetsHelper;
 
     private View mLongPressedView;
 
@@ -642,7 +643,8 @@
             StackStateLogger stackLogger,
             NotificationStackScrollLogger logger,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
-            FeatureFlags featureFlags) {
+            FeatureFlags featureFlags,
+            NotificationTargetsHelper notificationTargetsHelper) {
         mStackStateLogger = stackLogger;
         mLogger = logger;
         mAllowLongPress = allowLongPress;
@@ -679,6 +681,7 @@
         mRemoteInputManager = remoteInputManager;
         mShadeController = shadeController;
         mFeatureFlags = featureFlags;
+        mNotificationTargetsHelper = notificationTargetsHelper;
         updateResources();
     }
 
@@ -1380,7 +1383,7 @@
         return mView.calculateGapHeight(previousView, child, count);
     }
 
-    NotificationRoundnessManager getNoticationRoundessManager() {
+    NotificationRoundnessManager getNotificationRoundnessManager() {
         return mNotificationRoundnessManager;
     }
 
@@ -1537,6 +1540,10 @@
         mNotificationActivityStarter = activityStarter;
     }
 
+    public NotificationTargetsHelper getNotificationTargetsHelper() {
+        return mNotificationTargetsHelper;
+    }
+
     /**
      * Enum for UiEvent logged from this class
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
new file mode 100644
index 0000000..991a14b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelper.kt
@@ -0,0 +1,100 @@
+package com.android.systemui.statusbar.notification.stack
+
+import androidx.core.view.children
+import androidx.core.view.isVisible
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.Roundable
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.ExpandableView
+import javax.inject.Inject
+
+/**
+ * Utility class that helps us find the targets of an animation, often used to find the notification
+ * ([Roundable]) above and below the current one (see [findRoundableTargets]).
+ */
+@SysUISingleton
+class NotificationTargetsHelper
+@Inject
+constructor(
+    featureFlags: FeatureFlags,
+) {
+    private val isNotificationGroupCornerEnabled =
+        featureFlags.isEnabled(Flags.NOTIFICATION_GROUP_CORNER)
+
+    /**
+     * This method looks for views that can be rounded (and implement [Roundable]) during a
+     * notification swipe.
+     * @return The [Roundable] targets above/below the [viewSwiped] (if available). The
+     * [RoundableTargets.before] and [RoundableTargets.after] parameters can be `null` if there is
+     * no above/below notification or the notification is not part of the same section.
+     */
+    fun findRoundableTargets(
+        viewSwiped: ExpandableNotificationRow,
+        stackScrollLayout: NotificationStackScrollLayout,
+        sectionsManager: NotificationSectionsManager,
+    ): RoundableTargets {
+        val viewBefore: Roundable?
+        val viewAfter: Roundable?
+
+        val notificationParent = viewSwiped.notificationParent
+        val childrenContainer = notificationParent?.childrenContainer
+        val visibleStackChildren =
+            stackScrollLayout.children
+                .filterIsInstance<ExpandableView>()
+                .filter { it.isVisible }
+                .toList()
+        if (notificationParent != null && childrenContainer != null) {
+            // We are inside a notification group
+
+            if (!isNotificationGroupCornerEnabled) {
+                return RoundableTargets(null, null, null)
+            }
+
+            val visibleGroupChildren = childrenContainer.attachedChildren.filter { it.isVisible }
+            val indexOfParentSwipedView = visibleGroupChildren.indexOf(viewSwiped)
+
+            viewBefore =
+                visibleGroupChildren.getOrNull(indexOfParentSwipedView - 1)
+                    ?: childrenContainer.notificationHeaderWrapper
+
+            viewAfter =
+                visibleGroupChildren.getOrNull(indexOfParentSwipedView + 1)
+                    ?: visibleStackChildren.indexOf(notificationParent).let {
+                        visibleStackChildren.getOrNull(it + 1)
+                    }
+        } else {
+            // Assumption: we are inside the NotificationStackScrollLayout
+
+            val indexOfSwipedView = visibleStackChildren.indexOf(viewSwiped)
+
+            viewBefore =
+                visibleStackChildren.getOrNull(indexOfSwipedView - 1)?.takeIf {
+                    !sectionsManager.beginsSection(viewSwiped, it)
+                }
+
+            viewAfter =
+                visibleStackChildren.getOrNull(indexOfSwipedView + 1)?.takeIf {
+                    !sectionsManager.beginsSection(it, viewSwiped)
+                }
+        }
+
+        return RoundableTargets(
+            before = viewBefore,
+            swiped = viewSwiped,
+            after = viewAfter,
+        )
+    }
+}
+
+/**
+ * This object contains targets above/below the [swiped] (if available). The [before] and [after]
+ * parameters can be `null` if there is no above/below notification or the notification is not part
+ * of the same section.
+ */
+data class RoundableTargets(
+    val before: Roundable?,
+    val swiped: ExpandableNotificationRow?,
+    val after: Roundable?,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 0502159..eea1d911 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -31,6 +31,7 @@
 import com.android.systemui.animation.ShadeInterpolation;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.NotificationShelf;
+import com.android.systemui.statusbar.notification.SourceType;
 import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -804,7 +805,7 @@
                 row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
         final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
                 ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
-        row.setBottomRoundness(roundness, /* animate= */ false);
+        row.requestBottomRoundness(roundness, /* animate = */ false, SourceType.OnScroll);
     }
 
     @VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index da6d455..dd400b3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -47,6 +47,7 @@
 import android.view.View;
 import android.view.ViewAnimationUtils;
 import android.view.ViewGroup;
+import android.view.ViewRootImpl;
 import android.view.WindowInsets;
 import android.view.WindowInsetsAnimation;
 import android.view.WindowInsetsController;
@@ -61,6 +62,8 @@
 import android.widget.LinearLayout;
 import android.widget.ProgressBar;
 import android.widget.TextView;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -88,6 +91,7 @@
  */
 public class RemoteInputView extends LinearLayout implements View.OnClickListener {
 
+    private static final boolean DEBUG = false;
     private static final String TAG = "RemoteInput";
 
     // A marker object that let's us easily find views of this class.
@@ -124,6 +128,7 @@
     // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
     //  that need the controller shouldn't have access to the view
     private RemoteInputViewController mViewController;
+    private ViewRootImpl mTestableViewRootImpl;
 
     /**
      * Enum for logged notification remote input UiEvents.
@@ -430,10 +435,20 @@
         }
     }
 
+    @VisibleForTesting
+    protected void setViewRootImpl(ViewRootImpl viewRoot) {
+        mTestableViewRootImpl = viewRoot;
+    }
+
+    @VisibleForTesting
+    protected void setEditTextReferenceToSelf() {
+        mEditText.mRemoteInputView = this;
+    }
+
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        mEditText.mRemoteInputView = this;
+        setEditTextReferenceToSelf();
         mEditText.setOnEditorActionListener(mEditorActionHandler);
         mEditText.addTextChangedListener(mTextWatcher);
         if (mEntry.getRow().isChangingPosition()) {
@@ -457,7 +472,50 @@
     }
 
     @Override
+    public ViewRootImpl getViewRootImpl() {
+        if (mTestableViewRootImpl != null) {
+            return mTestableViewRootImpl;
+        }
+        return super.getViewRootImpl();
+    }
+
+    private void registerBackCallback() {
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot == null) {
+            if (DEBUG) {
+                Log.d(TAG, "ViewRoot was null, NOT registering Predictive Back callback");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "registering Predictive Back callback");
+        }
+        viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                OnBackInvokedDispatcher.PRIORITY_OVERLAY, mEditText.mOnBackInvokedCallback);
+    }
+
+    private void unregisterBackCallback() {
+        ViewRootImpl viewRoot = getViewRootImpl();
+        if (viewRoot == null) {
+            if (DEBUG) {
+                Log.d(TAG, "ViewRoot was null, NOT unregistering Predictive Back callback");
+            }
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "unregistering Predictive Back callback");
+        }
+        viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(
+                mEditText.mOnBackInvokedCallback);
+    }
+
+    @Override
     public void onVisibilityAggregated(boolean isVisible) {
+        if (isVisible) {
+            registerBackCallback();
+        } else {
+            unregisterBackCallback();
+        }
         super.onVisibilityAggregated(isVisible);
         mEditText.setEnabled(isVisible && !mSending);
     }
@@ -822,10 +880,21 @@
             return super.onKeyDown(keyCode, event);
         }
 
+        private final OnBackInvokedCallback mOnBackInvokedCallback = () -> {
+            if (DEBUG) {
+                Log.d(TAG, "Predictive Back Callback dispatched");
+            }
+            respondToKeycodeBack();
+        };
+
+        private void respondToKeycodeBack() {
+            defocusIfNeeded(true /* animate */);
+        }
+
         @Override
         public boolean onKeyUp(int keyCode, KeyEvent event) {
             if (keyCode == KeyEvent.KEYCODE_BACK) {
-                defocusIfNeeded(true /* animate */);
+                respondToKeycodeBack();
                 return true;
             }
             return super.onKeyUp(keyCode, event);
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index d5d904c..f0a50de 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.temporarydisplay
 
 import android.annotation.LayoutRes
-import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.PixelFormat
 import android.graphics.Rect
@@ -67,11 +66,10 @@
      * Window layout params that will be used as a starting point for the [windowLayoutParams] of
      * all subclasses.
      */
-    @SuppressLint("WrongConstant") // We're allowed to use TYPE_VOLUME_OVERLAY
     internal val commonWindowLayoutParams = WindowManager.LayoutParams().apply {
         width = WindowManager.LayoutParams.WRAP_CONTENT
         height = WindowManager.LayoutParams.WRAP_CONTENT
-        type = WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY
+        type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR
         flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
             WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
         title = windowTitle
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 1a8aafb..cd7bd2d 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -124,6 +124,9 @@
         // ---- Text ----
         val textView = currentView.requireViewById<TextView>(R.id.text)
         TextViewBinder.bind(textView, newInfo.text)
+        // Updates text view bounds to make sure it perfectly fits the new text
+        // (If the new text is smaller than the previous text) see b/253228632.
+        textView.requestLayout()
 
         // ---- End item ----
         // Loading
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/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index fbc6a58..309f168 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -22,6 +22,7 @@
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DIALOG_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ONE_HANDED_ACTIVE;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
@@ -55,6 +56,8 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.tracing.ProtoTracer;
 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;
@@ -111,6 +114,7 @@
     private final Optional<SplitScreen> mSplitScreenOptional;
     private final Optional<OneHanded> mOneHandedOptional;
     private final Optional<FloatingTasks> mFloatingTasksOptional;
+    private final Optional<DesktopMode> mDesktopModeOptional;
 
     private final CommandQueue mCommandQueue;
     private final ConfigurationController mConfigurationController;
@@ -173,6 +177,7 @@
             Optional<SplitScreen> splitScreenOptional,
             Optional<OneHanded> oneHandedOptional,
             Optional<FloatingTasks> floatingTasksOptional,
+            Optional<DesktopMode> desktopMode,
             CommandQueue commandQueue,
             ConfigurationController configurationController,
             KeyguardStateController keyguardStateController,
@@ -194,6 +199,7 @@
         mPipOptional = pipOptional;
         mSplitScreenOptional = splitScreenOptional;
         mOneHandedOptional = oneHandedOptional;
+        mDesktopModeOptional = desktopMode;
         mWakefulnessLifecycle = wakefulnessLifecycle;
         mProtoTracer = protoTracer;
         mUserTracker = userTracker;
@@ -219,6 +225,7 @@
         mPipOptional.ifPresent(this::initPip);
         mSplitScreenOptional.ifPresent(this::initSplitScreen);
         mOneHandedOptional.ifPresent(this::initOneHanded);
+        mDesktopModeOptional.ifPresent(this::initDesktopMode);
     }
 
     @VisibleForTesting
@@ -326,6 +333,16 @@
         });
     }
 
+    void initDesktopMode(DesktopMode desktopMode) {
+        desktopMode.addListener(new DesktopModeTaskRepository.VisibleTasksListener() {
+            @Override
+            public void onVisibilityChanged(boolean hasFreeformTasks) {
+                mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, hasFreeformTasks)
+                        .commitUpdate(DEFAULT_DISPLAY);
+            }
+        }, mSysUiMainExecutor);
+    }
+
     @Override
     public void writeToProto(SystemUiTraceProto proto) {
         if (proto.wmShell == null) {
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index ba28045..1b404a8 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -88,6 +88,11 @@
                   android:excludeFromRecents="true"
                   />
 
+        <activity android:name=".settings.brightness.BrightnessDialogTest$TestDialog"
+            android:exported="false"
+            android:excludeFromRecents="true"
+            />
+
         <activity android:name="com.android.systemui.screenshot.ScrollViewActivity"
                   android:exported="false" />
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 03efd06..1c3656d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockFaceEvents
+import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.mockito.any
@@ -82,7 +83,7 @@
     @Mock private lateinit var parentView: View
     @Mock private lateinit var transitionRepository: KeyguardTransitionRepository
     private lateinit var repository: FakeKeyguardRepository
-
+    @Mock private lateinit var logBuffer: LogBuffer
     private lateinit var underTest: ClockEventController
 
     @Before
@@ -109,6 +110,7 @@
             context,
             mainExecutor,
             bgExecutor,
+            logBuffer,
             featureFlags
         )
         underTest.clock = clock
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/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index d52612b..e8c760c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -52,6 +52,7 @@
 import org.mockito.Mockito.anyLong
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.junit.MockitoJUnit
@@ -123,6 +124,21 @@
     }
 
     @Test
+    fun testDismissBeforeIntroEnd() {
+        val container = initializeFingerprintContainer()
+        waitForIdleSync()
+
+        // STATE_ANIMATING_IN = 1
+        container?.mContainerState = 1
+
+        container.dismissWithoutCallback(false)
+
+        // the first time is triggered by initializeFingerprintContainer()
+        // the second time was triggered by dismissWithoutCallback()
+        verify(callback, times(2)).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
+    }
+
+    @Test
     fun testDismissesOnFocusLoss() {
         val container = initializeFingerprintContainer()
         waitForIdleSync()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index d610709..28e13b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -169,6 +169,8 @@
     @Mock
     private LatencyTracker mLatencyTracker;
     private FakeExecutor mFgExecutor;
+    @Mock
+    private UdfpsDisplayMode mUdfpsDisplayMode;
 
     // Stuff for configuring mocks
     @Mock
@@ -258,7 +260,6 @@
                 mVibrator,
                 mUdfpsHapticsSimulator,
                 mUdfpsShell,
-                Optional.of(mDisplayModeProvider),
                 mKeyguardStateController,
                 mDisplayManager,
                 mHandler,
@@ -275,6 +276,7 @@
         verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
         mScreenObserver = mScreenObserverCaptor.getValue();
         mUdfpsController.updateOverlayParams(TEST_UDFPS_SENSOR_ID, new UdfpsOverlayParams());
+        mUdfpsController.setUdfpsDisplayMode(mUdfpsDisplayMode);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java
new file mode 100644
index 0000000..7e35b26
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDisplayModeTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.biometrics;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.fingerprint.IUdfpsHbmListener;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper.RunWithLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.util.concurrency.FakeExecution;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper(setAsMainLooper = true)
+public class UdfpsDisplayModeTest extends SysuiTestCase {
+    private static final int DISPLAY_ID = 0;
+
+    @Mock
+    private AuthController mAuthController;
+    @Mock
+    private IUdfpsHbmListener mDisplayCallback;
+    @Mock
+    private Runnable mOnEnabled;
+    @Mock
+    private Runnable mOnDisabled;
+
+    private final FakeExecution mExecution = new FakeExecution();
+    private UdfpsDisplayMode mHbmController;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        // Force mContext to always return DISPLAY_ID
+        Context contextSpy = spy(mContext);
+        when(contextSpy.getDisplayId()).thenReturn(DISPLAY_ID);
+
+        // Set up mocks.
+        when(mAuthController.getUdfpsHbmListener()).thenReturn(mDisplayCallback);
+
+        // Create a real controller with mock dependencies.
+        mHbmController = new UdfpsDisplayMode(contextSpy, mExecution, mAuthController);
+    }
+
+    @Test
+    public void roundTrip() throws RemoteException {
+        // Enable the UDFPS mode.
+        mHbmController.enable(mOnEnabled);
+
+        // Should set the appropriate refresh rate for UDFPS and notify the caller.
+        verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+        verify(mOnEnabled).run();
+
+        // Disable the UDFPS mode.
+        mHbmController.disable(mOnDisabled);
+
+        // Should unset the refresh rate and notify the caller.
+        verify(mOnDisabled).run();
+        verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID));
+    }
+
+    @Test
+    public void mustNotEnableMoreThanOnce() throws RemoteException {
+        // First request to enable the UDFPS mode.
+        mHbmController.enable(mOnEnabled);
+
+        // Should set the appropriate refresh rate for UDFPS and notify the caller.
+        verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+        verify(mOnEnabled).run();
+
+        // Second request to enable the UDFPS mode, while it's still enabled.
+        mHbmController.enable(mOnEnabled);
+
+        // Should ignore the second request.
+        verifyNoMoreInteractions(mDisplayCallback);
+        verifyNoMoreInteractions(mOnEnabled);
+    }
+
+    @Test
+    public void mustNotDisableMoreThanOnce() throws RemoteException {
+        // Disable the UDFPS mode.
+        mHbmController.enable(mOnEnabled);
+
+        // Should set the appropriate refresh rate for UDFPS and notify the caller.
+        verify(mDisplayCallback).onHbmEnabled(eq(DISPLAY_ID));
+        verify(mOnEnabled).run();
+
+        // First request to disable the UDFPS mode.
+        mHbmController.disable(mOnDisabled);
+
+        // Should unset the refresh rate and notify the caller.
+        verify(mOnDisabled).run();
+        verify(mDisplayCallback).onHbmDisabled(eq(DISPLAY_ID));
+
+        // Second request to disable the UDFPS mode, when it's already disabled.
+        mHbmController.disable(mOnDisabled);
+
+        // Should ignore the second request.
+        verifyNoMoreInteractions(mOnDisabled);
+        verifyNoMoreInteractions(mDisplayCallback);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
index 677c7bd..b7f1c1a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlayControllerTest.java
@@ -39,8 +39,8 @@
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.screenshot.TimeoutHandler;
 
+import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -48,7 +48,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-@Ignore("b/254635291")
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class ClipboardOverlayControllerTest extends SysuiTestCase {
@@ -97,6 +96,11 @@
         mCallbacks = mOverlayCallbacksCaptor.getValue();
     }
 
+    @After
+    public void tearDown() {
+        mOverlayController.hideImmediate();
+    }
+
     @Test
     public void test_setClipData_nullData() {
         ClipData clipData = 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/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
new file mode 100644
index 0000000..1130bda
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.settings.brightness
+
+import android.content.Intent
+import android.graphics.Rect
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import androidx.test.rule.ActivityTestRule
+import androidx.test.runner.intercepting.SingleActivityFactory
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.util.mockito.any
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class BrightnessDialogTest : SysuiTestCase() {
+
+    @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory
+    @Mock private lateinit var backgroundHandler: Handler
+    @Mock private lateinit var brightnessSliderController: BrightnessSliderController
+
+    @Rule
+    @JvmField
+    var activityRule =
+        ActivityTestRule(
+            object : SingleActivityFactory<TestDialog>(TestDialog::class.java) {
+                override fun create(intent: Intent?): TestDialog {
+                    return TestDialog(
+                        fakeBroadcastDispatcher,
+                        brightnessSliderControllerFactory,
+                        backgroundHandler
+                    )
+                }
+            },
+            false,
+            false
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        `when`(brightnessSliderControllerFactory.create(any(), any()))
+            .thenReturn(brightnessSliderController)
+        `when`(brightnessSliderController.rootView).thenReturn(View(context))
+
+        activityRule.launchActivity(null)
+    }
+
+    @After
+    fun tearDown() {
+        activityRule.finishActivity()
+    }
+
+    @Test
+    fun testGestureExclusion() {
+        val frame = activityRule.activity.requireViewById<View>(R.id.brightness_mirror_container)
+
+        val lp = frame.layoutParams as ViewGroup.MarginLayoutParams
+        val horizontalMargin =
+            activityRule.activity.resources.getDimensionPixelSize(
+                R.dimen.notification_side_paddings
+            )
+        assertThat(lp.leftMargin).isEqualTo(horizontalMargin)
+        assertThat(lp.rightMargin).isEqualTo(horizontalMargin)
+
+        assertThat(frame.systemGestureExclusionRects.size).isEqualTo(1)
+        val exclusion = frame.systemGestureExclusionRects[0]
+        assertThat(exclusion)
+            .isEqualTo(Rect(-horizontalMargin, 0, frame.width + horizontalMargin, frame.height))
+    }
+
+    class TestDialog(
+        broadcastDispatcher: BroadcastDispatcher,
+        brightnessSliderControllerFactory: BrightnessSliderController.Factory,
+        backgroundHandler: Handler
+    ) : BrightnessDialog(broadcastDispatcher, brightnessSliderControllerFactory, backgroundHandler)
+}
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 cf5fa87..64dc956 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
@@ -16,6 +16,11 @@
 
 package com.android.systemui.shared.system;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.view.RemoteAnimationTarget.MODE_CHANGING;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
@@ -25,11 +30,6 @@
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_STANDARD;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CHANGING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
-import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -40,6 +40,7 @@
 import android.graphics.Rect;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
 import android.window.TransitionInfo;
@@ -73,12 +74,12 @@
                 .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */)
                 .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build();
         // Check apps extraction
-        RemoteAnimationTargetCompat[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
+        RemoteAnimationTarget[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
                 mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(2, wrapped.length);
         int changeLayer = -1;
         int closeLayer = -1;
-        for (RemoteAnimationTargetCompat t : wrapped) {
+        for (RemoteAnimationTarget t : wrapped) {
             if (t.mode == MODE_CHANGING) {
                 changeLayer = t.prefixOrderIndex;
             } else if (t.mode == MODE_CLOSING) {
@@ -91,14 +92,14 @@
         assertTrue(closeLayer < changeLayer);
 
         // Check wallpaper extraction
-        RemoteAnimationTargetCompat[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+        RemoteAnimationTarget[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
                 true /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(1, wallps.length);
         assertTrue(wallps[0].prefixOrderIndex < closeLayer);
         assertEquals(MODE_OPENING, wallps[0].mode);
 
         // Check non-apps extraction
-        RemoteAnimationTargetCompat[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+        RemoteAnimationTarget[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
                 false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(1, nonApps.length);
         assertTrue(nonApps[0].prefixOrderIndex < closeLayer);
@@ -115,9 +116,9 @@
         change.setTaskInfo(createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_HOME));
         change.setEndAbsBounds(endBounds);
         change.setEndRelOffset(0, 0);
-        final RemoteAnimationTargetCompat wrapped = new RemoteAnimationTargetCompat(change,
-                0 /* order */, tinfo, mock(SurfaceControl.Transaction.class));
-        assertEquals(ACTIVITY_TYPE_HOME, wrapped.activityType);
+        RemoteAnimationTarget wrapped = RemoteAnimationTargetCompat.newTarget(
+                change, 0 /* order */, tinfo, mock(SurfaceControl.Transaction.class), null);
+        assertEquals(ACTIVITY_TYPE_HOME, wrapped.windowConfiguration.getActivityType());
         assertEquals(new Rect(0, 0, 100, 140), wrapped.localBounds);
         assertEquals(endBounds, wrapped.screenSpaceBounds);
         assertTrue(wrapped.isTranslucent);
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/notification/stack/NotificationRoundnessManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
index a95a49c..8c8b644 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationRoundnessManagerTest.java
@@ -147,8 +147,8 @@
                 createSection(mFirst, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -170,13 +170,13 @@
         when(testHelper.getStatusBarStateController().isDozing()).thenReturn(true);
         row.setHeadsUp(true);
         mRoundnessManager.updateView(entry.getRow(), false);
-        Assert.assertEquals(1f, row.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1f, row.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1f, row.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1f, row.getTopRoundness(), 0.0f);
 
         row.setHeadsUp(false);
         mRoundnessManager.updateView(entry.getRow(), false);
-        Assert.assertEquals(mSmallRadiusRatio, row.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, row.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, row.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, row.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -185,8 +185,8 @@
                 createSection(mFirst, mFirst),
                 createSection(null, mSecond)
         });
-        Assert.assertEquals(1.0f, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -195,8 +195,8 @@
                 createSection(mFirst, mFirst),
                 createSection(mSecond, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mSecond.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mSecond.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mSecond.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mSecond.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -205,8 +205,8 @@
                 createSection(mFirst, null),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -215,8 +215,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -226,8 +226,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -238,8 +238,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -250,8 +250,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -262,8 +262,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -274,8 +274,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -286,8 +286,8 @@
                 createSection(mSecond, mSecond),
                 createSection(null, null)
         });
-        Assert.assertEquals(0.5f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(0.5f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(0.5f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(0.5f, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
@@ -298,8 +298,8 @@
                 createSection(null, null)
         });
         mFirst.setHeadsUpAnimatingAway(true);
-        Assert.assertEquals(1.0f, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(1.0f, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(1.0f, mFirst.getTopRoundness(), 0.0f);
     }
 
 
@@ -312,8 +312,8 @@
         });
         mFirst.setHeadsUpAnimatingAway(true);
         mFirst.setHeadsUpAnimatingAway(false);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentBottomRoundness(), 0.0f);
-        Assert.assertEquals(mSmallRadiusRatio, mFirst.getCurrentTopRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getBottomRoundness(), 0.0f);
+        Assert.assertEquals(mSmallRadiusRatio, mFirst.getTopRoundness(), 0.0f);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index a3c95dc..90061b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -129,6 +129,7 @@
     @Mock private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
     @Mock private ShadeTransitionController mShadeTransitionController;
     @Mock private FeatureFlags mFeatureFlags;
+    @Mock private NotificationTargetsHelper mNotificationTargetsHelper;
 
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
@@ -177,7 +178,8 @@
                 mStackLogger,
                 mLogger,
                 mNotificationStackSizeCalculator,
-                mFeatureFlags
+                mFeatureFlags,
+                mNotificationTargetsHelper
         );
 
         when(mNotificationStackScrollLayout.isAttachedToWindow()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 35c8b61..91aecd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -163,7 +163,7 @@
         mStackScroller.setCentralSurfaces(mCentralSurfaces);
         mStackScroller.setEmptyShadeView(mEmptyShadeView);
         when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
-        when(mStackScrollLayoutController.getNoticationRoundessManager())
+        when(mStackScrollLayoutController.getNotificationRoundnessManager())
                 .thenReturn(mNotificationRoundnessManager);
         mStackScroller.setController(mStackScrollLayoutController);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
new file mode 100644
index 0000000..a2e9230
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
@@ -0,0 +1,107 @@
+package com.android.systemui.statusbar.notification.stack
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.util.mockito.mock
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for {@link NotificationTargetsHelper}. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationTargetsHelperTest : SysuiTestCase() {
+    lateinit var notificationTestHelper: NotificationTestHelper
+    private val sectionsManager: NotificationSectionsManager = mock()
+    private val stackScrollLayout: NotificationStackScrollLayout = mock()
+
+    @Before
+    fun setUp() {
+        allowTestableLooperAsMainThread()
+        notificationTestHelper =
+            NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+    }
+
+    private fun notificationTargetsHelper(
+        notificationGroupCorner: Boolean = true,
+    ) =
+        NotificationTargetsHelper(
+            FakeFeatureFlags().apply {
+                set(Flags.NOTIFICATION_GROUP_CORNER, notificationGroupCorner)
+            }
+        )
+
+    @Test
+    fun targetsForFirstNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[0]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.notificationHeaderWrapper, // group header
+                swiped = swiped,
+                after = children.attachedChildren[1],
+            )
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun targetsForMiddleNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[1]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.attachedChildren[0],
+                swiped = swiped,
+                after = children.attachedChildren[2],
+            )
+        assertEquals(expected, actual)
+    }
+
+    @Test
+    fun targetsForLastNotificationInGroup() {
+        val children = notificationTestHelper.createGroup(3).childrenContainer
+        val swiped = children.attachedChildren[2]
+
+        val actual =
+            notificationTargetsHelper()
+                .findRoundableTargets(
+                    viewSwiped = swiped,
+                    stackScrollLayout = stackScrollLayout,
+                    sectionsManager = sectionsManager,
+                )
+
+        val expected =
+            RoundableTargets(
+                before = children.attachedChildren[1],
+                swiped = swiped,
+                after = null,
+            )
+        assertEquals(expected, actual)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 6ace404..915e999 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -23,8 +23,12 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
 import android.app.PendingIntent;
@@ -43,10 +47,14 @@
 import android.testing.TestableLooper;
 import android.view.ContentInfo;
 import android.view.View;
+import android.view.ViewRootImpl;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.widget.EditText;
 import android.widget.ImageButton;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
+import android.window.WindowOnBackInvokedDispatcher;
 
 import androidx.annotation.NonNull;
 import androidx.test.filters.SmallTest;
@@ -67,6 +75,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -229,6 +238,67 @@
     }
 
     @Test
+    public void testPredictiveBack_registerAndUnregister() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+
+        ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+        WindowOnBackInvokedDispatcher backInvokedDispatcher = mock(
+                WindowOnBackInvokedDispatcher.class);
+        ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass(
+                OnBackInvokedCallback.class);
+        when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
+        view.setViewRootImpl(viewRoot);
+
+        /* verify that predictive back callback registered when RemoteInputView becomes visible */
+        view.onVisibilityAggregated(true);
+        verify(backInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+                onBackInvokedCallbackCaptor.capture());
+
+        /* verify that same callback unregistered when RemoteInputView becomes invisible */
+        view.onVisibilityAggregated(false);
+        verify(backInvokedDispatcher).unregisterOnBackInvokedCallback(
+                eq(onBackInvokedCallbackCaptor.getValue()));
+    }
+
+    @Test
+    public void testUiPredictiveBack_openAndDispatchCallback() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+        ViewRootImpl viewRoot = mock(ViewRootImpl.class);
+        WindowOnBackInvokedDispatcher backInvokedDispatcher = mock(
+                WindowOnBackInvokedDispatcher.class);
+        ArgumentCaptor<OnBackInvokedCallback> onBackInvokedCallbackCaptor = ArgumentCaptor.forClass(
+                OnBackInvokedCallback.class);
+        when(viewRoot.getOnBackInvokedDispatcher()).thenReturn(backInvokedDispatcher);
+        view.setViewRootImpl(viewRoot);
+        view.onVisibilityAggregated(true);
+        view.setEditTextReferenceToSelf();
+
+        /* capture the callback during registration */
+        verify(backInvokedDispatcher).registerOnBackInvokedCallback(
+                eq(OnBackInvokedDispatcher.PRIORITY_OVERLAY),
+                onBackInvokedCallbackCaptor.capture());
+
+        view.focus();
+
+        /* invoke the captured callback */
+        onBackInvokedCallbackCaptor.getValue().onBackInvoked();
+
+        /* verify that the RemoteInputView goes away */
+        assertEquals(view.getVisibility(), View.GONE);
+    }
+
+    @Test
     public void testUiEventLogging_openAndSend() throws Exception {
         NotificationTestHelper helper = new NotificationTestHelper(
                 mContext,
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 cebe946..7af66f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/WMShellTest.java
@@ -34,6 +34,8 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.tracing.ProtoTracer;
 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;
@@ -49,6 +51,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.Optional;
+import java.util.concurrent.Executor;
 
 /**
  * Tests for {@link WMShell}.
@@ -76,12 +79,14 @@
     @Mock UserTracker mUserTracker;
     @Mock ShellExecutor mSysUiMainExecutor;
     @Mock FloatingTasks mFloatingTasks;
+    @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),
+                Optional.of(mDesktopMode),
                 mCommandQueue, mConfigurationController, mKeyguardStateController,
                 mKeyguardUpdateMonitor, mScreenLifecycle, mSysUiState, mProtoTracer,
                 mWakefulnessLifecycle, mUserTracker, mSysUiMainExecutor);
@@ -103,4 +108,12 @@
         verify(mOneHanded).registerTransitionCallback(any(OneHandedTransitionCallback.class));
         verify(mOneHanded).registerEventCallback(any(OneHandedEventCallback.class));
     }
+
+    @Test
+    public void initDesktopMode_registersListener() {
+        mWMShell.initDesktopMode(mDesktopMode);
+        verify(mDesktopMode).addListener(
+                any(DesktopModeTaskRepository.VisibleTasksListener.class),
+                any(Executor.class));
+    }
 }
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/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 014d580..18c29fa 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -644,8 +644,8 @@
             Permission bp = mRegistry.getPermission(info.name);
             added = bp == null;
             int fixedLevel = PermissionInfo.fixProtectionLevel(info.protectionLevel);
+            enforcePermissionCapLocked(info, tree);
             if (added) {
-                enforcePermissionCapLocked(info, tree);
                 bp = new Permission(info.name, tree.getPackageName(), Permission.TYPE_DYNAMIC);
             } else if (!bp.isDynamic()) {
                 throw new SecurityException("Not allowed to modify non-dynamic permission "
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/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/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());
     }