Merge "Shrink default launch bounds on small display" into tm-qpr-dev
diff --git a/core/java/android/os/BatteryManagerInternal.java b/core/java/android/os/BatteryManagerInternal.java
index 97ec594..9bad0de 100644
--- a/core/java/android/os/BatteryManagerInternal.java
+++ b/core/java/android/os/BatteryManagerInternal.java
@@ -47,6 +47,14 @@
     public abstract int getBatteryLevel();
 
     /**
+     * Returns battery health status as an integer representing the current battery health constant.
+     *
+     * This is a simple accessor that's safe to be called from any locks, but internally it may
+     * wait on the battery service lock.
+     */
+    public abstract int getBatteryHealth();
+
+    /**
      * Instantaneous battery capacity in uA-h, as defined in the HealthInfo HAL struct.
      * Please note apparently it could be bigger than {@link #getBatteryFullCharge}.
      *
diff --git a/core/java/android/window/ITaskFragmentOrganizerController.aidl b/core/java/android/window/ITaskFragmentOrganizerController.aidl
index 3250dd8..d25c8a8 100644
--- a/core/java/android/window/ITaskFragmentOrganizerController.aidl
+++ b/core/java/android/window/ITaskFragmentOrganizerController.aidl
@@ -39,13 +39,13 @@
      * animations if the transition only contains windows that belong to the organized
      * TaskFragments in the given Task.
      */
-    void registerRemoteAnimations(in ITaskFragmentOrganizer organizer, int taskId,
+    void registerRemoteAnimations(in ITaskFragmentOrganizer organizer,
         in RemoteAnimationDefinition definition);
 
     /**
      * Unregisters remote animations per transition type for the organizer.
      */
-    void unregisterRemoteAnimations(in ITaskFragmentOrganizer organizer, int taskId);
+    void unregisterRemoteAnimations(in ITaskFragmentOrganizer organizer);
 
     /**
      * Checks if an activity organized by a {@link android.window.TaskFragmentOrganizer} and
diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java
index 648541b..ab7d616 100644
--- a/core/java/android/window/TaskFragmentOrganizer.java
+++ b/core/java/android/window/TaskFragmentOrganizer.java
@@ -140,16 +140,13 @@
     /**
      * Registers remote animations per transition type for the organizer. It will override the
      * animations if the transition only contains windows that belong to the organized
-     * TaskFragments in the given Task.
-     *
-     * @param taskId overrides if the transition only contains windows belonging to this Task.
+     * TaskFragments, and at least one of the transition window is embedded (not filling the Task).
      * @hide
      */
     @CallSuper
-    public void registerRemoteAnimations(int taskId,
-            @NonNull RemoteAnimationDefinition definition) {
+    public void registerRemoteAnimations(@NonNull RemoteAnimationDefinition definition) {
         try {
-            getController().registerRemoteAnimations(mInterface, taskId, definition);
+            getController().registerRemoteAnimations(mInterface, definition);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -160,9 +157,9 @@
      * @hide
      */
     @CallSuper
-    public void unregisterRemoteAnimations(int taskId) {
+    public void unregisterRemoteAnimations() {
         try {
-            getController().unregisterRemoteAnimations(mInterface, taskId);
+            getController().unregisterRemoteAnimations(mInterface);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/proto/android/server/powermanagerservice.proto b/core/proto/android/server/powermanagerservice.proto
index bd4f990..004d5d6 100644
--- a/core/proto/android/server/powermanagerservice.proto
+++ b/core/proto/android/server/powermanagerservice.proto
@@ -189,6 +189,8 @@
     optional bool is_enhanced_discharge_prediction_personalized = 54;
     optional bool is_low_power_standby_active = 55;
     optional LowPowerStandbyControllerDumpProto low_power_standby_controller = 56;
+    // The battery level drained by the dream.
+    optional int32 battery_level_drained_while_dreaming = 57;
 }
 
 // A com.android.server.power.PowerManagerService.SuspendBlockerImpl object.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 9d841ea..d7d43aa 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -37,7 +37,6 @@
 import android.window.TaskFragmentTransaction;
 import android.window.WindowContainerTransaction;
 
-import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -85,26 +84,20 @@
     @Override
     public void unregisterOrganizer() {
         if (mAnimationController != null) {
-            mAnimationController.unregisterAllRemoteAnimations();
+            mAnimationController.unregisterRemoteAnimations();
             mAnimationController = null;
         }
         super.unregisterOrganizer();
     }
 
-    /** Overrides the animation if the transition is on the given Task. */
-    void startOverrideSplitAnimation(int taskId) {
+    /**
+     * Overrides the animation for transitions of embedded activities organized by this organizer.
+     */
+    void overrideSplitAnimation() {
         if (mAnimationController == null) {
             mAnimationController = new TaskFragmentAnimationController(this);
         }
-        mAnimationController.registerRemoteAnimations(taskId);
-    }
-
-    /** No longer overrides the animation if the transition is on the given Task. */
-    @GuardedBy("mLock")
-    void stopOverrideSplitAnimation(int taskId) {
-        if (mAnimationController != null) {
-            mAnimationController.unregisterRemoteAnimations(taskId);
-        }
+        mAnimationController.registerRemoteAnimations();
     }
 
     /**
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 d52caaf..c06548a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -193,7 +193,6 @@
                         continue;
                     }
                     updateContainersInTask(wct, taskContainer);
-                    updateAnimationOverride(taskContainer);
                 }
                 // The WCT should be applied and merged to the device state change transition if
                 // there is one.
@@ -208,9 +207,6 @@
         synchronized (mLock) {
             mSplitRules.clear();
             mSplitRules.addAll(rules);
-            for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
-                updateAnimationOverride(mTaskContainers.valueAt(i));
-            }
         }
     }
 
@@ -612,7 +608,6 @@
             }
             if (taskContainer.isEmpty()) {
                 // Cleanup the TaskContainer if it becomes empty.
-                mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId());
                 mTaskContainers.remove(taskContainer.getTaskId());
             }
             return;
@@ -622,43 +617,7 @@
     @GuardedBy("mLock")
     private void onTaskContainerInfoChanged(@NonNull TaskContainer taskContainer,
             @NonNull Configuration config) {
-        final boolean wasInPip = taskContainer.isInPictureInPicture();
-        final boolean isInPIp = isInPictureInPicture(config);
-
-        // We need to check the animation override when enter/exit PIP or has bounds changed.
-        boolean shouldUpdateAnimationOverride = wasInPip != isInPIp;
-        if (taskContainer.setTaskBounds(config.windowConfiguration.getBounds())
-                && !isInPIp) {
-            // We don't care the bounds change when it has already entered PIP.
-            shouldUpdateAnimationOverride = true;
-        }
-        if (shouldUpdateAnimationOverride) {
-            updateAnimationOverride(taskContainer);
-        }
-    }
-
-    /**
-     * Updates if we should override transition animation. We only want to override if the Task
-     * bounds is large enough for at least one split rule.
-     */
-    @GuardedBy("mLock")
-    private void updateAnimationOverride(@NonNull TaskContainer taskContainer) {
-        if (ENABLE_SHELL_TRANSITIONS) {
-            // TODO(b/207070762): cleanup with legacy app transition
-            // Animation will be handled by WM Shell with Shell transition enabled.
-            return;
-        }
-        if (!taskContainer.isTaskBoundsInitialized()) {
-            // We don't know about the Task bounds/windowingMode yet.
-            return;
-        }
-
-        // We only want to override if the TaskContainer may show split.
-        if (mayShowSplit(taskContainer)) {
-            mPresenter.startOverrideSplitAnimation(taskContainer.getTaskId());
-        } else {
-            mPresenter.stopOverrideSplitAnimation(taskContainer.getTaskId());
-        }
+        taskContainer.setTaskBounds(config.windowConfiguration.getBounds());
     }
 
     /** Returns whether the given {@link TaskContainer} may show in split. */
@@ -1283,7 +1242,6 @@
                 Log.w(TAG, "Can't find bounds from activity=" + activityInTask);
             }
         }
-        updateAnimationOverride(taskContainer);
         return container;
     }
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index cb470ba..f494b32 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -139,6 +139,11 @@
         super(executor, controller);
         mController = controller;
         registerOrganizer();
+        if (!SplitController.ENABLE_SHELL_TRANSITIONS) {
+            // TODO(b/207070762): cleanup with legacy app transition
+            // Animation will be handled by WM Shell when Shell transition is enabled.
+            overrideSplitAnimation();
+        }
     }
 
     /**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
index ee2e139..d7eb9a0 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java
@@ -18,13 +18,10 @@
 
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 
-import android.util.ArraySet;
 import android.util.Log;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationDefinition;
@@ -44,8 +41,7 @@
     private final TaskFragmentAnimationRunner mRemoteRunner = new TaskFragmentAnimationRunner();
     @VisibleForTesting
     final RemoteAnimationDefinition mDefinition;
-    /** Task Ids that we have registered for remote animation. */
-    private final ArraySet<Integer> mRegisterTasks = new ArraySet<>();
+    private boolean mIsRegistered;
 
     TaskFragmentAnimationController(@NonNull TaskFragmentOrganizer organizer) {
         mOrganizer = organizer;
@@ -54,39 +50,30 @@
                 new RemoteAnimationAdapter(mRemoteRunner, 0, 0, true /* changeNeedsSnapshot */);
         mDefinition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, animationAdapter);
         mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, animationAdapter);
-        mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_OPEN, animationAdapter);
         mDefinition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_CLOSE, animationAdapter);
         mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, animationAdapter);
-        mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_CLOSE, animationAdapter);
         mDefinition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, animationAdapter);
     }
 
-    void registerRemoteAnimations(int taskId) {
+    void registerRemoteAnimations() {
         if (DEBUG) {
             Log.v(TAG, "registerRemoteAnimations");
         }
-        if (mRegisterTasks.contains(taskId)) {
+        if (mIsRegistered) {
             return;
         }
-        mOrganizer.registerRemoteAnimations(taskId, mDefinition);
-        mRegisterTasks.add(taskId);
+        mOrganizer.registerRemoteAnimations(mDefinition);
+        mIsRegistered = true;
     }
 
-    void unregisterRemoteAnimations(int taskId) {
+    void unregisterRemoteAnimations() {
         if (DEBUG) {
             Log.v(TAG, "unregisterRemoteAnimations");
         }
-        if (!mRegisterTasks.contains(taskId)) {
+        if (!mIsRegistered) {
             return;
         }
-        mOrganizer.unregisterRemoteAnimations(taskId);
-        mRegisterTasks.remove(taskId);
-    }
-
-    void unregisterAllRemoteAnimations() {
-        final ArraySet<Integer> tasks = new ArraySet<>(mRegisterTasks);
-        for (int taskId : tasks) {
-            unregisterRemoteAnimations(taskId);
-        }
+        mOrganizer.unregisterRemoteAnimations();
+        mIsRegistered = false;
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
index 8c416e8..0e13c59 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java
@@ -20,11 +20,9 @@
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
 import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
 
 import android.animation.Animator;
@@ -169,11 +167,9 @@
         switch (transit) {
             case TRANSIT_OLD_ACTIVITY_OPEN:
             case TRANSIT_OLD_TASK_FRAGMENT_OPEN:
-            case TRANSIT_OLD_TASK_OPEN:
                 return createOpenAnimationAdapters(targets);
             case TRANSIT_OLD_ACTIVITY_CLOSE:
             case TRANSIT_OLD_TASK_FRAGMENT_CLOSE:
-            case TRANSIT_OLD_TASK_CLOSE:
                 return createCloseAnimationAdapters(targets);
             case TRANSIT_OLD_TASK_FRAGMENT_CHANGE:
                 return createChangeAnimationAdapters(targets);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index 79813c7..31aa09c 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -18,7 +18,6 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 
-import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -26,10 +25,8 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 
 import android.content.Intent;
 import android.content.res.Configuration;
@@ -85,35 +82,20 @@
 
     @Test
     public void testUnregisterOrganizer() {
-        mOrganizer.startOverrideSplitAnimation(TASK_ID);
-        mOrganizer.startOverrideSplitAnimation(TASK_ID + 1);
+        mOrganizer.overrideSplitAnimation();
         mOrganizer.unregisterOrganizer();
 
-        verify(mOrganizer).unregisterRemoteAnimations(TASK_ID);
-        verify(mOrganizer).unregisterRemoteAnimations(TASK_ID + 1);
+        verify(mOrganizer).unregisterRemoteAnimations();
     }
 
     @Test
-    public void testStartOverrideSplitAnimation() {
+    public void testOverrideSplitAnimation() {
         assertNull(mOrganizer.mAnimationController);
 
-        mOrganizer.startOverrideSplitAnimation(TASK_ID);
+        mOrganizer.overrideSplitAnimation();
 
         assertNotNull(mOrganizer.mAnimationController);
-        verify(mOrganizer).registerRemoteAnimations(TASK_ID,
-                mOrganizer.mAnimationController.mDefinition);
-    }
-
-    @Test
-    public void testStopOverrideSplitAnimation() {
-        mOrganizer.stopOverrideSplitAnimation(TASK_ID);
-
-        verify(mOrganizer, never()).unregisterRemoteAnimations(anyInt());
-
-        mOrganizer.startOverrideSplitAnimation(TASK_ID);
-        mOrganizer.stopOverrideSplitAnimation(TASK_ID);
-
-        verify(mOrganizer).unregisterRemoteAnimations(TASK_ID);
+        verify(mOrganizer).registerRemoteAnimations(mOrganizer.mAnimationController.mDefinition);
     }
 
     @Test
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java
index d31342b..379ea0c 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java
@@ -16,11 +16,8 @@
 
 package androidx.window.extensions.embedding;
 
-import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
-
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
 
 import android.platform.test.annotations.Presubmit;
@@ -57,41 +54,31 @@
 
     @Test
     public void testRegisterRemoteAnimations() {
-        mAnimationController.registerRemoteAnimations(TASK_ID);
+        mAnimationController.registerRemoteAnimations();
 
-        verify(mOrganizer).registerRemoteAnimations(TASK_ID, mAnimationController.mDefinition);
+        verify(mOrganizer).registerRemoteAnimations(mAnimationController.mDefinition);
 
-        mAnimationController.registerRemoteAnimations(TASK_ID);
+        mAnimationController.registerRemoteAnimations();
 
         // No extra call if it has been registered.
-        verify(mOrganizer).registerRemoteAnimations(TASK_ID, mAnimationController.mDefinition);
+        verify(mOrganizer).registerRemoteAnimations(mAnimationController.mDefinition);
     }
 
     @Test
     public void testUnregisterRemoteAnimations() {
-        mAnimationController.unregisterRemoteAnimations(TASK_ID);
+        mAnimationController.unregisterRemoteAnimations();
 
         // No call if it is not registered.
-        verify(mOrganizer, never()).unregisterRemoteAnimations(anyInt());
+        verify(mOrganizer, never()).unregisterRemoteAnimations();
 
-        mAnimationController.registerRemoteAnimations(TASK_ID);
-        mAnimationController.unregisterRemoteAnimations(TASK_ID);
+        mAnimationController.registerRemoteAnimations();
+        mAnimationController.unregisterRemoteAnimations();
 
-        verify(mOrganizer).unregisterRemoteAnimations(TASK_ID);
+        verify(mOrganizer).unregisterRemoteAnimations();
 
-        mAnimationController.unregisterRemoteAnimations(TASK_ID);
+        mAnimationController.unregisterRemoteAnimations();
 
         // No extra call if it has been unregistered.
-        verify(mOrganizer).unregisterRemoteAnimations(TASK_ID);
-    }
-
-    @Test
-    public void testUnregisterAllRemoteAnimations() {
-        mAnimationController.registerRemoteAnimations(TASK_ID);
-        mAnimationController.registerRemoteAnimations(TASK_ID + 1);
-        mAnimationController.unregisterAllRemoteAnimations();
-
-        verify(mOrganizer).unregisterRemoteAnimations(TASK_ID);
-        verify(mOrganizer).unregisterRemoteAnimations(TASK_ID + 1);
+        verify(mOrganizer).unregisterRemoteAnimations();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 780ee5f..7558b41 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -75,7 +75,11 @@
     @JvmField
     val NOTIFICATION_DISMISSAL_FADE =
         unreleasedFlag(113, "notification_dismissal_fade", teamfood = true)
-    val STABILITY_INDEX_FIX = unreleasedFlag(114, "stability_index_fix", teamfood = true)
+
+    // TODO(b/259558771): Tracking Bug
+    val STABILITY_INDEX_FIX = releasedFlag(114, "stability_index_fix")
+
+    // TODO(b/259559750): Tracking Bug
     val SEMI_STABLE_SORT = unreleasedFlag(115, "semi_stable_sort", teamfood = true)
 
     @JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index b719177..8698c04 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -246,7 +246,6 @@
         mLp.token = new Binder();
         mLp.gravity = Gravity.TOP;
         mLp.setFitInsetsTypes(0 /* types */);
-        mLp.softInputMode = LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
         mLp.setTitle("NotificationShade");
         mLp.packageName = mContext.getPackageName();
         mLp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
@@ -384,8 +383,6 @@
             mLpChanged.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
             mLpChanged.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
         }
-
-        mLpChanged.softInputMode = LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
     }
 
     private void applyForceShowNavigationFlag(State state) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 585d871..37d82ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -676,9 +676,6 @@
         return row != null && row.areChildrenExpanded();
     }
 
-    public boolean keepInParent() {
-        return row != null && row.keepInParent();
-    }
 
     //TODO: probably less confusing to say "is group fully visible"
     public boolean isGroupNotFullyVisible() {
@@ -698,10 +695,6 @@
         return row != null && row.isSummaryWithChildren();
     }
 
-    public void setKeepInParent(boolean keep) {
-        if (row != null) row.setKeepInParent(keep);
-    }
-
     public void onDensityOrFontScaleChanged() {
         if (row != null) row.onDensityOrFontScaleChanged();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index 3ae2545..65a21a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -1160,12 +1160,21 @@
                 mLogger.logParentChanged(mIterationCount, prev.getParent(), curr.getParent());
             }
 
-            if (curr.getSuppressedChanges().getParent() != null) {
-                mLogger.logParentChangeSuppressed(
+            GroupEntry currSuppressedParent = curr.getSuppressedChanges().getParent();
+            GroupEntry prevSuppressedParent = prev.getSuppressedChanges().getParent();
+            if (currSuppressedParent != null && (prevSuppressedParent == null
+                    || !prevSuppressedParent.getKey().equals(currSuppressedParent.getKey()))) {
+                mLogger.logParentChangeSuppressedStarted(
                         mIterationCount,
-                        curr.getSuppressedChanges().getParent(),
+                        currSuppressedParent,
                         curr.getParent());
             }
+            if (prevSuppressedParent != null && currSuppressedParent == null) {
+                mLogger.logParentChangeSuppressedStopped(
+                        mIterationCount,
+                        prevSuppressedParent,
+                        prev.getParent());
+            }
 
             if (curr.getSuppressedChanges().getSection() != null) {
                 mLogger.logSectionChangeSuppressed(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index 8e052c7..4adc90a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -193,7 +193,7 @@
         })
     }
 
-    fun logParentChangeSuppressed(
+    fun logParentChangeSuppressedStarted(
         buildId: Int,
         suppressedParent: GroupEntry?,
         keepingParent: GroupEntry?
@@ -207,6 +207,21 @@
         })
     }
 
+    fun logParentChangeSuppressedStopped(
+            buildId: Int,
+            previouslySuppressedParent: GroupEntry?,
+            previouslyKeptParent: GroupEntry?
+    ) {
+        buffer.log(TAG, INFO, {
+            long1 = buildId.toLong()
+            str1 = previouslySuppressedParent?.logKey
+            str2 = previouslyKeptParent?.logKey
+        }, {
+            "(Build $long1)     Change of parent to '$str1' no longer suppressed; " +
+                    "replaced parent '$str2'"
+        })
+    }
+
     fun logGroupPruningSuppressed(
         buildId: Int,
         keepingParent: GroupEntry?
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/MediaContainerController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/MediaContainerController.kt
index f949af0..8de0381 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/MediaContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/MediaContainerController.kt
@@ -55,4 +55,8 @@
 
     override val view: View
         get() = mediaContainerView!!
+
+    override fun offerToKeepInParentForAnimation(): Boolean = false
+    override fun removeFromParentIfKeptForAnimation(): Boolean = false
+    override fun resetKeepInParentForAnimation() {}
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt
index 26ba12c..ae72a3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NodeController.kt
@@ -30,6 +30,7 @@
  * below.
  */
 interface NodeController {
+
     /** A string that uniquely(ish) represents the node in the tree. Used for debugging. */
     val nodeLabel: String
 
@@ -64,6 +65,27 @@
 
     /** Called when this view has been removed */
     fun onViewRemoved() {}
+
+    /**
+     * Called before removing a node from its parent
+     *
+     * If returned true, the ShadeViewDiffer won't detach this row and the view system is
+     * responsible for ensuring the row is in eventually removed from the parent.
+     *
+     * @return false to opt out from this feature
+     */
+    fun offerToKeepInParentForAnimation(): Boolean
+
+    /**
+     * Called before a node is reattached. Removes the view from its parent
+     * if it was flagged to be kept before.
+     *
+     * @return whether it did a removal
+     */
+    fun removeFromParentIfKeptForAnimation(): Boolean
+
+    /** Called when a node is being reattached */
+    fun resetKeepInParentForAnimation()
 }
 
 /**
@@ -90,7 +112,7 @@
 }
 
 private fun treeSpecToStrHelper(tree: NodeSpec, sb: StringBuilder, indent: String) {
-    sb.append("${indent}{${tree.controller.nodeLabel}}\n")
+    sb.append("$indent{${tree.controller.nodeLabel}}\n")
     if (tree.children.isNotEmpty()) {
         val childIndent = "$indent  "
         for (child in tree.children) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RootNodeController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RootNodeController.kt
index 2073e92..5ff686a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RootNodeController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/RootNodeController.kt
@@ -32,6 +32,9 @@
     override val view: View
 ) : NodeController, PipelineDumpable {
     override val nodeLabel: String = "<root>"
+    override fun offerToKeepInParentForAnimation(): Boolean = false
+    override fun removeFromParentIfKeptForAnimation(): Boolean = false
+    override fun resetKeepInParentForAnimation() {}
 
     override fun getChildAt(index: Int): View? {
         return listContainer.getContainerChildAt(index)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt
index 2c9508e..7b59266 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/SectionHeaderController.kt
@@ -100,4 +100,7 @@
 
     override val view: View
         get() = _view!!
+    override fun offerToKeepInParentForAnimation(): Boolean = false
+    override fun removeFromParentIfKeptForAnimation(): Boolean = false
+    override fun resetKeepInParentForAnimation() {}
 }
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
index 9a9941e..18ee481 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
@@ -86,10 +86,10 @@
     }
 
     private fun maybeDetachChild(
-        parentNode: ShadeNode,
-        parentSpec: NodeSpec?,
-        childNode: ShadeNode,
-        childSpec: NodeSpec?
+            parentNode: ShadeNode,
+            parentSpec: NodeSpec?,
+            childNode: ShadeNode,
+            childSpec: NodeSpec?
     ) {
         val newParentNode = childSpec?.parent?.let { getNode(it) }
 
@@ -100,14 +100,27 @@
                 nodes.remove(childNode.controller)
             }
 
-            logger.logDetachingChild(
-                key = childNode.label,
-                isTransfer = !childCompletelyRemoved,
-                isParentRemoved = parentSpec == null,
-                oldParent = parentNode.label,
-                newParent = newParentNode?.label)
-            parentNode.removeChild(childNode, isTransfer = !childCompletelyRemoved)
-            childNode.parent = null
+            if (childCompletelyRemoved && parentSpec == null &&
+                    childNode.offerToKeepInParentForAnimation()) {
+                // If both the child and the parent are being removed at the same time, then
+                // keep the child attached to the parent for animation purposes
+                logger.logSkipDetachingChild(
+                        key = childNode.label,
+                        parentKey = parentNode.label,
+                        isTransfer = !childCompletelyRemoved,
+                        isParentRemoved = true
+                )
+            } else {
+                logger.logDetachingChild(
+                        key = childNode.label,
+                        isTransfer = !childCompletelyRemoved,
+                        isParentRemoved = parentSpec == null,
+                        oldParent = parentNode.label,
+                        newParent = newParentNode?.label
+                )
+                parentNode.removeChild(childNode, isTransfer = !childCompletelyRemoved)
+                childNode.parent = null
+            }
         }
     }
 
@@ -119,6 +132,16 @@
             val childNode = getNode(childSpec)
 
             if (childNode.view != currView) {
+                val removedFromParent = childNode.removeFromParentIfKeptForAnimation()
+                if (removedFromParent) {
+                    logger.logDetachingChild(
+                            key = childNode.label,
+                            isTransfer = false,
+                            isParentRemoved = true,
+                            oldParent = null,
+                            newParent = null
+                    )
+                }
 
                 when (childNode.parent) {
                     null -> {
@@ -142,6 +165,8 @@
                 }
             }
 
+            childNode.resetKeepInParentForAnimation()
+
             if (childSpec.children.isNotEmpty()) {
                 attachChildren(childNode, specMap)
             }
@@ -213,4 +238,16 @@
         controller.removeChild(child.controller, isTransfer)
         child.controller.onViewRemoved()
     }
+
+    fun offerToKeepInParentForAnimation(): Boolean {
+        return controller.offerToKeepInParentForAnimation()
+    }
+
+    fun removeFromParentIfKeptForAnimation(): Boolean {
+        return controller.removeFromParentIfKeptForAnimation()
+    }
+
+    fun resetKeepInParentForAnimation() {
+        controller.resetKeepInParentForAnimation()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
index b4b9438..1e22c2c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferLogger.kt
@@ -43,6 +43,20 @@
         })
     }
 
+    fun logSkipDetachingChild(
+            key: String,
+            parentKey: String?,
+            isTransfer: Boolean,
+            isParentRemoved: Boolean
+    ) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = key
+            str2 = parentKey
+            bool1 = isTransfer
+            bool2 = isParentRemoved
+        }, { "Skip detaching $str1 from $str2 isTransfer=$bool1 isParentRemoved=$bool2" })
+    }
+
     fun logAttachingChild(key: String, parent: String, atIndex: Int) {
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = key
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 b93e150..1eccc98 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
@@ -240,7 +240,7 @@
     private NotificationContentView mPrivateLayout;
     private NotificationContentView[] mLayouts;
     private int mNotificationColor;
-    private ExpansionLogger mLogger;
+    private ExpandableNotificationRowLogger mLogger;
     private String mLoggingKey;
     private NotificationGuts mGuts;
     private NotificationEntry mEntry;
@@ -339,7 +339,7 @@
             }
         }
     };
-    private boolean mKeepInParent;
+    private boolean mKeepInParentForDismissAnimation;
     private boolean mRemoved;
     private static final Property<ExpandableNotificationRow, Float> TRANSLATE_CONTENT =
             new FloatProperty<ExpandableNotificationRow>("translate") {
@@ -825,6 +825,12 @@
         if (mChildrenContainer == null) {
             mChildrenContainerStub.inflate();
         }
+
+        if (row.keepInParentForDismissAnimation()) {
+            logSkipAttachingKeepInParentChild(row);
+            return;
+        }
+
         mChildrenContainer.addNotification(row, childIndex);
         onAttachedChildrenCountChanged();
         row.setIsChildInGroup(true, this);
@@ -833,6 +839,7 @@
     public void removeChildNotification(ExpandableNotificationRow row) {
         if (mChildrenContainer != null) {
             mChildrenContainer.removeNotification(row);
+            row.setKeepInParentForDismissAnimation(false);
         }
         onAttachedChildrenCountChanged();
         row.setIsChildInGroup(false, null);
@@ -840,6 +847,31 @@
     }
 
     /**
+     * Removes the children notifications which were marked to keep for the dismissal animation.
+     */
+    public void removeChildrenWithKeepInParent() {
+        if (mChildrenContainer == null) return;
+
+        List<ExpandableNotificationRow> clonedList = new ArrayList<>(
+                mChildrenContainer.getAttachedChildren());
+        boolean childCountChanged = false;
+        for (ExpandableNotificationRow child : clonedList) {
+            if (child.keepInParentForDismissAnimation()) {
+                mChildrenContainer.removeNotification(child);
+                child.setIsChildInGroup(false, null);
+                child.requestBottomRoundness(0.0f, /* animate = */ false, SourceType.DefaultValue);
+                child.setKeepInParentForDismissAnimation(false);
+                logKeepInParentChildDetached(child);
+                childCountChanged = true;
+            }
+        }
+
+        if (childCountChanged) {
+            onAttachedChildrenCountChanged();
+        }
+    }
+
+    /**
      * Returns the child notification at [index], or null if no such child.
      */
     @Nullable
@@ -1361,12 +1393,15 @@
         }
     }
 
-    public boolean keepInParent() {
-        return mKeepInParent;
+    /**
+     * @return  if this entry should be kept in its parent during removal.
+     */
+    public boolean keepInParentForDismissAnimation() {
+        return mKeepInParentForDismissAnimation;
     }
 
-    public void setKeepInParent(boolean keepInParent) {
-        mKeepInParent = keepInParent;
+    public void setKeepInParentForDismissAnimation(boolean keepInParent) {
+        mKeepInParentForDismissAnimation = keepInParent;
     }
 
     @Override
@@ -1537,8 +1572,29 @@
         mUseIncreasedHeadsUpHeight = use;
     }
 
-    public interface ExpansionLogger {
+    /**
+     * Interface for logging {{@link ExpandableNotificationRow} events.}
+     */
+    public interface ExpandableNotificationRowLogger {
+        /**
+         * Called when the notification is expanded / collapsed.
+         */
         void logNotificationExpansion(String key, boolean userAction, boolean expanded);
+
+        /**
+         * Called when a notification which was previously kept in its parent for the
+         * dismiss animation is finally detached from its parent.
+         */
+        void logKeepInParentChildDetached(NotificationEntry child, NotificationEntry oldParent);
+
+        /**
+         * Called when we want to attach a notification to a new parent,
+         * but it still has the keepInParent flag set, so we skip it.
+         */
+        void logSkipAttachingKeepInParentChild(
+                NotificationEntry child,
+                NotificationEntry newParent
+        );
     }
 
     public ExpandableNotificationRow(Context context, AttributeSet attrs) {
@@ -1556,7 +1612,7 @@
             RemoteInputViewSubcomponent.Factory rivSubcomponentFactory,
             String appName,
             String notificationKey,
-            ExpansionLogger logger,
+            ExpandableNotificationRowLogger logger,
             KeyguardBypassController bypassController,
             GroupMembershipManager groupMembershipManager,
             GroupExpansionManager groupExpansionManager,
@@ -3567,6 +3623,18 @@
         });
     }
 
+    private void logKeepInParentChildDetached(ExpandableNotificationRow child) {
+        if (mLogger != null) {
+            mLogger.logKeepInParentChildDetached(child.getEntry(), getEntry());
+        }
+    }
+
+    private void logSkipAttachingKeepInParentChild(ExpandableNotificationRow child) {
+        if (mLogger != null) {
+            mLogger.logSkipAttachingKeepInParentChild(child.getEntry(), getEntry());
+        }
+    }
+
     private void setTargetPoint(Point p) {
         mTargetPoint = p;
     }
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 7b53925..f9e9a2d 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
@@ -83,13 +83,11 @@
     private final GroupExpansionManager mGroupExpansionManager;
     private final RowContentBindStage mRowContentBindStage;
     private final NotificationLogger mNotificationLogger;
+    private final NotificationRowLogger mLogBufferLogger;
     private final HeadsUpManager mHeadsUpManager;
     private final ExpandableNotificationRow.OnExpandClickListener mOnExpandClickListener;
     private final StatusBarStateController mStatusBarStateController;
     private final MetricsLogger mMetricsLogger;
-
-    private final ExpandableNotificationRow.ExpansionLogger mExpansionLogger =
-            this::logNotificationExpansion;
     private final ExpandableNotificationRow.CoordinateOnClickListener mOnFeedbackClickListener;
     private final NotificationGutsManager mNotificationGutsManager;
     private final OnUserInteractionCallback mOnUserInteractionCallback;
@@ -101,8 +99,32 @@
     private final Optional<BubblesManager> mBubblesManagerOptional;
     private final SmartReplyConstants mSmartReplyConstants;
     private final SmartReplyController mSmartReplyController;
-
     private final ExpandableNotificationRowDragController mDragController;
+    private final ExpandableNotificationRow.ExpandableNotificationRowLogger mLoggerCallback =
+            new ExpandableNotificationRow.ExpandableNotificationRowLogger() {
+                @Override
+                public void logNotificationExpansion(String key, boolean userAction,
+                        boolean expanded) {
+                    mNotificationLogger.onExpansionChanged(key, userAction, expanded);
+                }
+
+                @Override
+                public void logKeepInParentChildDetached(
+                        NotificationEntry child,
+                        NotificationEntry oldParent
+                ) {
+                    mLogBufferLogger.logKeepInParentChildDetached(child, oldParent);
+                }
+
+                @Override
+                public void logSkipAttachingKeepInParentChild(
+                        NotificationEntry child,
+                        NotificationEntry newParent
+                ) {
+                    mLogBufferLogger.logSkipAttachingKeepInParentChild(child, newParent);
+                }
+            };
+
 
     @Inject
     public ExpandableNotificationRowController(
@@ -110,6 +132,7 @@
             ActivatableNotificationViewController activatableNotificationViewController,
             RemoteInputViewSubcomponent.Factory rivSubcomponentFactory,
             MetricsLogger metricsLogger,
+            NotificationRowLogger logBufferLogger,
             NotificationListContainer listContainer,
             NotificationMediaManager mediaManager,
             SmartReplyConstants smartReplyConstants,
@@ -163,6 +186,7 @@
         mBubblesManagerOptional = bubblesManagerOptional;
         mDragController = dragController;
         mMetricsLogger = metricsLogger;
+        mLogBufferLogger = logBufferLogger;
         mSmartReplyConstants = smartReplyConstants;
         mSmartReplyController = smartReplyController;
     }
@@ -177,7 +201,7 @@
                 mRemoteInputViewSubcomponentFactory,
                 mAppName,
                 mNotificationKey,
-                mExpansionLogger,
+                mLoggerCallback,
                 mKeyguardBypassController,
                 mGroupMembershipManager,
                 mGroupExpansionManager,
@@ -243,10 +267,6 @@
                 }
             };
 
-    private void logNotificationExpansion(String key, boolean userAction, boolean expanded) {
-        mNotificationLogger.onExpansionChanged(key, userAction, expanded);
-    }
-
     @Override
     @NonNull
     public String getNodeLabel() {
@@ -336,4 +356,29 @@
     public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
         mView.setFeedbackIcon(icon);
     }
+
+    @Override
+    public boolean offerToKeepInParentForAnimation() {
+        if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_DISMISSAL_ANIMATION)) {
+            mView.setKeepInParentForDismissAnimation(true);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean removeFromParentIfKeptForAnimation() {
+        ExpandableNotificationRow parent = mView.getNotificationParent();
+        if (mView.keepInParentForDismissAnimation() && parent != null) {
+            parent.removeChildNotification(mView);
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public void resetKeepInParentForAnimation() {
+        mView.setKeepInParentForDismissAnimation(false);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowLogger.kt
new file mode 100644
index 0000000..ce11be3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowLogger.kt
@@ -0,0 +1,53 @@
+/*
+ * 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 com.android.systemui.log.dagger.NotificationLog
+import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
+import javax.inject.Inject
+
+class NotificationRowLogger @Inject constructor(@NotificationLog private val buffer: LogBuffer) {
+    fun logKeepInParentChildDetached(child: NotificationEntry, oldParent: NotificationEntry?) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = child.logKey
+                str2 = oldParent.logKey
+            },
+            { "Detach child $str1 kept in parent $str2" }
+        )
+    }
+
+    fun logSkipAttachingKeepInParentChild(child: NotificationEntry, newParent: NotificationEntry?) {
+        buffer.log(
+            TAG,
+            LogLevel.WARNING,
+            {
+                str1 = child.logKey
+                str2 = newParent.logKey
+            },
+            { "Skipping to attach $str1 to $str2, because it still flagged to keep in parent" }
+        )
+    }
+}
+
+private const val TAG = "NotifRow"
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 79700d0..2c096f5 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
@@ -2774,6 +2774,10 @@
             }
         } else {
             mSwipedOutViews.remove(child);
+
+            if (child instanceof ExpandableNotificationRow) {
+                ((ExpandableNotificationRow) child).removeChildrenWithKeepInParent();
+            }
         }
         updateAnimationState(false, child);
 
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 0240bbc..ad4501a 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
@@ -443,7 +443,11 @@
                     if (!row.isDismissed()) {
                         handleChildViewDismissed(view);
                     }
+
                     row.removeFromTransientContainer();
+                    if (row instanceof ExpandableNotificationRow) {
+                        ((ExpandableNotificationRow) row).removeChildrenWithKeepInParent();
+                    }
                 }
 
                 /**
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
index d411e34..ae30ca0 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/UnfoldLightRevealOverlayAnimation.kt
@@ -94,7 +94,7 @@
                 overlayContainer = builder.build()
 
                 SurfaceControl.Transaction()
-                    .setLayer(overlayContainer, Integer.MAX_VALUE)
+                    .setLayer(overlayContainer, UNFOLD_OVERLAY_LAYER_Z_INDEX)
                     .show(overlayContainer)
                     .apply()
 
@@ -268,4 +268,12 @@
                 this.isFolded = isFolded
             }
         )
+
+    private companion object {
+        private const val ROTATION_ANIMATION_OVERLAY_Z_INDEX = Integer.MAX_VALUE
+
+        // Put the unfold overlay below the rotation animation screenshot to hide the moment
+        // when it is rotated but the rotation of the other windows hasn't happen yet
+        private const val UNFOLD_OVERLAY_LAYER_Z_INDEX = ROTATION_ANIMATION_OVERLAY_Z_INDEX - 1
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 0e4f224..6672469 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -66,7 +66,6 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
@@ -138,18 +137,12 @@
     /** The currently-selected user. */
     val selectedUser: Flow<UserModel>
         get() =
-            combine(
-                repository.selectedUserInfo,
-                repository.userSwitcherSettings,
-            ) { selectedUserInfo, settings ->
+            repository.selectedUserInfo.map { selectedUserInfo ->
                 val selectedUserId = selectedUserInfo.id
-                checkNotNull(
-                    toUserModel(
-                        userInfo = selectedUserInfo,
-                        selectedUserId = selectedUserId,
-                        canSwitchUsers = canSwitchUsers(selectedUserId),
-                        isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
-                    )
+                toUserModel(
+                    userInfo = selectedUserInfo,
+                    selectedUserId = selectedUserId,
+                    canSwitchUsers = canSwitchUsers(selectedUserId)
                 )
             }
 
@@ -629,7 +622,7 @@
             // The guest user should go in the last position.
             .sortedBy { it.isGuest }
             .mapNotNull { userInfo ->
-                toUserModel(
+                filterAndMapToUserModel(
                     userInfo = userInfo,
                     selectedUserId = selectedUserId,
                     canSwitchUsers = canSwitchUsers,
@@ -638,51 +631,65 @@
             }
     }
 
-    private suspend fun toUserModel(
+    /**
+     * Maps UserInfo to UserModel based on some parameters and return null under certain conditions
+     * to be filtered out.
+     */
+    private suspend fun filterAndMapToUserModel(
         userInfo: UserInfo,
         selectedUserId: Int,
         canSwitchUsers: Boolean,
         isUserSwitcherEnabled: Boolean,
     ): UserModel? {
-        val userId = userInfo.id
-        val isSelected = userId == selectedUserId
-
         return when {
             // When the user switcher is not enabled in settings, we only show the primary user.
             !isUserSwitcherEnabled && !userInfo.isPrimary -> null
-
             // We avoid showing disabled users.
             !userInfo.isEnabled -> null
-            userInfo.isGuest ->
-                UserModel(
-                    id = userId,
-                    name = Text.Loaded(userInfo.name),
-                    image =
-                        getUserImage(
-                            isGuest = true,
-                            userId = userId,
-                        ),
-                    isSelected = isSelected,
-                    isSelectable = canSwitchUsers,
-                    isGuest = true,
-                )
-            userInfo.supportsSwitchToByUser() ->
-                UserModel(
-                    id = userId,
-                    name = Text.Loaded(userInfo.name),
-                    image =
-                        getUserImage(
-                            isGuest = false,
-                            userId = userId,
-                        ),
-                    isSelected = isSelected,
-                    isSelectable = canSwitchUsers || isSelected,
-                    isGuest = false,
-                )
+            // We meet the conditions to return the UserModel.
+            userInfo.isGuest || userInfo.supportsSwitchToByUser() ->
+                toUserModel(userInfo, selectedUserId, canSwitchUsers)
             else -> null
         }
     }
 
+    /** Maps UserInfo to UserModel based on some parameters. */
+    private suspend fun toUserModel(
+        userInfo: UserInfo,
+        selectedUserId: Int,
+        canSwitchUsers: Boolean
+    ): UserModel {
+        val userId = userInfo.id
+        val isSelected = userId == selectedUserId
+        return if (userInfo.isGuest) {
+            UserModel(
+                id = userId,
+                name = Text.Loaded(userInfo.name),
+                image =
+                    getUserImage(
+                        isGuest = true,
+                        userId = userId,
+                    ),
+                isSelected = isSelected,
+                isSelectable = canSwitchUsers,
+                isGuest = true,
+            )
+        } else {
+            UserModel(
+                id = userId,
+                name = Text.Loaded(userInfo.name),
+                image =
+                    getUserImage(
+                        isGuest = false,
+                        userId = userId,
+                    ),
+                isSelected = isSelected,
+                isSelectable = canSwitchUsers || isSelected,
+                isGuest = false,
+            )
+        }
+    }
+
     private suspend fun canSwitchUsers(selectedUserId: Int): Boolean {
         return withContext(backgroundDispatcher) {
             manager.getUserSwitchability(UserHandle.of(selectedUserId))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 3b05321..94e3e6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -92,6 +92,7 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -741,22 +742,24 @@
     @Test
     public void testGroupChildrenAreDismissedLocallyWhenSummaryIsDismissed() {
         // GIVEN a collection with two grouped notifs in it
-        CollectionEvent notif0 = postNotif(
+        CollectionEvent groupNotif = postNotif(
                 buildNotif(TEST_PACKAGE, 0)
                         .setGroup(mContext, GROUP_1)
                         .setGroupSummary(mContext, true));
-        CollectionEvent notif1 = postNotif(
+        CollectionEvent childNotif = postNotif(
                 buildNotif(TEST_PACKAGE, 1)
                         .setGroup(mContext, GROUP_1));
-        NotificationEntry entry0 = mCollectionListener.getEntry(notif0.key);
-        NotificationEntry entry1 = mCollectionListener.getEntry(notif1.key);
+        NotificationEntry groupEntry = mCollectionListener.getEntry(groupNotif.key);
+        NotificationEntry childEntry = mCollectionListener.getEntry(childNotif.key);
+        ExpandableNotificationRow childRow = mock(ExpandableNotificationRow.class);
+        childEntry.setRow(childRow);
 
         // WHEN the summary is dismissed
-        mCollection.dismissNotification(entry0, defaultStats(entry0));
+        mCollection.dismissNotification(groupEntry, defaultStats(groupEntry));
 
         // THEN all members of the group are marked as dismissed locally
-        assertEquals(DISMISSED, entry0.getDismissState());
-        assertEquals(PARENT_DISMISSED, entry1.getDismissState());
+        assertEquals(DISMISSED, groupEntry.getDismissState());
+        assertEquals(PARENT_DISMISSED, childEntry.getDismissState());
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt
index 15cf17d..6167b46 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDifferTest.kt
@@ -18,6 +18,7 @@
 import android.content.Context
 import android.testing.AndroidTestingRunner
 import android.view.View
+import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -26,6 +27,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.isNull
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.matches
+import org.mockito.Mockito.verify
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -124,6 +129,64 @@
         Assert.assertNull(controller3.view.parent)
         Assert.assertNull(controller4.view.parent)
         Assert.assertNull(controller5.view.parent)
+        verifyDetachingChildLogged(controller3, oldParent = controller2)
+        verifyDetachingChildLogged(controller4, oldParent = controller2)
+        verifyDetachingChildLogged(controller5, oldParent = controller2)
+    }
+
+    @Test
+    fun testRemovedGroupsWithKeepInParentAreKeptTogether() {
+        // GIVEN a preexisting tree with a group
+        // AND the group children supports keepInParent
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2, node(controller3), node(controller4), node(controller5))
+        )
+        controller3.supportsKeepInParent = true
+        controller4.supportsKeepInParent = true
+        controller5.supportsKeepInParent = true
+
+        // WHEN the new spec removes the entire group
+        applySpecAndCheck(node(controller1))
+
+        // THEN the group children are still attached to their parent
+        Assert.assertEquals(controller2.view, controller3.view.parent)
+        Assert.assertEquals(controller2.view, controller4.view.parent)
+        Assert.assertEquals(controller2.view, controller5.view.parent)
+        verifySkipDetachingChildLogged(controller3, parent = controller2)
+        verifySkipDetachingChildLogged(controller4, parent = controller2)
+        verifySkipDetachingChildLogged(controller5, parent = controller2)
+    }
+
+    @Test
+    fun testReuseRemovedGroupsWithKeepInParent() {
+        // GIVEN a preexisting tree with a dismissed group
+        // AND the group children supports keepInParent
+        controller3.supportsKeepInParent = true
+        controller4.supportsKeepInParent = true
+        controller5.supportsKeepInParent = true
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2, node(controller3), node(controller4), node(controller5))
+        )
+        applySpecAndCheck(node(controller1))
+
+        // WHEN a new spec is applied which reuses the dismissed views
+        applySpecAndCheck(
+            node(controller1),
+            node(controller2),
+            node(controller3),
+            node(controller4),
+            node(controller5)
+        )
+
+        // THEN the dismissed views can be reused
+        Assert.assertEquals(rootController.view, controller3.view.parent)
+        Assert.assertEquals(rootController.view, controller4.view.parent)
+        Assert.assertEquals(rootController.view, controller5.view.parent)
+        verifyDetachingChildLogged(controller3, oldParent = null)
+        verifyDetachingChildLogged(controller4, oldParent = null)
+        verifyDetachingChildLogged(controller5, oldParent = null)
     }
 
     @Test
@@ -184,7 +247,30 @@
         }
     }
 
+    private fun verifySkipDetachingChildLogged(child: NodeController, parent: NodeController) {
+        verify(logger)
+            .logSkipDetachingChild(
+                key = matches(child.nodeLabel),
+                parentKey = matches(parent.nodeLabel),
+                anyBoolean(),
+                anyBoolean()
+            )
+    }
+
+    private fun verifyDetachingChildLogged(child: NodeController, oldParent: NodeController?) {
+        verify(logger)
+            .logDetachingChild(
+                key = matches(child.nodeLabel),
+                isTransfer = anyBoolean(),
+                isParentRemoved = anyBoolean(),
+                oldParent = oldParent?.let { matches(it.nodeLabel) } ?: isNull(),
+                newParent = isNull()
+            )
+    }
+
     private class FakeController(context: Context, label: String) : NodeController {
+        var supportsKeepInParent: Boolean = false
+
         override val view: FrameLayout = FrameLayout(context)
         override val nodeLabel: String = label
         override fun getChildCount(): Int = view.childCount
@@ -209,6 +295,22 @@
         override fun onViewAdded() {}
         override fun onViewMoved() {}
         override fun onViewRemoved() {}
+        override fun offerToKeepInParentForAnimation(): Boolean {
+            return supportsKeepInParent
+        }
+
+        override fun removeFromParentIfKeptForAnimation(): Boolean {
+            if (supportsKeepInParent) {
+                (view.parent as? ViewGroup)?.removeView(view)
+                return true
+            }
+
+            return false
+        }
+
+        override fun resetKeepInParentForAnimation() {
+            supportsKeepInParent = false
+        }
     }
 
     private class SpecBuilder(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 12cc114..ee8db18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.app.Notification;
@@ -458,4 +459,79 @@
         verify(mNotificationTestHelper.mOnUserInteractionCallback, never())
                 .registerFutureDismissal(any(), anyInt());
     }
+
+    @Test
+    public void testAddChildNotification() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup(0);
+        ExpandableNotificationRow child = mNotificationTestHelper.createRow();
+
+        group.addChildNotification(child);
+
+        Assert.assertEquals(child, group.getChildNotificationAt(0));
+        Assert.assertEquals(group, child.getNotificationParent());
+        Assert.assertTrue(child.isChildInGroup());
+    }
+
+    @Test
+    public void testAddChildNotification_childSkipped() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup(0);
+        ExpandableNotificationRow child = mNotificationTestHelper.createRow();
+        child.setKeepInParentForDismissAnimation(true);
+
+        group.addChildNotification(child);
+
+        Assert.assertTrue(group.getAttachedChildren().isEmpty());
+        Assert.assertNotEquals(group, child.getNotificationParent());
+        verify(mNotificationTestHelper.getMockLogger()).logSkipAttachingKeepInParentChild(
+                /*child=*/ child.getEntry(),
+                /*newParent=*/ group.getEntry()
+        );
+    }
+
+    @Test
+    public void testRemoveChildNotification() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup(1);
+        ExpandableNotificationRow child = group.getAttachedChildren().get(0);
+        child.setKeepInParentForDismissAnimation(true);
+
+        group.removeChildNotification(child);
+
+        Assert.assertNull(child.getParent());
+        Assert.assertNull(child.getNotificationParent());
+        Assert.assertFalse(child.keepInParentForDismissAnimation());
+        verifyNoMoreInteractions(mNotificationTestHelper.getMockLogger());
+    }
+
+    @Test
+    public void testRemoveChildrenWithKeepInParent_removesChildWithKeepInParent() throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup(1);
+        ExpandableNotificationRow child = group.getAttachedChildren().get(0);
+        child.setKeepInParentForDismissAnimation(true);
+
+        group.removeChildrenWithKeepInParent();
+
+        Assert.assertNull(child.getParent());
+        Assert.assertNull(child.getNotificationParent());
+        Assert.assertFalse(child.keepInParentForDismissAnimation());
+        verify(mNotificationTestHelper.getMockLogger()).logKeepInParentChildDetached(
+                /*child=*/ child.getEntry(),
+                /*oldParent=*/ group.getEntry()
+        );
+    }
+
+    @Test
+    public void testRemoveChildrenWithKeepInParent_skipsChildrenWithoutKeepInParent()
+            throws Exception {
+        ExpandableNotificationRow group = mNotificationTestHelper.createGroup(1);
+        ExpandableNotificationRow child = group.getAttachedChildren().get(0);
+
+        group.removeChildrenWithKeepInParent();
+
+        Assert.assertEquals(group, child.getNotificationParent());
+        Assert.assertFalse(child.keepInParentForDismissAnimation());
+        verify(mNotificationTestHelper.getMockLogger(), never()).logKeepInParentChildDetached(
+                /*child=*/ any(),
+                /*oldParent=*/ any()
+        );
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index ab4ae6a..5fd9448 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -73,7 +73,7 @@
 import com.android.systemui.statusbar.notification.icon.IconBuilder;
 import com.android.systemui.statusbar.notification.icon.IconManager;
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpansionLogger;
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener;
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
@@ -116,6 +116,7 @@
     private final Context mContext;
     private final TestableLooper mTestLooper;
     private int mId;
+    private final ExpandableNotificationRowLogger mMockLogger;
     private final GroupMembershipManager mGroupMembershipManager;
     private final GroupExpansionManager mGroupExpansionManager;
     private ExpandableNotificationRow mRow;
@@ -139,6 +140,7 @@
         dependency.injectMockDependency(NotificationMediaManager.class);
         dependency.injectMockDependency(NotificationShadeWindowController.class);
         dependency.injectMockDependency(MediaOutputDialogFactory.class);
+        mMockLogger = mock(ExpandableNotificationRowLogger.class);
         mStatusBarStateController = mock(StatusBarStateController.class);
         mGroupMembershipManager = mock(GroupMembershipManager.class);
         mGroupExpansionManager = mock(GroupExpansionManager.class);
@@ -197,6 +199,10 @@
         mDefaultInflationFlags = defaultInflationFlags;
     }
 
+    public ExpandableNotificationRowLogger getMockLogger() {
+        return mMockLogger;
+    }
+
     /**
      * Creates a generic row with rounded border.
      *
@@ -527,7 +533,7 @@
                 mock(RemoteInputViewSubcomponent.Factory.class),
                 APP_NAME,
                 entry.getKey(),
-                mock(ExpansionLogger.class),
+                mMockLogger,
                 mock(KeyguardBypassController.class),
                 mGroupMembershipManager,
                 mGroupExpansionManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index d877209..47efcd9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -793,6 +793,19 @@
             job.cancel()
         }
 
+    @Test
+    fun `current user is not primary and user switcher is disabled`() =
+        runBlocking(IMMEDIATE) {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[1])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+            var selectedUser: UserModel? = null
+            val job = underTest.selectedUser.onEach { selectedUser = it }.launchIn(this)
+            assertThat(selectedUser).isNotNull()
+            job.cancel()
+        }
+
     private fun assertUsers(
         models: List<UserModel>?,
         count: Int,
diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java
index 2f8dea7..e282679 100644
--- a/services/core/java/com/android/server/BatteryService.java
+++ b/services/core/java/com/android/server/BatteryService.java
@@ -1287,6 +1287,13 @@
         }
 
         @Override
+        public int getBatteryHealth() {
+            synchronized (mLock) {
+                return mHealthInfo.batteryHealth;
+            }
+        }
+
+        @Override
         public boolean getBatteryLevelLow() {
             synchronized (mLock) {
                 return mBatteryLevelLow;
diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index f867808..c9b3597 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -367,7 +367,7 @@
                 return;
             }
 
-            int newState = mOrderedStates[0].getIdentifier();
+            int newState = INVALID_DEVICE_STATE;
             for (int i = 0; i < mOrderedStates.length; i++) {
                 int state = mOrderedStates[i].getIdentifier();
                 if (DEBUG) {
@@ -396,7 +396,7 @@
                 }
             }
 
-            if (newState != mLastReportedState) {
+            if (newState != INVALID_DEVICE_STATE && newState != mLastReportedState) {
                 mLastReportedState = newState;
                 stateToReport = newState;
             }
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index ab90ef9..0d03133 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -419,6 +419,9 @@
     // The current battery level percentage.
     private int mBatteryLevel;
 
+    // The amount of battery drained while the device has been in a dream state.
+    private int mDreamsBatteryLevelDrain;
+
     // True if updatePowerStateLocked() is already in progress.
     // TODO(b/215518989): Remove this once transactions are in place
     private boolean mUpdatePowerStateInProgress;
@@ -451,11 +454,6 @@
     @GuardedBy("mEnhancedDischargeTimeLock")
     private boolean mEnhancedDischargePredictionIsPersonalized;
 
-    // The battery level percentage at the time the dream started.
-    // This is used to terminate a dream and go to sleep if the battery is
-    // draining faster than it is charging and the user activity timeout has expired.
-    private int mBatteryLevelWhenDreamStarted;
-
     // The current dock state.
     private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
 
@@ -2462,15 +2460,25 @@
             final int oldPlugType = mPlugType;
             mIsPowered = mBatteryManagerInternal.isPowered(BatteryManager.BATTERY_PLUGGED_ANY);
             mPlugType = mBatteryManagerInternal.getPlugType();
+            final int oldBatteryLevel = mBatteryLevel;
             mBatteryLevel = mBatteryManagerInternal.getBatteryLevel();
             mBatteryLevelLow = mBatteryManagerInternal.getBatteryLevelLow();
+            final boolean isOverheat = mBatteryManagerInternal.getBatteryHealth()
+                    == BatteryManager.BATTERY_HEALTH_OVERHEAT;
 
             if (DEBUG_SPEW) {
                 Slog.d(TAG, "updateIsPoweredLocked: wasPowered=" + wasPowered
                         + ", mIsPowered=" + mIsPowered
                         + ", oldPlugType=" + oldPlugType
                         + ", mPlugType=" + mPlugType
-                        + ", mBatteryLevel=" + mBatteryLevel);
+                        + ", oldBatteryLevel=" + oldBatteryLevel
+                        + ", mBatteryLevel=" + mBatteryLevel
+                        + ", isOverheat=" + isOverheat);
+            }
+
+            if (!isOverheat && oldBatteryLevel > 0
+                    && getGlobalWakefulnessLocked() == WAKEFULNESS_DREAMING) {
+                mDreamsBatteryLevelDrain += (oldBatteryLevel - mBatteryLevel);
             }
 
             if (wasPowered != mIsPowered || oldPlugType != mPlugType) {
@@ -3292,7 +3300,7 @@
 
             // Remember the initial battery level when the dream started.
             if (startDreaming && isDreaming) {
-                mBatteryLevelWhenDreamStarted = mBatteryLevel;
+                mDreamsBatteryLevelDrain = 0;
                 if (wakefulness == WAKEFULNESS_DOZING) {
                     Slog.i(TAG, "Dozing...");
                 } else {
@@ -3313,16 +3321,15 @@
             if (wakefulness == WAKEFULNESS_DREAMING) {
                 if (isDreaming && canDreamLocked(powerGroup)) {
                     if (mDreamsBatteryLevelDrainCutoffConfig >= 0
-                            && mBatteryLevel < mBatteryLevelWhenDreamStarted
-                                    - mDreamsBatteryLevelDrainCutoffConfig
+                            && mDreamsBatteryLevelDrain > mDreamsBatteryLevelDrainCutoffConfig
                             && !isBeingKeptAwakeLocked(powerGroup)) {
                         // If the user activity timeout expired and the battery appears
                         // to be draining faster than it is charging then stop dreaming
                         // and go to sleep.
                         Slog.i(TAG, "Stopping dream because the battery appears to "
                                 + "be draining faster than it is charging.  "
-                                + "Battery level when dream started: "
-                                + mBatteryLevelWhenDreamStarted + "%.  "
+                                + "Battery level drained while dreaming: "
+                                + mDreamsBatteryLevelDrain + "%.  "
                                 + "Battery level now: " + mBatteryLevel + "%.");
                     } else {
                         return; // continue dreaming
@@ -3553,6 +3560,11 @@
                 mScreenBrightnessBoostInProgress);
     }
 
+    @VisibleForTesting
+    int getDreamsBatteryLevelDrain() {
+        return mDreamsBatteryLevelDrain;
+    }
+
     private final DisplayManagerInternal.DisplayPowerCallbacks mDisplayPowerCallbacks =
             new DisplayManagerInternal.DisplayPowerCallbacks() {
 
@@ -4397,7 +4409,7 @@
             pw.println("  mIsPowered=" + mIsPowered);
             pw.println("  mPlugType=" + mPlugType);
             pw.println("  mBatteryLevel=" + mBatteryLevel);
-            pw.println("  mBatteryLevelWhenDreamStarted=" + mBatteryLevelWhenDreamStarted);
+            pw.println("  mDreamsBatteryLevelDrain=" + mDreamsBatteryLevelDrain);
             pw.println("  mDockState=" + mDockState);
             pw.println("  mStayOn=" + mStayOn);
             pw.println("  mProximityPositive=" + mProximityPositive);
@@ -4638,8 +4650,8 @@
             proto.write(PowerManagerServiceDumpProto.PLUG_TYPE, mPlugType);
             proto.write(PowerManagerServiceDumpProto.BATTERY_LEVEL, mBatteryLevel);
             proto.write(
-                    PowerManagerServiceDumpProto.BATTERY_LEVEL_WHEN_DREAM_STARTED,
-                    mBatteryLevelWhenDreamStarted);
+                    PowerManagerServiceDumpProto.BATTERY_LEVEL_DRAINED_WHILE_DREAMING,
+                    mDreamsBatteryLevelDrain);
             proto.write(PowerManagerServiceDumpProto.DOCK_STATE, mDockState);
             proto.write(PowerManagerServiceDumpProto.IS_STAY_ON, mStayOn);
             proto.write(PowerManagerServiceDumpProto.IS_PROXIMITY_POSITIVE, mProximityPositive);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index fc15890..66992aa 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -5261,42 +5261,7 @@
         }
         // If we are preparing an app transition, then delay changing
         // the visibility of this token until we execute that transition.
-        // Note that we ignore display frozen since we want the opening / closing transition type
-        // can be updated correctly even display frozen, and it's safe since in applyAnimation will
-        // still check DC#okToAnimate again if the transition animation is fine to apply.
-        // TODO(new-app-transition): Rewrite this logic using WM Shell.
-        final boolean recentsAnimating = isAnimating(PARENTS, ANIMATION_TYPE_RECENTS);
-        final boolean isEnteringPipWithoutVisibleChange = mWaitForEnteringPinnedMode
-                && mVisible == visible;
-        if (okToAnimate(true /* ignoreFrozen */, canTurnScreenOn())
-                && (appTransition.isTransitionSet()
-                || (recentsAnimating && !isActivityTypeHome()))
-                // If the visibility is not changed during enter PIP, we don't want to include it in
-                // app transition to affect the animation theme, because the Pip organizer will
-                // animate the entering PIP instead.
-                && !isEnteringPipWithoutVisibleChange) {
-            if (visible) {
-                displayContent.mOpeningApps.add(this);
-                mEnteringAnimation = true;
-            } else if (mVisible) {
-                displayContent.mClosingApps.add(this);
-                mEnteringAnimation = false;
-            }
-            if ((appTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0) {
-                // We're launchingBehind, add the launching activity to mOpeningApps.
-                final WindowState win = getDisplayContent().findFocusedWindow();
-                if (win != null) {
-                    final ActivityRecord focusedActivity = win.mActivityRecord;
-                    if (focusedActivity != null) {
-                        ProtoLog.d(WM_DEBUG_APP_TRANSITIONS,
-                                "TRANSIT_FLAG_OPEN_BEHIND,  adding %s to mOpeningApps",
-                                focusedActivity);
-
-                        // Force animation to be loaded.
-                        displayContent.mOpeningApps.add(focusedActivity);
-                    }
-                }
-            }
+        if (deferCommitVisibilityChange(visible)) {
             return;
         }
 
@@ -5304,6 +5269,61 @@
         updateReportedVisibilityLocked();
     }
 
+    /**
+     * Returns {@code true} if this activity is either added to opening-apps or closing-apps.
+     * Then its visibility will be committed until the transition is ready.
+     */
+    private boolean deferCommitVisibilityChange(boolean visible) {
+        if (!mDisplayContent.mAppTransition.isTransitionSet()) {
+            if (mTransitionController.isShellTransitionsEnabled()) {
+                // Shell transition doesn't use opening/closing sets.
+                return false;
+            }
+            // Defer committing visibility for non-home app which is animating by recents.
+            if (isActivityTypeHome() || !isAnimating(PARENTS, ANIMATION_TYPE_RECENTS)) {
+                return false;
+            }
+        }
+        if (mWaitForEnteringPinnedMode && mVisible == visible) {
+            // If the visibility is not changed during enter PIP, we don't want to include it in
+            // app transition to affect the animation theme, because the Pip organizer will
+            // animate the entering PIP instead.
+            return false;
+        }
+
+        // The animation will be visible soon so do not skip by screen off.
+        final boolean ignoreScreenOn = canTurnScreenOn() || mTaskSupervisor.getKeyguardController()
+                .isKeyguardGoingAway(mDisplayContent.mDisplayId);
+        // Ignore display frozen so the opening / closing transition type can be updated correctly
+        // even if the display is frozen. And it's safe since in applyAnimation will still check
+        // DC#okToAnimate again if the transition animation is fine to apply.
+        if (!okToAnimate(true /* ignoreFrozen */, ignoreScreenOn)) {
+            return false;
+        }
+        if (visible) {
+            mDisplayContent.mOpeningApps.add(this);
+            mEnteringAnimation = true;
+        } else if (mVisible) {
+            mDisplayContent.mClosingApps.add(this);
+            mEnteringAnimation = false;
+        }
+        if ((mDisplayContent.mAppTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0) {
+            // Add the launching-behind activity to mOpeningApps.
+            final WindowState win = mDisplayContent.findFocusedWindow();
+            if (win != null) {
+                final ActivityRecord focusedActivity = win.mActivityRecord;
+                if (focusedActivity != null) {
+                    ProtoLog.d(WM_DEBUG_APP_TRANSITIONS,
+                            "TRANSIT_FLAG_OPEN_BEHIND,  adding %s to mOpeningApps",
+                            focusedActivity);
+                    // Force animation to be loaded.
+                    mDisplayContent.mOpeningApps.add(focusedActivity);
+                }
+            }
+        }
+        return true;
+    }
+
     @Override
     boolean applyAnimation(LayoutParams lp, @TransitionOldType int transit, boolean enter,
             boolean isVoiceInteraction, @Nullable ArrayList<WindowContainer> sources) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 54664f5..6eaeb15e 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -2593,7 +2593,8 @@
                         // Apply options to prevent pendingOptions be taken when scheduling
                         // activity lifecycle transaction to make sure the override pending app
                         // transition will be applied immediately.
-                        if (activityOptions.getAnimationType() == ANIM_REMOTE_ANIMATION) {
+                        if (activityOptions != null
+                                && activityOptions.getAnimationType() == ANIM_REMOTE_ANIMATION) {
                             targetActivity.mPendingRemoteAnimation =
                                     activityOptions.getRemoteAnimationAdapter();
                         }
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index 27370bf..adf862b 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -169,7 +169,8 @@
     private final WindowManagerService mService;
     private final DisplayContent mDisplayContent;
 
-    private final TransitionAnimation mTransitionAnimation;
+    @VisibleForTesting
+    final TransitionAnimation mTransitionAnimation;
 
     private @TransitionFlags int mNextAppTransitionFlags = 0;
     private final ArrayList<Integer> mNextAppTransitionRequests = new ArrayList<>();
@@ -315,10 +316,33 @@
         setAppTransitionState(APP_STATE_TIMEOUT);
     }
 
+    /**
+     * Gets the animation overridden by app via {@link #overridePendingAppTransition}.
+     */
+    @Nullable
+    Animation getNextAppRequestedAnimation(boolean enter) {
+        final Animation a = mTransitionAnimation.loadAppTransitionAnimation(
+                mNextAppTransitionPackage,
+                enter ? mNextAppTransitionEnter : mNextAppTransitionExit);
+        if (mNextAppTransitionBackgroundColor != 0 && a != null) {
+            a.setBackdropColor(mNextAppTransitionBackgroundColor);
+        }
+        return a;
+    }
+
+    /**
+     * Gets the animation background color overridden by app via
+     * {@link #overridePendingAppTransition}.
+     */
     @ColorInt int getNextAppTransitionBackgroundColor() {
         return mNextAppTransitionBackgroundColor;
     }
 
+    @VisibleForTesting
+    boolean isNextAppTransitionOverrideRequested() {
+        return mNextAppTransitionOverrideRequested;
+    }
+
     HardwareBuffer getAppTransitionThumbnailHeader(WindowContainer container) {
         AppTransitionAnimationSpec spec = mNextAppTransitionAnimationsSpecs.get(
                 container.hashCode());
@@ -411,9 +435,12 @@
     }
 
     void clear() {
+        clear(true /* clearAppOverride */);
+    }
+
+    private void clear(boolean clearAppOverride) {
         mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE;
         mNextAppTransitionOverrideRequested = false;
-        mNextAppTransitionPackage = null;
         mNextAppTransitionAnimationsSpecs.clear();
         mRemoteAnimationController = null;
         mNextAppTransitionAnimationsSpecsFuture = null;
@@ -421,6 +448,12 @@
         mAnimationFinishedCallback = null;
         mOverrideTaskTransition = false;
         mNextAppTransitionIsSync = false;
+        if (clearAppOverride) {
+            mNextAppTransitionPackage = null;
+            mNextAppTransitionEnter = 0;
+            mNextAppTransitionExit = 0;
+            mNextAppTransitionBackgroundColor = 0;
+        }
     }
 
     void freeze() {
@@ -528,7 +561,7 @@
         return TransitionAnimation.loadAnimationSafely(context, resId, TAG);
     }
 
-    static int mapOpenCloseTransitTypes(int transit, boolean enter) {
+    private static int mapOpenCloseTransitTypes(int transit, boolean enter) {
         int animAttr = 0;
         switch (transit) {
             case TRANSIT_OLD_ACTIVITY_OPEN:
@@ -788,11 +821,7 @@
                     "applyAnimation: anim=%s transit=%s Callers=%s", a,
                     appTransitionOldToString(transit), Debug.getCallers(3));
         } else if (mNextAppTransitionType == NEXT_TRANSIT_TYPE_CUSTOM) {
-            a = mTransitionAnimation.loadAppTransitionAnimation(mNextAppTransitionPackage,
-                    enter ? mNextAppTransitionEnter : mNextAppTransitionExit);
-            if (mNextAppTransitionBackgroundColor != 0) {
-                a.setBackdropColor(mNextAppTransitionBackgroundColor);
-            }
+            a = getNextAppRequestedAnimation(enter);
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM,
                     "applyAnimation: anim=%s nextAppTransition=ANIM_CUSTOM transit=%s "
                             + "isEntrance=%b Callers=%s",
@@ -1032,7 +1061,9 @@
         ProtoLog.i(WM_DEBUG_APP_TRANSITIONS, "Override pending remote transitionSet=%b adapter=%s",
                         isTransitionSet(), remoteAnimationAdapter);
         if (isTransitionSet() && !mNextAppTransitionIsSync) {
-            clear();
+            // ActivityEmbedding animation will run by the app process for which we want to respect
+            // the app override for whether or not to show background color.
+            clear(!isActivityEmbedding /* clearAppOverride */);
             mNextAppTransitionType = NEXT_TRANSIT_TYPE_REMOTE;
             mRemoteAnimationController = new RemoteAnimationController(mService, mDisplayContent,
                     remoteAnimationAdapter, mHandler, isActivityEmbedding);
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 12133bc..d5c9e66 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -562,6 +562,34 @@
     }
 
     /**
+     * Whether the transition contains any embedded {@link TaskFragment} that does not fill the
+     * parent {@link Task} before or after the transition.
+     */
+    private boolean transitionContainsTaskFragmentWithBoundsOverride() {
+        for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) {
+            final WindowContainer wc = mDisplayContent.mChangingContainers.valueAt(i);
+            if (wc.isEmbedded()) {
+                // Contains embedded TaskFragment with bounds changed.
+                return true;
+            }
+        }
+        mTempTransitionWindows.clear();
+        mTempTransitionWindows.addAll(mDisplayContent.mClosingApps);
+        mTempTransitionWindows.addAll(mDisplayContent.mOpeningApps);
+        boolean containsTaskFragmentWithBoundsOverride = false;
+        for (int i = mTempTransitionWindows.size() - 1; i >= 0; i--) {
+            final ActivityRecord r = mTempTransitionWindows.get(i).asActivityRecord();
+            final TaskFragment tf = r.getTaskFragment();
+            if (tf != null && tf.isEmbeddedWithBoundsOverride()) {
+                containsTaskFragmentWithBoundsOverride = true;
+                break;
+            }
+        }
+        mTempTransitionWindows.clear();
+        return containsTaskFragmentWithBoundsOverride;
+    }
+
+    /**
      * Finds the common parent {@link Task} that is parent of all embedded app windows in the
      * current transition.
      * @return {@code null} if app windows in the transition are not children of the same Task, or
@@ -664,12 +692,17 @@
         if (transitionMayContainNonAppWindows(transit)) {
             return false;
         }
+        if (!transitionContainsTaskFragmentWithBoundsOverride()) {
+            // No need to play TaskFragment remote animation if all embedded TaskFragment in the
+            // transition fill the Task.
+            return false;
+        }
 
         final Task task = findParentTaskForAllEmbeddedWindows();
         final ITaskFragmentOrganizer organizer = findTaskFragmentOrganizer(task);
         final RemoteAnimationDefinition definition = organizer != null
                 ? mDisplayContent.mAtmService.mTaskFragmentOrganizerController
-                    .getRemoteAnimationDefinition(organizer, task.mTaskId)
+                    .getRemoteAnimationDefinition(organizer)
                 : null;
         final RemoteAnimationAdapter adapter = definition != null
                 ? definition.getAdapter(transit, activityTypes)
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 38f6a53..3a936a5 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -4935,9 +4935,8 @@
     @Override
     boolean okToAnimate(boolean ignoreFrozen, boolean ignoreScreenOn) {
         return okToDisplay(ignoreFrozen, ignoreScreenOn)
-                && (mDisplayId != DEFAULT_DISPLAY
-                || mWmService.mPolicy.okToAnimate(ignoreScreenOn))
-                && getDisplayPolicy().isScreenOnFully();
+                && (mDisplayId != DEFAULT_DISPLAY || mWmService.mPolicy.okToAnimate(ignoreScreenOn))
+                && (ignoreFrozen || mDisplayPolicy.isScreenOnFully());
     }
 
     static final class TaskForResizePointSearchResult implements Predicate<Task> {
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index ff09163..0cadc25 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -929,6 +929,7 @@
             // Update windowing mode if necessary, e.g. launch into a different windowing mode.
             if (windowingMode != WINDOWING_MODE_UNDEFINED && candidateTask.isRootTask()
                     && candidateTask.getWindowingMode() != windowingMode) {
+                candidateTask.mTransitionController.collect(candidateTask);
                 candidateTask.setWindowingMode(windowingMode);
             }
             return candidateTask.getRootTask();
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 52df3ca..273b4c4 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -581,15 +581,7 @@
 
     @Override
     boolean isEmbedded() {
-        if (mIsEmbedded) {
-            return true;
-        }
-        final WindowContainer<?> parent = getParent();
-        if (parent != null) {
-            final TaskFragment taskFragment = parent.asTaskFragment();
-            return taskFragment != null && taskFragment.isEmbedded();
-        }
-        return false;
+        return mIsEmbedded;
     }
 
     @EmbeddingCheckResult
@@ -2519,6 +2511,22 @@
         return mTaskFragmentOrganizer != null;
     }
 
+    /**
+     * Whether this is an embedded {@link TaskFragment} that does not fill the parent {@link Task}.
+     */
+    boolean isEmbeddedWithBoundsOverride() {
+        if (!mIsEmbedded) {
+            return false;
+        }
+        final Task task = getTask();
+        if (task == null) {
+            return false;
+        }
+        final Rect taskBounds = task.getBounds();
+        final Rect taskFragBounds = getBounds();
+        return !taskBounds.equals(taskFragBounds) && taskBounds.contains(taskFragBounds);
+    }
+
     /** Whether the Task should be visible. */
     boolean isTaskVisibleRequested() {
         final Task task = getTask();
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index 2e716ae..6e4df79 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -36,7 +36,6 @@
 import android.annotation.Nullable;
 import android.content.Intent;
 import android.content.res.Configuration;
-import android.graphics.Rect;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -133,12 +132,11 @@
                 new WeakHashMap<>();
 
         /**
-         * Map from Task Id to {@link RemoteAnimationDefinition}.
-         * @see android.window.TaskFragmentOrganizer#registerRemoteAnimations(int,
-         * RemoteAnimationDefinition) )
+         * {@link RemoteAnimationDefinition} for embedded activities transition animation that is
+         * organized by this organizer.
          */
-        private final SparseArray<RemoteAnimationDefinition> mRemoteAnimationDefinitions =
-                new SparseArray<>();
+        @Nullable
+        private RemoteAnimationDefinition mRemoteAnimationDefinition;
 
         /**
          * Map from {@link TaskFragmentTransaction#getTransactionToken()} to the
@@ -427,7 +425,7 @@
             ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER,
                     "Register task fragment organizer=%s uid=%d pid=%d",
                     organizer.asBinder(), uid, pid);
-            if (mTaskFragmentOrganizerState.containsKey(organizer.asBinder())) {
+            if (isOrganizerRegistered(organizer)) {
                 throw new IllegalStateException(
                         "Replacing existing organizer currently unsupported");
             }
@@ -455,7 +453,7 @@
     }
 
     @Override
-    public void registerRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, int taskId,
+    public void registerRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer,
             @NonNull RemoteAnimationDefinition definition) {
         final int pid = Binder.getCallingPid();
         final int uid = Binder.getCallingUid();
@@ -468,20 +466,19 @@
             if (organizerState == null) {
                 throw new IllegalStateException("The organizer hasn't been registered.");
             }
-            if (organizerState.mRemoteAnimationDefinitions.contains(taskId)) {
+            if (organizerState.mRemoteAnimationDefinition != null) {
                 throw new IllegalStateException(
                         "The organizer has already registered remote animations="
-                                + organizerState.mRemoteAnimationDefinitions.get(taskId)
-                                + " for TaskId=" + taskId);
+                                + organizerState.mRemoteAnimationDefinition);
             }
 
             definition.setCallingPidUid(pid, uid);
-            organizerState.mRemoteAnimationDefinitions.put(taskId, definition);
+            organizerState.mRemoteAnimationDefinition = definition;
         }
     }
 
     @Override
-    public void unregisterRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer, int taskId) {
+    public void unregisterRemoteAnimations(@NonNull ITaskFragmentOrganizer organizer) {
         final int pid = Binder.getCallingPid();
         final long uid = Binder.getCallingUid();
         synchronized (mGlobalLock) {
@@ -495,7 +492,7 @@
                 return;
             }
 
-            organizerState.mRemoteAnimationDefinitions.remove(taskId);
+            organizerState.mRemoteAnimationDefinition = null;
         }
     }
 
@@ -505,10 +502,18 @@
             @WindowManager.TransitionType int transitionType, boolean shouldApplyIndependently) {
         // Keep the calling identity to avoid unsecure change.
         synchronized (mGlobalLock) {
-            applyTransaction(wct, transitionType, shouldApplyIndependently);
-            final TaskFragmentOrganizerState state = validateAndGetState(
-                    wct.getTaskFragmentOrganizer());
-            state.onTransactionFinished(transactionToken);
+            if (isValidTransaction(wct)) {
+                applyTransaction(wct, transitionType, shouldApplyIndependently);
+            }
+            // Even if the transaction is empty, we still need to invoke #onTransactionFinished
+            // unless the organizer has been unregistered.
+            final ITaskFragmentOrganizer organizer = wct.getTaskFragmentOrganizer();
+            final TaskFragmentOrganizerState state = organizer != null
+                    ? mTaskFragmentOrganizerState.get(organizer.asBinder())
+                    : null;
+            if (state != null) {
+                state.onTransactionFinished(transactionToken);
+            }
         }
     }
 
@@ -517,7 +522,7 @@
             @WindowManager.TransitionType int transitionType, boolean shouldApplyIndependently) {
         // Keep the calling identity to avoid unsecure change.
         synchronized (mGlobalLock) {
-            if (wct.isEmpty()) {
+            if (!isValidTransaction(wct)) {
                 return;
             }
             mWindowOrganizerController.applyTaskFragmentTransactionLocked(wct, transitionType,
@@ -527,16 +532,16 @@
 
     /**
      * Gets the {@link RemoteAnimationDefinition} set on the given organizer if exists. Returns
-     * {@code null} if it doesn't, or if the organizer has activity(ies) embedded in untrusted mode.
+     * {@code null} if it doesn't.
      */
     @Nullable
     public RemoteAnimationDefinition getRemoteAnimationDefinition(
-            @NonNull ITaskFragmentOrganizer organizer, int taskId) {
+            @NonNull ITaskFragmentOrganizer organizer) {
         synchronized (mGlobalLock) {
             final TaskFragmentOrganizerState organizerState =
                     mTaskFragmentOrganizerState.get(organizer.asBinder());
             return organizerState != null
-                    ? organizerState.mRemoteAnimationDefinitions.get(taskId)
+                    ? organizerState.mRemoteAnimationDefinition
                     : null;
         }
     }
@@ -658,7 +663,7 @@
             }
             organizer = organizedTf[0].getTaskFragmentOrganizer();
         }
-        if (!mTaskFragmentOrganizerState.containsKey(organizer.asBinder())) {
+        if (!isOrganizerRegistered(organizer)) {
             Slog.w(TAG, "The last TaskFragmentOrganizer no longer exists");
             return;
         }
@@ -704,7 +709,7 @@
         mPendingTaskFragmentEvents.get(event.mTaskFragmentOrg.asBinder()).remove(event);
     }
 
-    boolean isOrganizerRegistered(@NonNull ITaskFragmentOrganizer organizer) {
+    private boolean isOrganizerRegistered(@NonNull ITaskFragmentOrganizer organizer) {
         return mTaskFragmentOrganizerState.containsKey(organizer.asBinder());
     }
 
@@ -741,6 +746,20 @@
         return state;
     }
 
+    boolean isValidTransaction(@NonNull WindowContainerTransaction t) {
+        if (t.isEmpty()) {
+            return false;
+        }
+        final ITaskFragmentOrganizer organizer = t.getTaskFragmentOrganizer();
+        if (t.getTaskFragmentOrganizer() == null || !isOrganizerRegistered(organizer)) {
+            // Transaction from an unregistered organizer should not be applied. This can happen
+            // when the organizer process died before the transaction is applied.
+            Slog.e(TAG, "Caller organizer=" + organizer + " is no longer registered");
+            return false;
+        }
+        return true;
+    }
+
     /**
      * A class to store {@link ITaskFragmentOrganizer} and its organized
      * {@link TaskFragment TaskFragments} with different pending event request.
@@ -1085,16 +1104,7 @@
                 return false;
             }
             final TaskFragment taskFragment = activity.getOrganizedTaskFragment();
-            if (taskFragment == null) {
-                return false;
-            }
-            final Task parentTask = taskFragment.getTask();
-            if (parentTask != null) {
-                final Rect taskBounds = parentTask.getBounds();
-                final Rect taskFragBounds = taskFragment.getBounds();
-                return !taskBounds.equals(taskFragBounds) && taskBounds.contains(taskFragBounds);
-            }
-            return false;
+            return taskFragment != null && taskFragment.isEmbeddedWithBoundsOverride();
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index fa1bc54..80357eb 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2995,10 +2995,9 @@
                     // screen empty. Show background color to cover that.
                     showBackdrop = getDisplayContent().mChangingContainers.size() > 1;
                 } else {
-                    // Check whether or not to show backdrop for open/close transition.
-                    final int animAttr = AppTransition.mapOpenCloseTransitTypes(transit, enter);
-                    final Animation a = animAttr != 0
-                            ? appTransition.loadAnimationAttr(lp, animAttr, transit) : null;
+                    // Check whether the app has requested to show backdrop for open/close
+                    // transition.
+                    final Animation a = appTransition.getNextAppRequestedAnimation(enter);
                     showBackdrop = a != null && a.getShowBackdrop();
                 }
                 backdropColor = appTransition.getNextAppTransitionBackgroundColor();
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 557c0ef..b30fd07 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -403,9 +403,6 @@
      */
     void applyTaskFragmentTransactionLocked(@NonNull WindowContainerTransaction wct,
             @WindowManager.TransitionType int type, boolean shouldApplyIndependently) {
-        if (!isValidTransaction(wct)) {
-            return;
-        }
         enforceTaskFragmentOrganizerPermission("applyTaskFragmentTransaction()",
                 Objects.requireNonNull(wct.getTaskFragmentOrganizer()),
                 Objects.requireNonNull(wct));
@@ -459,7 +456,7 @@
                     // calls startSyncSet.
                     () -> mTransitionController.moveToCollecting(nextTransition),
                     () -> {
-                        if (isValidTransaction(wct)) {
+                        if (mTaskFragmentOrganizerController.isValidTransaction(wct)) {
                             applyTransaction(wct, -1 /*syncId*/, nextTransition, caller);
                             mTransitionController.requestStartTransition(nextTransition,
                                     null /* startTask */, null /* remoteTransition */,
@@ -1620,18 +1617,6 @@
         return (cfgChanges & CONTROLLABLE_CONFIGS) == 0;
     }
 
-    private boolean isValidTransaction(@NonNull WindowContainerTransaction t) {
-        if (t.getTaskFragmentOrganizer() != null && !mTaskFragmentOrganizerController
-                .isOrganizerRegistered(t.getTaskFragmentOrganizer())) {
-            // Transaction from an unregistered organizer should not be applied. This can happen
-            // when the organizer process died before the transaction is applied.
-            Slog.e(TAG, "Caller organizer=" + t.getTaskFragmentOrganizer()
-                    + " is no longer registered");
-            return false;
-        }
-        return true;
-    }
-
     /**
      * Makes sure that the transaction only contains operations that are allowed for the
      * {@link WindowContainerTransaction#getTaskFragmentOrganizer()}.
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index 90b19a4..6d2631a 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -65,6 +65,7 @@
     private final ArgumentCaptor<DeviceState[]> mDeviceStateArrayCaptor = ArgumentCaptor.forClass(
             DeviceState[].class);
     private final ArgumentCaptor<Integer> mIntegerCaptor = ArgumentCaptor.forClass(Integer.class);
+    private static final int MAX_HINGE_ANGLE_EXCLUSIVE = 360;
 
     private Context mContext;
     private SensorManager mSensorManager;
@@ -268,11 +269,7 @@
         assertEquals(1, mIntegerCaptor.getValue().intValue());
     }
 
-    @Test
-    public void create_sensor() throws Exception {
-        Sensor sensor = newSensor("sensor", Sensor.STRING_TYPE_HINGE_ANGLE);
-        when(mSensorManager.getSensorList(anyInt())).thenReturn(List.of(sensor));
-
+    private DeviceStateProviderImpl create_sensorBasedProvider(Sensor sensor) {
         String configString = "<device-state-config>\n"
                 + "    <device-state>\n"
                 + "        <identifier>1</identifier>\n"
@@ -310,14 +307,22 @@
                 + "                <name>" + sensor.getName() + "</name>\n"
                 + "                <value>\n"
                 + "                    <min-inclusive>180</min-inclusive>\n"
+                + "                    <max>" + MAX_HINGE_ANGLE_EXCLUSIVE + "</max>\n"
                 + "                </value>\n"
                 + "            </sensor>\n"
                 + "        </conditions>\n"
                 + "    </device-state>\n"
                 + "</device-state-config>\n";
         DeviceStateProviderImpl.ReadableConfig config = new TestReadableConfig(configString);
-        DeviceStateProviderImpl provider = DeviceStateProviderImpl.createFromConfig(mContext,
+        return DeviceStateProviderImpl.createFromConfig(mContext,
                 config);
+    }
+
+    @Test
+    public void create_sensor() throws Exception {
+        Sensor sensor = newSensor("sensor", Sensor.STRING_TYPE_HINGE_ANGLE);
+        when(mSensorManager.getSensorList(anyInt())).thenReturn(List.of(sensor));
+        DeviceStateProviderImpl provider = create_sensorBasedProvider(sensor);
 
         DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
         provider.setListener(listener);
@@ -371,6 +376,40 @@
     }
 
     @Test
+    public void test_invalidSensorValues() throws Exception {
+        // onStateChanged() should not be triggered by invalid sensor values.
+
+        Sensor sensor = newSensor("sensor", Sensor.STRING_TYPE_HINGE_ANGLE);
+        when(mSensorManager.getSensorList(anyInt())).thenReturn(List.of(sensor));
+        DeviceStateProviderImpl provider = create_sensorBasedProvider(sensor);
+
+        DeviceStateProvider.Listener listener = mock(DeviceStateProvider.Listener.class);
+        provider.setListener(listener);
+        Mockito.clearInvocations(listener);
+
+        // First, switch to a non-default state.
+        SensorEvent event1 = mock(SensorEvent.class);
+        event1.sensor = sensor;
+        FieldSetter.setField(event1, event1.getClass().getField("values"), new float[]{90});
+        provider.onSensorChanged(event1);
+        verify(listener).onStateChanged(mIntegerCaptor.capture());
+        assertEquals(2, mIntegerCaptor.getValue().intValue());
+
+        Mockito.clearInvocations(listener);
+
+        // Then, send an invalid sensor event, verify that onStateChanged() is not triggered.
+        SensorEvent event2 = mock(SensorEvent.class);
+        event2.sensor = sensor;
+        FieldSetter.setField(event2, event2.getClass().getField("values"),
+                new float[]{MAX_HINGE_ANGLE_EXCLUSIVE});
+
+        provider.onSensorChanged(event2);
+
+        verify(listener, never()).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture());
+        verify(listener, never()).onStateChanged(mIntegerCaptor.capture());
+    }
+
+    @Test
     public void create_invalidSensor() throws Exception {
         Sensor sensor = newSensor("sensor", Sensor.STRING_TYPE_HINGE_ANGLE);
         when(mSensorManager.getSensorList(anyInt())).thenReturn(List.of());
diff --git a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
index 32772a4..a42d009 100644
--- a/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -375,6 +375,18 @@
         mBatteryReceiver.onReceive(mContextSpy, new Intent(Intent.ACTION_BATTERY_CHANGED));
     }
 
+    private void setBatteryLevel(int batteryLevel) {
+        when(mBatteryManagerInternalMock.getBatteryLevel())
+                .thenReturn(batteryLevel);
+        mBatteryReceiver.onReceive(mContextSpy, new Intent(Intent.ACTION_BATTERY_CHANGED));
+    }
+
+    private void setBatteryHealth(int batteryHealth) {
+        when(mBatteryManagerInternalMock.getBatteryHealth())
+                .thenReturn(batteryHealth);
+        mBatteryReceiver.onReceive(mContextSpy, new Intent(Intent.ACTION_BATTERY_CHANGED));
+    }
+
     private void setAttentiveTimeout(int attentiveTimeoutMillis) {
         Settings.Secure.putInt(
                 mContextSpy.getContentResolver(), Settings.Secure.ATTENTIVE_TIMEOUT,
@@ -399,6 +411,12 @@
                 .thenReturn(disable);
     }
 
+    private void setDreamsBatteryLevelDrainConfig(int threshold) {
+        when(mResourcesSpy.getInteger(
+                com.android.internal.R.integer.config_dreamsBatteryLevelDrainCutoff)).thenReturn(
+                threshold);
+    }
+
     private void advanceTime(long timeMs) {
         mClock.fastForward(timeMs);
         mTestLooper.dispatchAll();
@@ -938,6 +956,41 @@
     }
 
     @Test
+    public void testBatteryDrainDuringDream() {
+        Settings.Secure.putInt(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP, 1);
+        Settings.Secure.putInt(mContextSpy.getContentResolver(),
+                Settings.Secure.SCREENSAVER_ENABLED, 1);
+
+        setMinimumScreenOffTimeoutConfig(100);
+        setDreamsBatteryLevelDrainConfig(5);
+        createService();
+        startSystem();
+
+        doAnswer(inv -> {
+            when(mDreamManagerInternalMock.isDreaming()).thenReturn(true);
+            return null;
+        }).when(mDreamManagerInternalMock).startDream(anyBoolean(), anyString());
+
+        setBatteryLevel(100);
+        setPluggedIn(true);
+
+        forceAwake();  // Needs to be awake first before it can dream.
+        forceDream();
+        advanceTime(10); // Allow async calls to happen
+        assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING);
+        setBatteryLevel(90);
+        advanceTime(10); // Allow async calls to happen
+        assertThat(mService.getDreamsBatteryLevelDrain()).isEqualTo(10);
+
+        // If battery overheat protection is enabled, we shouldn't count battery drain
+        setBatteryHealth(BatteryManager.BATTERY_HEALTH_OVERHEAT);
+        setBatteryLevel(70);
+        advanceTime(10); // Allow async calls to happen
+        assertThat(mService.getDreamsBatteryLevelDrain()).isEqualTo(10);
+    }
+
+    @Test
     public void testSetDozeOverrideFromDreamManager_triggersSuspendBlocker() {
         final String suspendBlockerName = "PowerManagerService.Display";
         final String tag = "acq_causes_wakeup";
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index ff5622f..d3e638f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3091,6 +3091,17 @@
         assertTrue(activity.mVisibleRequested);
         assertTrue(activity.mDisplayContent.mOpeningApps.contains(activity));
         assertFalse(activity.mDisplayContent.mClosingApps.contains(activity));
+
+        // There should still be animation (add to opening) if keyguard is going away while the
+        // screen is off because it will be visible after screen is turned on by unlocking.
+        mDisplayContent.mOpeningApps.remove(activity);
+        mDisplayContent.mClosingApps.remove(activity);
+        activity.commitVisibility(false /* visible */, false /* performLayout */);
+        mDisplayContent.getDisplayPolicy().screenTurnedOff();
+        final KeyguardController controller = mSupervisor.getKeyguardController();
+        doReturn(true).when(controller).isKeyguardGoingAway(anyInt());
+        activity.setVisibility(true);
+        assertTrue(mDisplayContent.mOpeningApps.contains(activity));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 0332c4b..43e79f9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -25,6 +25,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
 import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_CLOSE;
 import static android.view.WindowManager.TRANSIT_OLD_DREAM_ACTIVITY_OPEN;
@@ -56,6 +57,7 @@
 import static org.mockito.Mockito.verify;
 
 import android.annotation.Nullable;
+import android.graphics.Rect;
 import android.gui.DropInputMode;
 import android.os.Binder;
 import android.os.IBinder;
@@ -918,7 +920,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Create a TaskFragment with embedded activity.
         final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
@@ -935,11 +937,77 @@
     }
 
     @Test
+    public void testOverrideTaskFragmentAdapter_noOverrideWithOnlyTaskFragmentFillingTask() {
+        final Task task = createTask(mDisplayContent);
+        final ActivityRecord closingActivity = createActivityRecord(task);
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
+
+        // Create a TaskFragment with embedded activity.
+        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
+
+        // Make sure the TaskFragment is not embedded.
+        assertFalse(taskFragment.isEmbeddedWithBoundsOverride());
+        final ActivityRecord openingActivity = taskFragment.getTopMostActivity();
+        prepareActivityForAppTransition(closingActivity);
+        prepareActivityForAppTransition(openingActivity);
+        final int uid = 12345;
+        closingActivity.info.applicationInfo.uid = uid;
+        openingActivity.info.applicationInfo.uid = uid;
+        task.effectiveUid = uid;
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare and start transition.
+        prepareAndTriggerAppTransition(openingActivity, closingActivity,
+                null /* changingTaskFragment */);
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+
+        // Animation is not run by the remote handler because the activity is filling the Task.
+        assertFalse(remoteAnimationRunner.isAnimationStarted());
+    }
+
+    @Test
+    public void testOverrideTaskFragmentAdapter_overrideWithTaskFragmentNotFillingTask() {
+        final Task task = createTask(mDisplayContent);
+        final ActivityRecord closingActivity = createActivityRecord(task);
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
+
+        // Create a TaskFragment with embedded activity.
+        final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
+
+        // Make sure the TaskFragment is embedded.
+        taskFragment.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+        final Rect embeddedBounds = new Rect(task.getBounds());
+        embeddedBounds.right = embeddedBounds.left + embeddedBounds.width() / 2;
+        taskFragment.setBounds(embeddedBounds);
+        assertTrue(taskFragment.isEmbeddedWithBoundsOverride());
+        final ActivityRecord openingActivity = taskFragment.getTopMostActivity();
+        prepareActivityForAppTransition(closingActivity);
+        prepareActivityForAppTransition(openingActivity);
+        final int uid = 12345;
+        closingActivity.info.applicationInfo.uid = uid;
+        openingActivity.info.applicationInfo.uid = uid;
+        task.effectiveUid = uid;
+        spyOn(mDisplayContent.mAppTransition);
+
+        // Prepare and start transition.
+        prepareAndTriggerAppTransition(openingActivity, closingActivity,
+                null /* changingTaskFragment */);
+        mWm.mAnimator.executeAfterPrepareSurfacesRunnables();
+
+        // Animation run by the remote handler.
+        assertTrue(remoteAnimationRunner.isAnimationStarted());
+    }
+
+    @Test
     public void testOverrideTaskFragmentAdapter_overrideWithNonEmbeddedActivity() {
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Closing non-embedded activity.
         final ActivityRecord closingActivity = createActivityRecord(task);
@@ -964,7 +1032,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Closing TaskFragment with embedded activity.
         final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer);
@@ -991,7 +1059,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Closing activity in Task1.
         final ActivityRecord closingActivity = createActivityRecord(mDisplayContent);
@@ -1015,7 +1083,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Closing TaskFragment with embedded activity.
         final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
@@ -1043,7 +1111,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Create a TaskFragment with embedded activity.
         final TaskFragment taskFragment = createTaskFragmentWithEmbeddedActivity(task, organizer);
@@ -1069,7 +1137,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Create a TaskFragment with embedded activities, one is trusted embedded, and the other
         // one is untrusted embedded.
@@ -1128,7 +1196,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Create a TaskFragment with only trusted embedded activity
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
@@ -1168,7 +1236,7 @@
         final Task task = createTask(mDisplayContent);
         final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
         final TestRemoteAnimationRunner remoteAnimationRunner = new TestRemoteAnimationRunner();
-        setupTaskFragmentRemoteAnimation(organizer, task.mTaskId, remoteAnimationRunner);
+        setupTaskFragmentRemoteAnimation(organizer, remoteAnimationRunner);
 
         // Create a TaskFragment with only trusted embedded activity
         final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
@@ -1259,7 +1327,7 @@
     }
 
     /** Registers remote animation for the organizer. */
-    private void setupTaskFragmentRemoteAnimation(TaskFragmentOrganizer organizer, int taskId,
+    private void setupTaskFragmentRemoteAnimation(TaskFragmentOrganizer organizer,
             TestRemoteAnimationRunner remoteAnimationRunner) {
         final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
                 remoteAnimationRunner, 10, 1);
@@ -1268,9 +1336,10 @@
         definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CHANGE, adapter);
         definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_OPEN, adapter);
         definition.addRemoteAnimation(TRANSIT_OLD_TASK_FRAGMENT_CLOSE, adapter);
+        definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_OPEN, adapter);
+        definition.addRemoteAnimation(TRANSIT_OLD_ACTIVITY_CLOSE, adapter);
         mAtm.mTaskFragmentOrganizerController.registerOrganizer(iOrganizer);
-        mAtm.mTaskFragmentOrganizerController.registerRemoteAnimations(iOrganizer, taskId,
-                definition);
+        mAtm.mTaskFragmentOrganizerController.registerRemoteAnimations(iOrganizer, definition);
     }
 
     private static ITaskFragmentOrganizer getITaskFragmentOrganizer(
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 32c95fa..8cfe503 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -49,6 +49,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.mock;
@@ -67,11 +68,14 @@
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
+import android.view.animation.Animation;
 import android.window.ITaskFragmentOrganizer;
 import android.window.TaskFragmentOrganizer;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.policy.TransitionAnimation;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -495,6 +499,80 @@
         assertEquals(startBounds, taskFragment.mSurfaceFreezer.mFreezeBounds);
     }
 
+    @Test
+    public void testGetNextAppTransitionBackgroundColor() {
+        assumeFalse(WindowManagerService.sEnableShellTransitions);
+
+        // No override by default.
+        assertEquals(0, mDc.mAppTransition.getNextAppTransitionBackgroundColor());
+
+        // Override with a custom color.
+        mDc.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0);
+        final int testColor = 123;
+        mDc.mAppTransition.overridePendingAppTransition("testPackage", 0 /* enterAnim */,
+                0 /* exitAnim */, testColor, null /* startedCallback */, null /* endedCallback */,
+                false /* overrideTaskTransaction */);
+
+        assertEquals(testColor, mDc.mAppTransition.getNextAppTransitionBackgroundColor());
+        assertTrue(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
+
+        // Override with ActivityEmbedding remote animation. Background color should be kept.
+        mDc.mAppTransition.overridePendingAppTransitionRemote(mock(RemoteAnimationAdapter.class),
+                false /* sync */, true /* isActivityEmbedding */);
+
+        assertEquals(testColor, mDc.mAppTransition.getNextAppTransitionBackgroundColor());
+        assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
+
+        // Background color should not be cleared anymore after #clear().
+        mDc.mAppTransition.clear();
+        assertEquals(0, mDc.mAppTransition.getNextAppTransitionBackgroundColor());
+        assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
+    }
+
+    @Test
+    public void testGetNextAppRequestedAnimation() {
+        assumeFalse(WindowManagerService.sEnableShellTransitions);
+        final String packageName = "testPackage";
+        final int enterAnimResId = 1;
+        final int exitAnimResId = 2;
+        final int testColor = 123;
+        final Animation enterAnim = mock(Animation.class);
+        final Animation exitAnim = mock(Animation.class);
+        final TransitionAnimation transitionAnimation = mDc.mAppTransition.mTransitionAnimation;
+        spyOn(transitionAnimation);
+        doReturn(enterAnim).when(transitionAnimation)
+                .loadAppTransitionAnimation(packageName, enterAnimResId);
+        doReturn(exitAnim).when(transitionAnimation)
+                .loadAppTransitionAnimation(packageName, exitAnimResId);
+
+        // No override by default.
+        assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */));
+        assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */));
+
+        // Override with a custom animation.
+        mDc.mAppTransition.prepareAppTransition(TRANSIT_OPEN, 0);
+        mDc.mAppTransition.overridePendingAppTransition(packageName, enterAnimResId, exitAnimResId,
+                testColor, null /* startedCallback */, null /* endedCallback */,
+                false /* overrideTaskTransaction */);
+
+        assertEquals(enterAnim, mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */));
+        assertEquals(exitAnim, mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */));
+        assertTrue(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
+
+        // Override with ActivityEmbedding remote animation. Custom animation should be kept.
+        mDc.mAppTransition.overridePendingAppTransitionRemote(mock(RemoteAnimationAdapter.class),
+                false /* sync */, true /* isActivityEmbedding */);
+
+        assertEquals(enterAnim, mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */));
+        assertEquals(exitAnim, mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */));
+        assertFalse(mDc.mAppTransition.isNextAppTransitionOverrideRequested());
+
+        // Custom animation should not be cleared anymore after #clear().
+        mDc.mAppTransition.clear();
+        assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(true /* enter */));
+        assertNull(mDc.mAppTransition.getNextAppRequestedAnimation(false /* enter */));
+    }
+
     private class TestRemoteAnimationRunner implements IRemoteAnimationRunner {
         boolean mCancelled = false;
         @Override
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 c535182..2b49314 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -474,13 +474,13 @@
 
     @Test
     public void testRegisterRemoteAnimations() {
-        mController.registerRemoteAnimations(mIOrganizer, TASK_ID, mDefinition);
+        mController.registerRemoteAnimations(mIOrganizer, mDefinition);
 
-        assertEquals(mDefinition, mController.getRemoteAnimationDefinition(mIOrganizer, TASK_ID));
+        assertEquals(mDefinition, mController.getRemoteAnimationDefinition(mIOrganizer));
 
-        mController.unregisterRemoteAnimations(mIOrganizer, TASK_ID);
+        mController.unregisterRemoteAnimations(mIOrganizer);
 
-        assertNull(mController.getRemoteAnimationDefinition(mIOrganizer, TASK_ID));
+        assertNull(mController.getRemoteAnimationDefinition(mIOrganizer));
     }
 
     @Test
@@ -822,6 +822,21 @@
     }
 
     @Test
+    public void testOnTransactionHandled_skipTransactionForUnregisterOrganizer() {
+        mController.unregisterOrganizer(mIOrganizer);
+        final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent);
+        final IBinder fragmentToken = new Binder();
+
+        // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment.
+        createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken);
+        mController.onTransactionHandled(new Binder(), mTransaction,
+                getTransitionType(mTransaction), false /* shouldApplyIndependently */);
+
+        // Nothing should happen as the organizer is not registered.
+        assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken));
+    }
+
+    @Test
     public void testOrganizerRemovedWithPendingEvents() {
         final TaskFragment tf0 = new TaskFragmentBuilder(mAtm)
                 .setCreateParentTask()