Merge "Make PerfettoProtoLogImpl implement the Perfetto client interface" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
index dc5e341..93904a7 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.os.Environment;
 import android.os.SystemClock;
+import android.os.UserHandle;
 import android.util.AtomicFile;
 import android.util.IndentingPrintWriter;
 import android.util.Pair;
@@ -113,15 +114,18 @@
     }
 
     /**
-     * Add user wakeup for the alarm.
+     * Add user wakeup for the alarm if needed.
      * @param userId Id of the user that scheduled alarm.
      * @param alarmTime time when alarm is expected to trigger.
      */
     public void addUserWakeup(int userId, long alarmTime) {
-        synchronized (mUserWakeupLock) {
-            mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+        // SYSTEM user is always running, so no need to schedule wakeup for it.
+        if (userId != UserHandle.USER_SYSTEM) {
+            synchronized (mUserWakeupLock) {
+                mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+            }
+            updateUserListFile();
         }
-        updateUserListFile();
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index 1bf1259..da212e7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -66,7 +66,7 @@
                         idealSize
                     }
                 } else {
-                    maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+                    maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
                 }
             }
             ORIENTATION_PORTRAIT -> {
@@ -85,13 +85,13 @@
                 } else {
                     if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
                         // Apply custom app width and calculate maximum size
-                        maximumSizeMaintainingAspectRatio(
+                        maximizeSizeGivenAspectRatio(
                             taskInfo,
                             Size(customPortraitWidthForLandscapeApp, idealSize.height),
                             appAspectRatio
                         )
                     } else {
-                        maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+                        maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio)
                     }
                 }
             }
@@ -107,7 +107,7 @@
  * Calculates the largest size that can fit in a given area while maintaining a specific aspect
  * ratio.
  */
-fun maximumSizeMaintainingAspectRatio(
+fun maximizeSizeGivenAspectRatio(
     taskInfo: RunningTaskInfo,
     targetArea: Size,
     aspectRatio: Float
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 31f797a..a91edaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -671,7 +671,7 @@
             } else {
                 // if non-resizable then calculate max bounds according to aspect ratio
                 val activityAspectRatio = calculateAspectRatio(taskInfo)
-                val newSize = maximumSizeMaintainingAspectRatio(taskInfo,
+                val newSize = maximizeSizeGivenAspectRatio(taskInfo,
                     Size(stableBounds.width(), stableBounds.height()), activityAspectRatio)
                 val newBounds = centerInArea(
                     newSize, stableBounds, stableBounds.left, stableBounds.top)
@@ -1079,7 +1079,6 @@
         wct: WindowContainerTransaction,
         taskInfo: RunningTaskInfo
     ) {
-        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
         val targetWindowingMode =
@@ -1089,9 +1088,6 @@
             } else {
                 WINDOWING_MODE_FREEFORM
             }
-        if (Flags.enableWindowingDynamicInitialBounds()) {
-            wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
-        }
         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
         wct.reorder(taskInfo.token, true /* onTop */)
         if (useDesktopOverrideDensity()) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 6002c21..8421365 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -580,138 +580,6 @@
   }
 
   @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
-    val task = setUpFullscreenTask()
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
-    val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
-    val task =
-        setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
-            shouldLetterbox = true)
-    setUpLandscapeDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
-    val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-            shouldLetterbox = true)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
-  }
-
-  @Test
-  @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
-  fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
-    val task =
-        setUpFullscreenTask(
-            isResizable = false,
-            deviceOrientation = ORIENTATION_PORTRAIT,
-            screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
-            shouldLetterbox = true)
-    setUpPortraitDisplay()
-
-    controller.moveToDesktop(task, transitionSource = UNKNOWN)
-    val wct = getLatestEnterDesktopWct()
-    assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
-  }
-
-  @Test
   fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
     val task = setUpFullscreenTask()
     val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index d629eec..f8bd633 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.util.animation.MeasurementInput
 
-private object MediaCarousel {
+object MediaCarousel {
     object Elements {
         internal val Content =
             ElementKey(debugName = "MediaCarouselContent", scenePicker = MediaScenePicker)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
index 0398133..a22bc34 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaScenePicker.kt
@@ -25,7 +25,7 @@
 /** [ElementScenePicker] implementation for the media carousel object. */
 object MediaScenePicker : ElementScenePicker {
 
-    private val shadeLockscreenFraction = 0.65f
+    const val SHADE_FRACTION = 0.66f
     private val scenes =
         setOf(
             Scenes.Lockscreen,
@@ -44,7 +44,7 @@
         return when {
             // TODO: 352052894 - update with the actual scene picking
             transition.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade) -> {
-                if (transition.progress < shadeLockscreenFraction) {
+                if (transition.progress < SHADE_FRACTION) {
                     Scenes.Lockscreen
                 } else {
                     Scenes.Shade
@@ -53,7 +53,7 @@
 
             // TODO: 345467290 - update with the actual scene picking
             transition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) -> {
-                if (transition.progress < 1f - shadeLockscreenFraction) {
+                if (transition.progress < 1f - SHADE_FRACTION) {
                     Scenes.Shade
                 } else {
                     Scenes.Lockscreen
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
index df47cba..7d46c75 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToShadeTransition.kt
@@ -25,6 +25,8 @@
 import com.android.compose.animation.scene.TransitionBuilder
 import com.android.compose.animation.scene.UserActionDistance
 import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.media.controls.ui.composable.MediaCarousel
+import com.android.systemui.media.controls.ui.composable.MediaScenePicker
 import com.android.systemui.notifications.ui.composable.Notifications
 import com.android.systemui.qs.ui.composable.QuickSettings
 import com.android.systemui.scene.shared.model.Scenes
@@ -59,10 +61,13 @@
         fade(QuickSettings.Elements.SplitShadeQuickSettings)
         fade(QuickSettings.Elements.FooterActions)
     }
-    translate(
-        QuickSettings.Elements.QuickQuickSettings,
-        y = -ShadeHeader.Dimensions.CollapsedHeight * .66f
-    )
+
+    val qsTranslation = ShadeHeader.Dimensions.CollapsedHeight * MediaScenePicker.SHADE_FRACTION
+    val qsExpansionDiff =
+        ShadeHeader.Dimensions.ExpandedHeight - ShadeHeader.Dimensions.CollapsedHeight
+
+    translate(QuickSettings.Elements.QuickQuickSettings, y = -qsTranslation)
+    translate(MediaCarousel.Elements.Content, y = -(qsExpansionDiff + qsTranslation))
     translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
 }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index a120bdc..540a85a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -175,12 +175,14 @@
             transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone)
             assertThat(isVisible).isFalse()
 
-            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+            kosmos.headsUpNotificationRepository.setNotifications(
                 buildNotificationRows(isPinned = true)
+            )
             assertThat(isVisible).isTrue()
 
-            kosmos.headsUpNotificationRepository.activeHeadsUpRows.value =
+            kosmos.headsUpNotificationRepository.setNotifications(
                 buildNotificationRows(isPinned = false)
+            )
             assertThat(isVisible).isFalse()
         }
 
@@ -1699,8 +1701,8 @@
         return transitionStateFlow
     }
 
-    private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> =
-        setOf(
+    private fun buildNotificationRows(isPinned: Boolean = false): List<HeadsUpRowRepository> =
+        listOf(
             fakeHeadsUpRowRepository(key = "0", isPinned = isPinned),
             fakeHeadsUpRowRepository(key = "1", isPinned = isPinned),
             fakeHeadsUpRowRepository(key = "2", isPinned = isPinned),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
new file mode 100644
index 0000000..8810ade
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import android.app.Notification
+import android.app.NotificationManager.IMPORTANCE_DEFAULT
+import android.app.NotificationManager.IMPORTANCE_LOW
+import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.shadeTestUtil
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.modifyEntry
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
+import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.google.common.truth.StringSubject
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
+
+    private val kosmos =
+        testKosmos().apply {
+            testDispatcher = UnconfinedTestDispatcher()
+            statusBarStateController =
+                mock<SysuiStatusBarStateController>().also { mock ->
+                    doAnswer { statusBarState.ordinal }.whenever(mock).state
+                }
+            fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+        }
+    private val notifPipeline: NotifPipeline = mock()
+    private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
+
+    @Test
+    fun topUnseenSectioner() {
+        val solo = NotificationEntryBuilder().setTag("solo").build()
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child1.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = parent.key
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isTrue()
+            assertThat(topUnseenSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(topUnseenSectioner.isInSection(solo)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child1)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(child2)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(parent)).isFalse()
+            assertThat(topUnseenSectioner.isInSection(group)).isFalse()
+        }
+    }
+
+    @Test
+    fun topOngoingSectioner() {
+        val solo = NotificationEntryBuilder().setTag("solo").build()
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        val group = GroupEntryBuilder().addChild(child1).addChild(child2).setSummary(parent).build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = solo.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = parent.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isTrue()
+            assertThat(topOngoingSectioner.isInSection(group)).isTrue()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = solo.key
+            assertThat(topOngoingSectioner.isInSection(solo)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child1)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(child2)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(parent)).isFalse()
+            assertThat(topOngoingSectioner.isInSection(group)).isFalse()
+        }
+    }
+
+    @Test
+    fun testPromoter() {
+        val child1 = NotificationEntryBuilder().setTag("child1").build()
+        val child2 = NotificationEntryBuilder().setTag("child2").build()
+        val child3 = NotificationEntryBuilder().setTag("child3").build()
+        val parent = NotificationEntryBuilder().setTag("parent").build()
+        GroupEntryBuilder()
+            .addChild(child1)
+            .addChild(child2)
+            .addChild(child3)
+            .setSummary(parent)
+            .build()
+
+        runCoordinatorTest {
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = null
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+            assertThat(promoter.shouldPromoteToTopLevel(child2)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = null
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(child2))
+                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+
+            kosmos.activeNotificationListRepository.topOngoingNotificationKey.value = child1.key
+            kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
+            assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
+            assertThat(promoter.shouldPromoteToTopLevel(child2))
+                .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+            assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
+            assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
+        }
+    }
+
+    @Test
+    fun topOngoingIdentifier() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val parent = defaultEntryBuilder().setTag("parent").setRank(3).build()
+        val child1 = defaultEntryBuilder().setTag("child1").setRank(4).build()
+        val child2 = defaultEntryBuilder().setTag("child2").setRank(5).build()
+        val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+        val listEntryList = listOf(group, solo1, solo2)
+
+        runCoordinatorTest {
+            // TEST: base case - no entries in the list
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: none of these are unseen or ongoing yet, so don't pick them
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: when solo2 is the only one colorized, it gets picked up
+            solo2.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: once solo1 is colorized, it takes priority for being ranked higher
+            solo1.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: changing just the rank of solo1 causes it to pick up solo2 instead
+            solo1.modifyEntry { setRank(20) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching to SHADE disables the whole thing
+            statusBarState = StatusBarState.SHADE
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching back to KEYGUARD picks up the same entry again
+            statusBarState = StatusBarState.KEYGUARD
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo2.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: updating to not colorized revokes the top-ongoing status
+            solo2.setColorizedFgs(false)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: updating the importance to LOW revokes top-ongoing status
+            solo1.modifyEntry { setImportance(IMPORTANCE_LOW) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+        }
+    }
+
+    @Test
+    fun topUnseenIdentifier() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val parent = defaultEntryBuilder().setTag("parent").setRank(4).build()
+        val child1 = defaultEntryBuilder().setTag("child1").setRank(5).build()
+        val child2 = defaultEntryBuilder().setTag("child2").setRank(6).build()
+        val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
+        val listEntryList = listOf(group, solo1, solo2)
+        val notificationEntryList = listOf(solo1, solo2, parent, child1, child2)
+
+        runCoordinatorTest {
+            // All entries are added (and now unseen)
+            notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+            // TEST: Filtered out entries are ignored
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(emptyList())
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: top-ranked unseen child is selected (not the summary)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(group))
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(child1.key)
+
+            // TEST: top-ranked entry is picked
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen
+            solo1.setColorizedFgs(true)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(solo1.key)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: no more colorized entries
+            solo1.setColorizedFgs(false)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: if the rank of solo1 is reduced, solo2 will be preferred
+            solo1.modifyEntry { setRank(3) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: switching to SHADE state will disable the entire selector
+            statusBarState = StatusBarState.SHADE
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: switching back to KEYGUARD re-enables the selector
+            statusBarState = StatusBarState.KEYGUARD
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: QS Expansion does not mark entries as seen
+            setShadeAndQsExpansionThenWait(0f, 1f)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: Shade expansion does mark entries as seen
+            setShadeAndQsExpansionThenWait(1f, 0f)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: Entries updated while shade is expanded are NOT marked unseen
+            collectionListener.onEntryUpdated(solo1)
+            collectionListener.onEntryUpdated(solo2)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(null)
+
+            // TEST: Entries updated after shade is collapsed ARE marked unseen
+            setShadeAndQsExpansionThenWait(0f, 0f)
+            collectionListener.onEntryUpdated(solo1)
+            collectionListener.onEntryUpdated(solo2)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: low importance disqualifies the entry for top unseen
+            solo2.modifyEntry { setImportance(IMPORTANCE_LOW) }
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopOngoingKey().isEqualTo(null)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+        }
+    }
+
+    @Test
+    fun topUnseenIdentifier_headsUpMarksSeen() {
+        val solo1 = defaultEntryBuilder().setTag("solo1").setRank(1).build()
+        val solo2 = defaultEntryBuilder().setTag("solo2").setRank(2).build()
+        val listEntryList = listOf(solo1, solo2)
+        val notificationEntryList = listOf(solo1, solo2)
+
+        val hunRepo1 = solo1.fakeHeadsUpRowRepository()
+        val hunRepo2 = solo2.fakeHeadsUpRowRepository()
+
+        runCoordinatorTest {
+            // All entries are added (and now unseen)
+            notificationEntryList.forEach { collectionListener.onEntryAdded(it) }
+
+            // TEST: top-ranked entry is picked
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: heads up state and waiting isn't enough to be seen
+            kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value =
+                listOf(hunRepo1, hunRepo2)
+            testScheduler.advanceTimeBy(1.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: even being pinned doesn't take effect immediately
+            hunRepo1.isPinned.value = true
+            testScheduler.advanceTimeBy(0.5.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo1.key)
+
+            // TEST: after being pinned a full second, solo1 is seen
+            testScheduler.advanceTimeBy(0.5.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(solo2.key)
+
+            // TEST: repeat; being heads up and pinned for 1 second triggers seen
+            kosmos.headsUpNotificationRepository.orderedHeadsUpRows.value = listOf(hunRepo2)
+            hunRepo1.isPinned.value = false
+            hunRepo2.isPinned.value = true
+            testScheduler.advanceTimeBy(1.seconds)
+            onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
+            assertThatTopUnseenKey().isEqualTo(null)
+        }
+    }
+
+    private fun NotificationEntry.fakeHeadsUpRowRepository() =
+        FakeHeadsUpRowRepository(key = key, elementKey = Any())
+
+    private fun KeyguardCoordinatorTestScope.setShadeAndQsExpansionThenWait(
+        shadeExpansion: Float,
+        qsExpansion: Float
+    ) {
+        kosmos.shadeTestUtil.setShadeAndQsExpansion(shadeExpansion, qsExpansion)
+        // The coordinator waits a fraction of a second for the shade expansion to stick.
+        testScheduler.advanceTimeBy(1.seconds)
+    }
+
+    private fun defaultEntryBuilder() = NotificationEntryBuilder().setImportance(IMPORTANCE_DEFAULT)
+
+    private fun runCoordinatorTest(testBlock: suspend KeyguardCoordinatorTestScope.() -> Unit) {
+        kosmos.lockScreenMinimalismCoordinator.attach(notifPipeline)
+        kosmos.testScope.runTest(dispatchTimeoutMs = 1.seconds.inWholeMilliseconds) {
+            KeyguardCoordinatorTestScope(
+                    kosmos.lockScreenMinimalismCoordinator,
+                    kosmos.testScope,
+                    kosmos.fakeSettings,
+                )
+                .testBlock()
+        }
+    }
+
+    private inner class KeyguardCoordinatorTestScope(
+        private val coordinator: LockScreenMinimalismCoordinator,
+        private val scope: TestScope,
+        private val fakeSettings: FakeSettings,
+    ) : CoroutineScope by scope {
+        fun assertThatTopOngoingKey(): StringSubject {
+            return assertThat(
+                kosmos.activeNotificationListRepository.topOngoingNotificationKey.value
+            )
+        }
+
+        fun assertThatTopUnseenKey(): StringSubject {
+            return assertThat(
+                kosmos.activeNotificationListRepository.topUnseenNotificationKey.value
+            )
+        }
+
+        val testScheduler: TestCoroutineScheduler
+            get() = scope.testScheduler
+
+        val promoter: NotifPromoter
+            get() = coordinator.unseenNotifPromoter
+
+        val topUnseenSectioner: NotifSectioner
+            get() = coordinator.topUnseenSectioner
+
+        val topOngoingSectioner: NotifSectioner
+            get() = coordinator.topOngoingSectioner
+
+        val onBeforeTransformGroupsListener: OnBeforeTransformGroupsListener =
+            argumentCaptor { verify(notifPipeline).addOnBeforeTransformGroupsListener(capture()) }
+                .lastValue
+
+        val collectionListener: NotifCollectionListener =
+            argumentCaptor { verify(notifPipeline).addCollectionListener(capture()) }.lastValue
+
+        var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
+            get() =
+                fakeSettings.getIntForUser(
+                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    UserHandle.USER_CURRENT,
+                ) == 1
+            set(value) {
+                fakeSettings.putIntForUser(
+                    Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    if (value) 1 else 2,
+                    UserHandle.USER_CURRENT,
+                )
+            }
+    }
+
+    companion object {
+
+        private fun NotificationEntry.setColorizedFgs(colorized: Boolean) {
+            sbn.notification.setColorizedFgs(colorized)
+        }
+
+        private fun Notification.setColorizedFgs(colorized: Boolean) {
+            extras.putBoolean(Notification.EXTRA_COLORIZED, colorized)
+            flags =
+                if (colorized) {
+                    flags or Notification.FLAG_FOREGROUND_SERVICE
+                } else {
+                    flags and Notification.FLAG_FOREGROUND_SERVICE.inv()
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
index 8b4265f..14134cc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
@@ -33,7 +33,6 @@
 import com.android.systemui.statusbar.notification.data.repository.notificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index f8e6337..f96cf10 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -43,7 +43,6 @@
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
 import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
 import com.android.systemui.statusbar.policy.fakeConfigurationController
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index b643968..c3c5a48 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -50,6 +50,7 @@
                 statusBarStateController = statusBarStateController,
                 mainExecutor = mainExecutor,
                 legacyActivityStarter = { legacyActivityStarterInternal },
+                activityStarterInternal = { activityStarterInternal },
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
index 10a2f64..d82b9db 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
@@ -231,7 +231,6 @@
         // extra activity options to set on pending intent
         val activityOptions = mock(ActivityOptions::class.java)
         activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR
-        activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false
         val bundleCaptor = argumentCaptor<Bundle>()
 
         startPendingIntentMaybeDismissingKeyguard(
@@ -255,7 +254,8 @@
                 bundleCaptor.capture()
             )
         val options = ActivityOptions.fromBundle(bundleCaptor.firstValue)
-        assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse()
+        assertThat(options.getPendingIntentBackgroundActivityStartMode())
+            .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS)
         assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
index 7f8103e..6864f4e 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt
@@ -18,7 +18,7 @@
 
 import android.app.Activity
 import android.app.ActivityOptions
-import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
 import android.app.Dialog
 import android.app.PendingIntent
 import android.content.ComponentName
@@ -93,8 +93,8 @@
                 0 /* enterResId */,
                 0 /* exitResId */
             ).apply {
-                pendingIntentBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_ALLOWED
-                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+                pendingIntentBackgroundActivityStartMode =
+                    MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
                 taskAlwaysOnTop = true
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 1ba274f..0e06117 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -37,6 +37,8 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
 import javax.inject.Inject
@@ -55,6 +57,7 @@
         FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
         NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
         PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
+        NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token
 
         // SceneContainer dependencies
         SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index ec03a6d..046e79c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -188,7 +188,7 @@
      * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
      * but not vice-versa.
      */
-    val isDreaming: Flow<Boolean> = repository.isDreaming
+    val isDreaming: StateFlow<Boolean> = repository.isDreaming
 
     /** Whether the system is dreaming with an overlay active */
     val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
@@ -205,7 +205,8 @@
                         trySendWithFailureLogging(
                             cameraLaunchSourceIntToModel(source),
                             TAG,
-                            "updated onCameraLaunchGestureDetected")
+                            "updated onCameraLaunchGestureDetected"
+                        )
                     }
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 51447cc..72f37fc 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -187,7 +187,6 @@
         applicationScope.launch {
             // TODO(b/296114544): Combine with some global hun state to make it visible!
             deviceProvisioningInteractor.isDeviceProvisioned
-                .distinctUntilChanged()
                 .flatMapLatest { isAllowedToBeVisible ->
                     if (isAllowedToBeVisible) {
                         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index 3dcaff3..b342722 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -52,8 +52,11 @@
     }
 
     fun getNotificationBuckets(): IntArray {
-        if (PriorityPeopleSection.isEnabled || NotificationMinimalismPrototype.V2.isEnabled
-            || NotificationClassificationFlag.isEnabled) {
+        if (
+            PriorityPeopleSection.isEnabled ||
+                NotificationMinimalismPrototype.isEnabled ||
+                NotificationClassificationFlag.isEnabled
+        ) {
             // We don't need this list to be adaptive, it can be the superset of all features.
             return PriorityBucket.getAllInOrder()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
new file mode 100644
index 0000000..a6605f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2024 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.collection.coordinator
+
+import android.annotation.SuppressLint
+import android.app.NotificationManager
+import android.os.UserHandle
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.ListEntry
+import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
+import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
+import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+/**
+ * If the setting is enabled, this will track seen notifications and ensure that they only show in
+ * the shelf on the lockscreen.
+ *
+ * This class is a replacement of the [OriginalUnseenKeyguardCoordinator].
+ */
+@CoordinatorScope
+@SuppressLint("SharedFlowCreation")
+class LockScreenMinimalismCoordinator
+@Inject
+constructor(
+    @Background private val bgDispatcher: CoroutineDispatcher,
+    private val dumpManager: DumpManager,
+    private val headsUpInteractor: HeadsUpNotificationInteractor,
+    private val logger: LockScreenMinimalismCoordinatorLogger,
+    @Application private val scope: CoroutineScope,
+    private val secureSettings: SecureSettings,
+    private val seenNotificationsInteractor: SeenNotificationsInteractor,
+    private val statusBarStateController: StatusBarStateController,
+    private val shadeInteractor: ShadeInteractor,
+) : Coordinator, Dumpable {
+
+    private val unseenNotifications = mutableSetOf<NotificationEntry>()
+    private var isShadeVisible = false
+    private var unseenFilterEnabled = false
+
+    override fun attach(pipeline: NotifPipeline) {
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+            return
+        }
+        pipeline.addPromoter(unseenNotifPromoter)
+        pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
+        pipeline.addCollectionListener(collectionListener)
+        scope.launch { trackUnseenFilterSettingChanges() }
+        dumpManager.registerDumpable(this)
+    }
+
+    private suspend fun trackSeenNotifications() {
+        coroutineScope {
+            launch { clearUnseenNotificationsWhenShadeIsExpanded() }
+            launch { markHeadsUpNotificationsAsSeen() }
+        }
+    }
+
+    private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
+        shadeInteractor.isShadeFullyExpanded.collectLatest { isExpanded ->
+            // Give keyguard events time to propagate, in case this expansion is part of the
+            // keyguard transition and not the user expanding the shade
+            delay(SHADE_VISIBLE_SEEN_TIMEOUT)
+            isShadeVisible = isExpanded
+            if (isExpanded) {
+                logger.logShadeVisible(unseenNotifications.size)
+                unseenNotifications.clear()
+                // no need to invalidateList; filtering is inactive while shade is open
+            } else {
+                logger.logShadeHidden()
+            }
+        }
+    }
+
+    private suspend fun markHeadsUpNotificationsAsSeen() {
+        headsUpInteractor.topHeadsUpRowIfPinned
+            .map { it?.let { headsUpInteractor.notificationKey(it) } }
+            .collectLatest { key ->
+                if (key == null) {
+                    logger.logTopHeadsUpRow(key = null, wasUnseenWhenPinned = false)
+                } else {
+                    val wasUnseenWhenPinned = unseenNotifications.any { it.key == key }
+                    logger.logTopHeadsUpRow(key, wasUnseenWhenPinned)
+                    if (wasUnseenWhenPinned) {
+                        delay(HEADS_UP_SEEN_TIMEOUT)
+                        val wasUnseenAfterDelay = unseenNotifications.removeIf { it.key == key }
+                        logger.logHunHasBeenSeen(key, wasUnseenAfterDelay)
+                        // no need to invalidateList; nothing should change until after heads up
+                    }
+                }
+            }
+    }
+
+    private fun unseenFeatureEnabled(): Flow<Boolean> {
+        // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
+        //  Or should we actually just repurpose using the existing setting?
+        if (NotificationMinimalismPrototype.isEnabled) {
+            return flowOf(true)
+        }
+        return secureSettings
+            // emit whenever the setting has changed
+            .observerFlow(
+                UserHandle.USER_ALL,
+                Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+            )
+            // perform a query immediately
+            .onStart { emit(Unit) }
+            // for each change, lookup the new value
+            .map {
+                secureSettings.getIntForUser(
+                    name = Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+                    def = 0,
+                    userHandle = UserHandle.USER_CURRENT,
+                ) == 1
+            }
+            // don't emit anything if nothing has changed
+            .distinctUntilChanged()
+            // perform lookups on the bg thread pool
+            .flowOn(bgDispatcher)
+            // only track the most recent emission, if events are happening faster than they can be
+            // consumed
+            .conflate()
+    }
+
+    private suspend fun trackUnseenFilterSettingChanges() {
+        unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+            // update local field and invalidate if necessary
+            if (isSettingEnabled != unseenFilterEnabled) {
+                unseenFilterEnabled = isSettingEnabled
+                unseenNotifPromoter.invalidateList("unseen setting changed")
+            }
+            // if the setting is enabled, then start tracking and filtering unseen notifications
+            logger.logTrackingUnseen(isSettingEnabled)
+            if (isSettingEnabled) {
+                trackSeenNotifications()
+            }
+        }
+    }
+
+    private val collectionListener =
+        object : NotifCollectionListener {
+            override fun onEntryAdded(entry: NotificationEntry) {
+                if (!isShadeVisible) {
+                    logger.logUnseenAdded(entry.key)
+                    unseenNotifications.add(entry)
+                }
+            }
+
+            override fun onEntryUpdated(entry: NotificationEntry) {
+                if (!isShadeVisible) {
+                    logger.logUnseenUpdated(entry.key)
+                    unseenNotifications.add(entry)
+                }
+            }
+
+            override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+                if (unseenNotifications.remove(entry)) {
+                    logger.logUnseenRemoved(entry.key)
+                }
+            }
+        }
+
+    private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+        // Only ever elevate a top unseen notification on keyguard, not even locked shade
+        if (statusBarStateController.state != StatusBarState.KEYGUARD) {
+            seenNotificationsInteractor.setTopOngoingNotification(null)
+            seenNotificationsInteractor.setTopUnseenNotification(null)
+            return
+        }
+        // On keyguard pick the top-ranked unseen or ongoing notification to elevate
+        val nonSummaryEntries: Sequence<NotificationEntry> =
+            list
+                .asSequence()
+                .flatMap {
+                    when (it) {
+                        is NotificationEntry -> listOfNotNull(it)
+                        is GroupEntry -> it.children
+                        else -> error("unhandled type of $it")
+                    }
+                }
+                .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
+        seenNotificationsInteractor.setTopOngoingNotification(
+            nonSummaryEntries
+                .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
+                .minByOrNull { it.ranking.rank }
+        )
+        seenNotificationsInteractor.setTopUnseenNotification(
+            nonSummaryEntries
+                .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
+                .minByOrNull { it.ranking.rank }
+        )
+    }
+
+    @VisibleForTesting
+    val unseenNotifPromoter =
+        object : NotifPromoter(TAG) {
+            override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
+                when {
+                    NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+                    seenNotificationsInteractor.isTopOngoingNotification(child) -> true
+                    !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+                    else -> seenNotificationsInteractor.isTopUnseenNotification(child)
+                }
+        }
+
+    val topOngoingSectioner =
+        object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
+            override fun isInSection(entry: ListEntry): Boolean {
+                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                return entry.anyEntry { notificationEntry ->
+                    seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
+                }
+            }
+        }
+
+    val topUnseenSectioner =
+        object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
+            override fun isInSection(entry: ListEntry): Boolean {
+                if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+                return entry.anyEntry { notificationEntry ->
+                    seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
+                }
+            }
+        }
+
+    private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
+        when {
+            predicate(representativeEntry) -> true
+            this !is GroupEntry -> false
+            else -> children.any(predicate)
+        }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) =
+        with(pw.asIndenting()) {
+            seenNotificationsInteractor.dump(this)
+            printCollection("unseen notifications", unseenNotifications) { println(it.key) }
+        }
+
+    companion object {
+        private const val TAG = "LockScreenMinimalismCoordinator"
+        private val SHADE_VISIBLE_SEEN_TIMEOUT = 0.25.seconds
+        private val HEADS_UP_SEEN_TIMEOUT = 0.75.seconds
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
new file mode 100644
index 0000000..e44a77c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 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.collection.coordinator
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.UnseenNotificationLog
+import javax.inject.Inject
+
+private const val TAG = "LockScreenMinimalismCoordinator"
+
+class LockScreenMinimalismCoordinatorLogger
+@Inject
+constructor(
+    @UnseenNotificationLog private val buffer: LogBuffer,
+) {
+
+    fun logTrackingUnseen(trackingUnseen: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { bool1 = trackingUnseen },
+            messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+        )
+
+    fun logShadeVisible(numUnseen: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { int1 = numUnseen },
+            messagePrinter = { "Shade expanded. Notifications marked as seen: $int1" }
+        )
+    }
+
+    fun logShadeHidden() {
+        buffer.log(TAG, LogLevel.DEBUG, "Shade no longer expanded.")
+    }
+
+    fun logUnseenAdded(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif added: $str1" },
+        )
+
+    fun logUnseenUpdated(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif updated: $str1" },
+        )
+
+    fun logUnseenRemoved(key: String) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = { str1 = key },
+            messagePrinter = { "Unseen notif removed: $str1" },
+        )
+
+    fun logHunHasBeenSeen(key: String, wasUnseen: Boolean) =
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = {
+                str1 = key
+                bool1 = wasUnseen
+            },
+            messagePrinter = { "Heads up notif has been seen: $str1 wasUnseen=$bool1" },
+        )
+
+    fun logTopHeadsUpRow(key: String?, wasUnseenWhenPinned: Boolean) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            messageInitializer = {
+                str1 = key
+                bool1 = wasUnseenWhenPinned
+            },
+            messagePrinter = { "New notif is top heads up: $str1 wasUnseen=$bool1" },
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 99327d1..73ce48b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -47,6 +47,7 @@
     hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator,
     keyguardCoordinator: KeyguardCoordinator,
     unseenKeyguardCoordinator: OriginalUnseenKeyguardCoordinator,
+    lockScreenMinimalismCoordinator: LockScreenMinimalismCoordinator,
     rankingCoordinator: RankingCoordinator,
     colorizedFgsCoordinator: ColorizedFgsCoordinator,
     deviceProvisionedCoordinator: DeviceProvisionedCoordinator,
@@ -87,7 +88,11 @@
         mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
         mCoordinators.add(hideNotifsForOtherUsersCoordinator)
         mCoordinators.add(keyguardCoordinator)
-        mCoordinators.add(unseenKeyguardCoordinator)
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mCoordinators.add(lockScreenMinimalismCoordinator)
+        } else {
+            mCoordinators.add(unseenKeyguardCoordinator)
+        }
         mCoordinators.add(rankingCoordinator)
         mCoordinators.add(colorizedFgsCoordinator)
         mCoordinators.add(deviceProvisionedCoordinator)
@@ -120,12 +125,12 @@
         }
 
         // Manually add Ordered Sections
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            mOrderedSections.add(unseenKeyguardCoordinator.topOngoingSectioner) // Top Ongoing
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
         }
         mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            mOrderedSections.add(unseenKeyguardCoordinator.topUnseenSectioner) // Top Unseen
+        if (NotificationMinimalismPrototype.isEnabled) {
+            mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
         }
         mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
         if (PriorityPeopleSection.isEnabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index 5dd1663..5b25b11 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.notification.collection.coordinator
 
 import android.annotation.SuppressLint
-import android.app.NotificationManager
 import android.os.UserHandle
 import android.provider.Settings
 import androidx.annotation.VisibleForTesting
@@ -30,21 +29,14 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.statusbar.StatusBarState
 import com.android.systemui.statusbar.expansionChanges
-import com.android.systemui.statusbar.notification.collection.GroupEntry
-import com.android.systemui.statusbar.notification.collection.ListEntry
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
-import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
-import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.statusbar.policy.headsUpEvents
 import com.android.systemui.util.asIndenting
@@ -73,9 +65,12 @@
 import kotlinx.coroutines.yield
 
 /**
- * Filters low priority and privacy-sensitive notifications from the lockscreen, and hides section
- * headers on the lockscreen. If enabled, it will also track and hide seen notifications on the
- * lockscreen.
+ * If the setting is enabled, this will track and hide seen notifications on the lockscreen.
+ *
+ * This is the "original" unseen keyguard coordinator because this is the logic originally developed
+ * for large screen devices where showing "seen" notifications on the lock screen was distracting.
+ * Moreover, this file was created during a project that will replace this logic, so the
+ * [LockScreenMinimalismCoordinator] is the expected replacement of this file.
  */
 @CoordinatorScope
 @SuppressLint("SharedFlowCreation")
@@ -100,10 +95,7 @@
     private var unseenFilterEnabled = false
 
     override fun attach(pipeline: NotifPipeline) {
-        if (NotificationMinimalismPrototype.V2.isEnabled) {
-            pipeline.addPromoter(unseenNotifPromoter)
-            pipeline.addOnBeforeTransformGroupsListener(::pickOutTopUnseenNotifs)
-        }
+        NotificationMinimalismPrototype.assertInLegacyMode()
         pipeline.addFinalizeFilter(unseenNotifFilter)
         pipeline.addCollectionListener(collectionListener)
         scope.launch { trackUnseenFilterSettingChanges() }
@@ -112,6 +104,7 @@
 
     private suspend fun trackSeenNotifications() {
         // Whether or not keyguard is visible (or occluded).
+        @Suppress("DEPRECATION")
         val isKeyguardPresentFlow: Flow<Boolean> =
             keyguardTransitionInteractor
                 .transitionValue(
@@ -265,11 +258,9 @@
     }
 
     private fun unseenFeatureEnabled(): Flow<Boolean> {
-        if (
-            NotificationMinimalismPrototype.V1.isEnabled ||
-                NotificationMinimalismPrototype.V2.isEnabled
-        ) {
-            return flowOf(true)
+        if (NotificationMinimalismPrototype.isEnabled) {
+            // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
+            return flowOf(false)
         }
         return secureSettings
             // emit whenever the setting has changed
@@ -340,110 +331,18 @@
             }
         }
 
-    private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
-        // Only ever elevate a top unseen notification on keyguard, not even locked shade
-        if (statusBarStateController.state != StatusBarState.KEYGUARD) {
-            seenNotificationsInteractor.setTopOngoingNotification(null)
-            seenNotificationsInteractor.setTopUnseenNotification(null)
-            return
-        }
-        // On keyguard pick the top-ranked unseen or ongoing notification to elevate
-        val nonSummaryEntries: Sequence<NotificationEntry> =
-            list
-                .asSequence()
-                .flatMap {
-                    when (it) {
-                        is NotificationEntry -> listOfNotNull(it)
-                        is GroupEntry -> it.children
-                        else -> error("unhandled type of $it")
-                    }
-                }
-                .filter { it.importance >= NotificationManager.IMPORTANCE_DEFAULT }
-        seenNotificationsInteractor.setTopOngoingNotification(
-            nonSummaryEntries
-                .filter { ColorizedFgsCoordinator.isRichOngoing(it) }
-                .minByOrNull { it.ranking.rank }
-        )
-        seenNotificationsInteractor.setTopUnseenNotification(
-            nonSummaryEntries
-                .filter { !ColorizedFgsCoordinator.isRichOngoing(it) && it in unseenNotifications }
-                .minByOrNull { it.ranking.rank }
-        )
-    }
-
-    @VisibleForTesting
-    val unseenNotifPromoter =
-        object : NotifPromoter("$TAG-unseen") {
-            override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
-                else if (!NotificationMinimalismPrototype.V2.ungroupTopUnseen) false
-                else
-                    seenNotificationsInteractor.isTopOngoingNotification(child) ||
-                        seenNotificationsInteractor.isTopUnseenNotification(child)
-        }
-
-    val topOngoingSectioner =
-        object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
-            override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
-                return entry.anyEntry { notificationEntry ->
-                    seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
-                }
-            }
-        }
-
-    val topUnseenSectioner =
-        object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
-            override fun isInSection(entry: ListEntry): Boolean {
-                if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return false
-                return entry.anyEntry { notificationEntry ->
-                    seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
-                }
-            }
-        }
-
-    private fun ListEntry.anyEntry(predicate: (NotificationEntry?) -> Boolean) =
-        when {
-            predicate(representativeEntry) -> true
-            this !is GroupEntry -> false
-            else -> children.any(predicate)
-        }
-
     @VisibleForTesting
     val unseenNotifFilter =
-        object : NotifFilter("$TAG-unseen") {
+        object : NotifFilter(TAG) {
 
             var hasFilteredAnyNotifs = false
 
-            /**
-             * Encapsulates a definition of "being on the keyguard". Note that these two definitions
-             * are wildly different: [StatusBarState.KEYGUARD] is when on the lock screen and does
-             * not include shade or occluded states, whereas [KeyguardRepository.isKeyguardShowing]
-             * is any state where the keyguard has not been dismissed, including locked shade and
-             * occluded lock screen.
-             *
-             * Returning false for locked shade and occluded states means that this filter will
-             * allow seen notifications to appear in the locked shade.
-             */
-            private fun isOnKeyguard(): Boolean =
-                if (NotificationMinimalismPrototype.V2.isEnabled) {
-                    false // disable this feature under this prototype
-                } else if (
-                    NotificationMinimalismPrototype.V1.isEnabled &&
-                        NotificationMinimalismPrototype.V1.showOnLockedShade
-                ) {
-                    statusBarStateController.state == StatusBarState.KEYGUARD
-                } else {
-                    keyguardRepository.isKeyguardShowing()
-                }
-
             override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean =
                 when {
                     // Don't apply filter if the setting is disabled
                     !unseenFilterEnabled -> false
                     // Don't apply filter if the keyguard isn't currently showing
-                    !isOnKeyguard() -> false
+                    !keyguardRepository.isKeyguardShowing() -> false
                     // Don't apply the filter if the notification is unseen
                     unseenNotifications.contains(entry) -> false
                     // Don't apply the filter to (non-promoted) group summaries
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index caa6c17..71c98b8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -150,8 +150,9 @@
                     if (entry == null) {
                         return false;
                     }
-                    boolean isTopUnseen = NotificationMinimalismPrototype.V2.isEnabled()
-                            && mSeenNotificationsInteractor.isTopUnseenNotification(entry);
+                    boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+                            && (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
+                                || mSeenNotificationsInteractor.isTopOngoingNotification(entry));
                     if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
                         return !mVisibilityLocationProvider.isInVisibleLocation(entry);
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
index bf44b9f..24b75d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt
@@ -30,6 +30,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
@@ -44,8 +45,17 @@
     private val shadeInteractor: ShadeInteractor,
 ) {
 
+    /** The top-ranked heads up row, regardless of pinned state */
     val topHeadsUpRow: Flow<HeadsUpRowKey?> = headsUpRepository.topHeadsUpRow
 
+    /** The top-ranked heads up row, if that row is pinned */
+    val topHeadsUpRowIfPinned: Flow<HeadsUpRowKey?> =
+        headsUpRepository.topHeadsUpRow
+            .flatMapLatest { repository ->
+                repository?.isPinned?.map { pinned -> repository.takeIf { pinned } } ?: flowOf(null)
+            }
+            .distinctUntilChanged()
+
     /** Set of currently pinned top-level heads up rows to be displayed. */
     val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy {
         if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) {
@@ -89,10 +99,10 @@
             flowOf(false)
         } else {
             combine(hasPinnedRows, headsUpRepository.isHeadsUpAnimatingAway) {
-                    hasPinnedRows,
-                    animatingAway ->
-                    hasPinnedRows || animatingAway
-                }
+                hasPinnedRows,
+                animatingAway ->
+                hasPinnedRows || animatingAway
+            }
         }
     }
 
@@ -127,6 +137,9 @@
 
     fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey
 
+    /** Returns the Notification Key (the standard string) of this row. */
+    fun notificationKey(key: HeadsUpRowKey): String = (key as HeadsUpRowRepository).key
+
     fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
         headsUpRepository.setHeadsUpAnimatingAway(animatingAway)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 85c66bd..948a3c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -16,10 +16,12 @@
 
 package com.android.systemui.statusbar.notification.domain.interactor
 
+import android.util.IndentingPrintWriter
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.util.printSection
 import javax.inject.Inject
 import kotlinx.coroutines.flow.StateFlow
 
@@ -41,24 +43,42 @@
 
     /** Set the entry that is identified as the top ongoing notification. */
     fun setTopOngoingNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topOngoingNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top ongoing notification. */
     fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
         else
             entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
 
     /** Set the entry that is identified as the top unseen notification. */
     fun setTopUnseenNotification(entry: NotificationEntry?) {
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) return
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
         notificationListRepository.topUnseenNotificationKey.value = entry?.key
     }
 
     /** Determine if the given notification is the top unseen notification. */
     fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
-        if (NotificationMinimalismPrototype.V2.isUnexpectedlyInLegacyMode()) false
+        if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
         else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
+
+    fun dump(pw: IndentingPrintWriter) =
+        with(pw) {
+            printSection("SeenNotificationsInteractor") {
+                print(
+                    "hasFilteredOutSeenNotifications",
+                    notificationListRepository.hasFilteredOutSeenNotifications.value
+                )
+                print(
+                    "topOngoingNotificationKey",
+                    notificationListRepository.topOngoingNotificationKey.value
+                )
+                print(
+                    "topUnseenNotificationKey",
+                    notificationListRepository.topUnseenNotificationKey.value
+                )
+            }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
index bf37036..06f3db5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
@@ -24,102 +24,43 @@
 /** Helper for reading or using the minimalism prototype flag state. */
 @Suppress("NOTHING_TO_INLINE")
 object NotificationMinimalismPrototype {
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
 
-    val version: Int by lazy {
-        SystemProperties.getInt("persist.notification_minimalism_prototype.version", 2)
-    }
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
 
-    object V1 {
-        /** The aconfig flag name */
-        const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+    /** Is the heads-up cycling animation enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationMinimalismPrototype()
 
-        /** A token used for dependency declaration */
-        val token: FlagToken
-            get() = FlagToken(FLAG_NAME, isEnabled)
+    /**
+     * The prototype will (by default) use a promoter to ensure that the top unseen notification is
+     * not grouped, but this property read allows that behavior to be disabled.
+     */
+    val ungroupTopUnseen: Boolean
+        get() =
+            if (isUnexpectedlyInLegacyMode()) false
+            else
+                SystemProperties.getBoolean(
+                    "persist.notification_minimalism_prototype.ungroup_top_unseen",
+                    false
+                )
 
-        /** Is the heads-up cycling animation enabled */
-        @JvmStatic
-        inline val isEnabled
-            get() = Flags.notificationMinimalismPrototype() && version == 1
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
 
-        /**
-         * the prototype will now show seen notifications on the locked shade by default, but this
-         * property read allows that to be quickly disabled for testing
-         */
-        val showOnLockedShade: Boolean
-            get() =
-                if (isUnexpectedlyInLegacyMode()) false
-                else
-                    SystemProperties.getBoolean(
-                        "persist.notification_minimalism_prototype.show_on_locked_shade",
-                        true
-                    )
-
-        /** gets the configurable max number of notifications */
-        val maxNotifs: Int
-            get() =
-                if (isUnexpectedlyInLegacyMode()) -1
-                else
-                    SystemProperties.getInt(
-                        "persist.notification_minimalism_prototype.lock_screen_max_notifs",
-                        1
-                    )
-
-        /**
-         * Called to ensure code is only run when the flag is enabled. This protects users from the
-         * unintended behaviors caused by accidentally running new logic, while also crashing on an
-         * eng build to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun isUnexpectedlyInLegacyMode() =
-            RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-        /**
-         * Called to ensure code is only run when the flag is disabled. This will throw an exception
-         * if the flag is enabled to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-    }
-    object V2 {
-        const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
-
-        /** A token used for dependency declaration */
-        val token: FlagToken
-            get() = FlagToken(FLAG_NAME, isEnabled)
-
-        /** Is the heads-up cycling animation enabled */
-        @JvmStatic
-        inline val isEnabled
-            get() = Flags.notificationMinimalismPrototype() && version == 2
-
-        /**
-         * The prototype will (by default) use a promoter to ensure that the top unseen notification
-         * is not grouped, but this property read allows that behavior to be disabled.
-         */
-        val ungroupTopUnseen: Boolean
-            get() =
-                if (isUnexpectedlyInLegacyMode()) false
-                else
-                    SystemProperties.getBoolean(
-                        "persist.notification_minimalism_prototype.ungroup_top_unseen",
-                        true
-                    )
-
-        /**
-         * Called to ensure code is only run when the flag is enabled. This protects users from the
-         * unintended behaviors caused by accidentally running new logic, while also crashing on an
-         * eng build to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun isUnexpectedlyInLegacyMode() =
-            RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
-
-        /**
-         * Called to ensure code is only run when the flag is disabled. This will throw an exception
-         * if the flag is enabled to ensure that the refactor author catches issues in testing.
-         */
-        @JvmStatic
-        inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-    }
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 391bc43..06222fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -74,7 +74,7 @@
 
     /** Whether we allow keyguard to show less important notifications above the shelf. */
     private val limitLockScreenToOneImportant
-        get() = NotificationMinimalismPrototype.V2.isEnabled
+        get() = NotificationMinimalismPrototype.isEnabled
 
     /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
     private var dividerHeight by notNull<Float>()
@@ -405,16 +405,8 @@
 
     fun updateResources() {
         maxKeyguardNotifications =
-            infiniteIfNegative(
-                if (NotificationMinimalismPrototype.V1.isEnabled) {
-                    NotificationMinimalismPrototype.V1.maxNotifs
-                } else {
-                    resources.getInteger(R.integer.keyguard_max_notification_count)
-                }
-            )
-        maxNotificationsExcludesMedia =
-            NotificationMinimalismPrototype.V1.isEnabled ||
-                NotificationMinimalismPrototype.V2.isEnabled
+            infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
+        maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
 
         dividerHeight =
             max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 97266c5..86c7c6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import dagger.Lazy
@@ -36,10 +37,16 @@
 constructor(
     private val statusBarStateController: SysuiStatusBarStateController,
     @Main private val mainExecutor: DelayableExecutor,
+    activityStarterInternal: Lazy<ActivityStarterInternalImpl>,
     legacyActivityStarter: Lazy<LegacyActivityStarterInternalImpl>
 ) : ActivityStarter {
 
-    private val activityStarterInternal: ActivityStarterInternal = legacyActivityStarter.get()
+    private val activityStarterInternal: ActivityStarterInternal =
+        if (SceneContainerFlag.isEnabled) {
+            activityStarterInternal.get()
+        } else {
+            legacyActivityStarter.get()
+        }
 
     override fun startPendingIntentDismissingKeyguard(intent: PendingIntent) {
         activityStarterInternal.startPendingIntentDismissingKeyguard(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
index ae98e1d..107bf1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
@@ -16,23 +16,93 @@
 
 package com.android.systemui.statusbar.phone
 
+import android.app.ActivityManager
+import android.app.ActivityOptions
+import android.app.ActivityTaskManager
 import android.app.PendingIntent
+import android.app.TaskStackBuilder
+import android.content.Context
 import android.content.Intent
+import android.content.res.Resources
 import android.os.Bundle
+import android.os.RemoteException
 import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.view.RemoteAnimationAdapter
 import android.view.View
+import android.view.WindowManager
+import com.android.systemui.ActivityIntentHelper
+import com.android.systemui.Flags
 import com.android.systemui.animation.ActivityTransitionAnimator
+import com.android.systemui.animation.DelegateTransitionAnimatorController
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.camera.CameraIntents
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.DisplayId
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.kotlin.getOrNull
+import dagger.Lazy
+import java.util.Optional
 import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 /**
- * Encapsulates the activity logic for activity starter when flexiglass is enabled.
+ * Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled.
  *
  * TODO: b/308819693
  */
+@ExperimentalCoroutinesApi
 @SysUISingleton
-class ActivityStarterInternalImpl @Inject constructor() : ActivityStarterInternal {
+class ActivityStarterInternalImpl
+@Inject
+constructor(
+    private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val centralSurfacesOptLazy: Lazy<Optional<CentralSurfaces>>,
+    private val context: Context,
+    @Main private val resources: Resources,
+    private val selectedUserInteractor: SelectedUserInteractor,
+    private val deviceEntryInteractor: DeviceEntryInteractor,
+    private val activityTransitionAnimator: ActivityTransitionAnimator,
+    @DisplayId private val displayId: Int,
+    private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
+    private val activityIntentHelper: ActivityIntentHelper,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+    private val assistManagerLazy: Lazy<AssistManager>,
+    @Main private val mainExecutor: DelayableExecutor,
+    private val shadeControllerLazy: Lazy<ShadeController>,
+    private val communalSceneInteractor: CommunalSceneInteractor,
+    private val statusBarWindowController: StatusBarWindowController,
+    private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>,
+    private val shadeAnimationInteractor: ShadeAnimationInteractor,
+    private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>,
+    private val commandQueue: CommandQueue,
+    private val lockScreenUserManager: NotificationLockscreenUserManager,
+) : ActivityStarterInternal {
+    private val centralSurfaces: CentralSurfaces?
+        get() = centralSurfacesOptLazy.get().getOrNull()
+
     override fun startPendingIntentDismissingKeyguard(
         intent: PendingIntent,
         dismissShade: Boolean,
@@ -45,7 +115,119 @@
         extraOptions: Bundle?,
         customMessage: String?,
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val animationController =
+            if (associatedView is ExpandableNotificationRow) {
+                centralSurfaces?.getAnimatorControllerFromNotification(associatedView)
+            } else animationController
+
+        val willLaunchResolverActivity =
+            intent.isActivity &&
+                activityIntentHelper.wouldPendingLaunchResolverActivity(
+                    intent,
+                    lockScreenUserManager.currentUserId,
+                )
+
+        val actuallyShowOverLockscreen =
+            showOverLockscreen &&
+                intent.isActivity &&
+                (skipLockscreenChecks ||
+                    activityIntentHelper.wouldPendingShowOverLockscreen(
+                        intent,
+                        lockScreenUserManager.currentUserId
+                    ))
+
+        val animate =
+            !willLaunchResolverActivity &&
+                animationController != null &&
+                shouldAnimateLaunch(intent.isActivity, actuallyShowOverLockscreen)
+
+        // We wrap animationCallback with a StatusBarLaunchAnimatorController so
+        // that the shade is collapsed after the animation (or when it is cancelled,
+        // aborted, etc).
+        val statusBarController =
+            wrapAnimationControllerForShadeOrStatusBar(
+                animationController = animationController,
+                dismissShade = dismissShade,
+                isLaunchForActivity = intent.isActivity,
+            )
+        val controller =
+            if (actuallyShowOverLockscreen) {
+                wrapAnimationControllerForLockscreen(dismissShade, statusBarController)
+            } else {
+                statusBarController
+            }
+
+        // If we animate, don't collapse the shade and defer the keyguard dismiss (in case we
+        // run the animation on the keyguard). The animation will take care of (instantly)
+        // collapsing the shade and hiding the keyguard once it is done.
+        val collapse = dismissShade && !animate
+        val runnable = Runnable {
+            try {
+                activityTransitionAnimator.startPendingIntentWithAnimation(
+                    controller,
+                    animate,
+                    intent.creatorPackage,
+                    actuallyShowOverLockscreen,
+                    object : ActivityTransitionAnimator.PendingIntentStarter {
+                        override fun startPendingIntent(
+                            animationAdapter: RemoteAnimationAdapter?
+                        ): Int {
+                            val options =
+                                ActivityOptions(
+                                    CentralSurfaces.getActivityOptions(displayId, animationAdapter)
+                                        .apply { extraOptions?.let { putAll(it) } }
+                                )
+                            // TODO b/221255671: restrict this to only be set for notifications
+                            options.isEligibleForLegacyPermissionPrompt = true
+                            options.setPendingIntentBackgroundActivityStartMode(
+                                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+                            )
+                            return intent.sendAndReturnResult(
+                                context,
+                                0,
+                                fillInIntent,
+                                null,
+                                null,
+                                null,
+                                options.toBundle()
+                            )
+                        }
+                    },
+                )
+            } catch (e: PendingIntent.CanceledException) {
+                // the stack trace isn't very helpful here.
+                // Just log the exception message.
+                Log.w(TAG, "Sending intent failed: $e")
+                if (!collapse) {
+                    // executeRunnableDismissingKeyguard did not collapse for us already.
+                    shadeControllerLazy.get().collapseOnMainThread()
+                }
+                // TODO: Dismiss Keyguard.
+            }
+            if (intent.isActivity) {
+                assistManagerLazy.get().hideAssist()
+                // This activity could have started while the device is dreaming, in which case
+                // the dream would occlude the activity. In order to show the newly started
+                // activity, we wake from the dream.
+                centralSurfaces?.awakenDreams()
+            }
+            intentSentUiThreadCallback?.let { mainExecutor.execute(it) }
+        }
+
+        if (!actuallyShowOverLockscreen) {
+            mainExecutor.execute {
+                executeRunnableDismissingKeyguard(
+                    runnable = runnable,
+                    afterKeyguardGone = willLaunchResolverActivity,
+                    dismissShade = collapse,
+                    willAnimateOnKeyguard = animate,
+                    customMessage = customMessage,
+                )
+            }
+        } else {
+            mainExecutor.execute(runnable)
+        }
     }
 
     override fun startActivityDismissingKeyguard(
@@ -59,7 +241,116 @@
         disallowEnterPictureInPictureWhileLaunching: Boolean,
         userHandle: UserHandle?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val userHandle: UserHandle = userHandle ?: getActivityUserHandle(intent)
+
+        if (onlyProvisioned && !deviceProvisioningInteractor.isDeviceProvisioned()) return
+
+        val willLaunchResolverActivity: Boolean =
+            activityIntentHelper.wouldLaunchResolverActivity(
+                intent,
+                selectedUserInteractor.getSelectedUserId(),
+            )
+
+        val animate =
+            animationController != null &&
+                !willLaunchResolverActivity &&
+                shouldAnimateLaunch(isActivityIntent = true)
+        val animController =
+            wrapAnimationControllerForShadeOrStatusBar(
+                animationController = animationController,
+                dismissShade = dismissShade,
+                isLaunchForActivity = true,
+            )
+
+        // If we animate, we will dismiss the shade only once the animation is done. This is
+        // taken care of by the StatusBarLaunchAnimationController.
+        val dismissShadeDirectly = dismissShade && animController == null
+
+        val runnable = Runnable {
+            assistManagerLazy.get().hideAssist()
+            intent.flags =
+                if (intent.flags and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT != 0) {
+                    Intent.FLAG_ACTIVITY_NEW_TASK
+                } else {
+                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+                }
+            intent.addFlags(flags)
+            val result = intArrayOf(ActivityManager.START_CANCELED)
+            activityTransitionAnimator.startIntentWithAnimation(
+                animController,
+                animate,
+                intent.getPackage()
+            ) { adapter: RemoteAnimationAdapter? ->
+                val options =
+                    ActivityOptions(CentralSurfaces.getActivityOptions(displayId, adapter))
+
+                // We know that the intent of the caller is to dismiss the keyguard and
+                // this runnable is called right after the keyguard is solved, so we tell
+                // WM that we should dismiss it to avoid flickers when opening an activity
+                // that can also be shown over the keyguard.
+                options.setDismissKeyguardIfInsecure()
+                options.setDisallowEnterPictureInPictureWhileLaunching(
+                    disallowEnterPictureInPictureWhileLaunching
+                )
+                if (CameraIntents.isInsecureCameraIntent(intent)) {
+                    // Normally an activity will set it's requested rotation
+                    // animation on its window. However when launching an activity
+                    // causes the orientation to change this is too late. In these cases
+                    // the default animation is used. This doesn't look good for
+                    // the camera (as it rotates the camera contents out of sync
+                    // with physical reality). So, we ask the WindowManager to
+                    // force the cross fade animation if an orientation change
+                    // happens to occur during the launch.
+                    options.rotationAnimationHint =
+                        WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS
+                }
+                if (Settings.Panel.ACTION_VOLUME == intent.action) {
+                    // Settings Panel is implemented as activity(not a dialog), so
+                    // underlying app is paused and may enter picture-in-picture mode
+                    // as a result.
+                    // So we need to disable picture-in-picture mode here
+                    // if it is volume panel.
+                    options.setDisallowEnterPictureInPictureWhileLaunching(true)
+                }
+                try {
+                    result[0] =
+                        ActivityTaskManager.getService()
+                            .startActivityAsUser(
+                                null,
+                                context.basePackageName,
+                                context.attributionTag,
+                                intent,
+                                intent.resolveTypeIfNeeded(context.contentResolver),
+                                null,
+                                null,
+                                0,
+                                Intent.FLAG_ACTIVITY_NEW_TASK,
+                                null,
+                                options.toBundle(),
+                                userHandle.identifier,
+                            )
+                } catch (e: RemoteException) {
+                    Log.w(TAG, "Unable to start activity", e)
+                }
+                result[0]
+            }
+            callback?.onActivityStarted(result[0])
+        }
+        val cancelRunnable = Runnable {
+            callback?.onActivityStarted(ActivityManager.START_CANCELED)
+        }
+        // Do not deferKeyguard when occluded because, when keyguard is occluded,
+        // we do not launch the activity until keyguard is done.
+        executeRunnableDismissingKeyguard(
+            runnable,
+            cancelRunnable,
+            dismissShadeDirectly,
+            willLaunchResolverActivity,
+            deferred = !isKeyguardOccluded(),
+            animate,
+            customMessage,
+        )
     }
 
     override fun startActivity(
@@ -69,7 +360,64 @@
         showOverLockscreenWhenLocked: Boolean,
         userHandle: UserHandle?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val userHandle = userHandle ?: getActivityUserHandle(intent)
+        // Make sure that we dismiss the keyguard if it is directly dismissible or when we don't
+        // want to show the activity above it.
+        if (deviceEntryInteractor.isUnlocked.value || !showOverLockscreenWhenLocked) {
+            startActivityDismissingKeyguard(
+                intent = intent,
+                onlyProvisioned = false,
+                dismissShade = dismissShade,
+                disallowEnterPictureInPictureWhileLaunching = false,
+                callback = null,
+                flags = 0,
+                animationController = animationController,
+                userHandle = userHandle,
+            )
+            return
+        }
+
+        val animate =
+            animationController != null &&
+                shouldAnimateLaunch(
+                    isActivityIntent = true,
+                    showOverLockscreen = showOverLockscreenWhenLocked
+                )
+
+        var controller: ActivityTransitionAnimator.Controller? = null
+        if (animate) {
+            // Wrap the animation controller to dismiss the shade and set
+            // mIsLaunchingActivityOverLockscreen during the animation.
+            val delegate =
+                wrapAnimationControllerForShadeOrStatusBar(
+                    animationController = animationController,
+                    dismissShade = dismissShade,
+                    isLaunchForActivity = true,
+                )
+            controller = wrapAnimationControllerForLockscreen(dismissShade, delegate)
+        } else if (dismissShade) {
+            // The animation will take care of dismissing the shade at the end of the animation.
+            // If we don't animate, collapse it directly.
+            shadeControllerLazy.get().cancelExpansionAndCollapseShade()
+        }
+
+        // We should exit the dream to prevent the activity from starting below the
+        // dream.
+        if (keyguardInteractor.isDreaming.value) {
+            centralSurfaces?.awakenDreams()
+        }
+
+        activityTransitionAnimator.startIntentWithAnimation(
+            controller,
+            animate,
+            intent.getPackage(),
+            showOverLockscreenWhenLocked
+        ) { adapter: RemoteAnimationAdapter? ->
+            TaskStackBuilder.create(context)
+                .addNextIntent(intent)
+                .startActivities(CentralSurfaces.getActivityOptions(displayId, adapter), userHandle)
+        }
     }
 
     override fun dismissKeyguardThenExecute(
@@ -78,7 +426,23 @@
         afterKeyguardGone: Boolean,
         customMessage: String?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        Log.i(TAG, "Invoking dismissKeyguardThenExecute, afterKeyguardGone: $afterKeyguardGone")
+
+        // TODO b/308819693: startWakeAndUnlock animation when pulsing
+
+        if (isKeyguardShowing()) {
+            statusBarKeyguardViewManagerLazy
+                .get()
+                .dismissWithAction(action, cancel, afterKeyguardGone, customMessage)
+        } else {
+            // If the keyguard isn't showing but the device is dreaming, we should exit the
+            // dream.
+            if (keyguardInteractor.isDreaming.value) {
+                centralSurfaces?.awakenDreams()
+            }
+            action.onDismiss()
+        }
     }
 
     override fun executeRunnableDismissingKeyguard(
@@ -90,10 +454,195 @@
         willAnimateOnKeyguard: Boolean,
         customMessage: String?
     ) {
-        TODO("Not yet implemented b/308819693")
+        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return
+        val onDismissAction: ActivityStarter.OnDismissAction =
+            object : ActivityStarter.OnDismissAction {
+                override fun onDismiss(): Boolean {
+                    if (runnable != null) {
+                        if (isKeyguardOccluded()) {
+                            statusBarKeyguardViewManagerLazy
+                                .get()
+                                .addAfterKeyguardGoneRunnable(runnable)
+                        } else {
+                            mainExecutor.execute(runnable)
+                        }
+                    }
+                    if (dismissShade) {
+                        shadeControllerLazy.get().collapseShadeForActivityStart()
+                    }
+                    if (Flags.communalHub()) {
+                        communalSceneInteractor.changeSceneForActivityStartOnDismissKeyguard()
+                    }
+                    return deferred
+                }
+
+                override fun willRunAnimationOnKeyguard(): Boolean {
+                    if (Flags.communalHub() && communalSceneInteractor.isIdleOnCommunal.value) {
+                        // Override to false when launching activity over the hub that requires auth
+                        return false
+                    }
+                    return willAnimateOnKeyguard
+                }
+            }
+        dismissKeyguardThenExecute(
+            onDismissAction,
+            cancelAction,
+            afterKeyguardGone,
+            customMessage,
+        )
     }
 
     override fun shouldAnimateLaunch(isActivityIntent: Boolean): Boolean {
-        TODO("Not yet implemented b/308819693")
+        return shouldAnimateLaunch(isActivityIntent, false)
+    }
+
+    /**
+     * Whether we should animate an activity launch.
+     *
+     * Note: This method must be called *before* dismissing the keyguard.
+     */
+    private fun shouldAnimateLaunch(
+        isActivityIntent: Boolean,
+        showOverLockscreen: Boolean,
+    ): Boolean {
+        // TODO(b/294418322): always support launch animations when occluded.
+        val ignoreOcclusion = showOverLockscreen && Flags.mediaLockscreenLaunchAnimation()
+        if (isKeyguardOccluded() && !ignoreOcclusion) {
+            return false
+        }
+
+        // Always animate if we are not showing the keyguard or if we animate over the lockscreen
+        // (without unlocking it).
+        if (showOverLockscreen || !isKeyguardShowing()) {
+            return true
+        }
+
+        // We don't animate non-activity launches as they can break the animation.
+        // TODO(b/184121838): Support non activity launches on the lockscreen.
+        return isActivityIntent
+    }
+
+    /** Retrieves the current user handle to start the Activity. */
+    private fun getActivityUserHandle(intent: Intent): UserHandle {
+        val packages: Array<String> = resources.getStringArray(R.array.system_ui_packages)
+        for (pkg in packages) {
+            val componentName = intent.component ?: break
+            if (pkg == componentName.packageName) {
+                return UserHandle(UserHandle.myUserId())
+            }
+        }
+        return UserHandle(selectedUserInteractor.getSelectedUserId())
+    }
+
+    private fun isKeyguardShowing(): Boolean {
+        return !deviceEntryInteractor.isDeviceEntered.value
+    }
+
+    private fun isKeyguardOccluded(): Boolean {
+        return keyguardTransitionInteractor.getCurrentState() == KeyguardState.OCCLUDED
+    }
+
+    /**
+     * Return a [ActivityTransitionAnimator.Controller] wrapping `animationController` so that:
+     * - if it launches in the notification shade window and `dismissShade` is true, then the shade
+     *   will be instantly dismissed at the end of the animation.
+     * - if it launches in status bar window, it will make the status bar window match the device
+     *   size during the animation (that way, the animation won't be clipped by the status bar
+     *   size).
+     *
+     * @param animationController the controller that is wrapped and will drive the main animation.
+     * @param dismissShade whether the notification shade will be dismissed at the end of the
+     *   animation. This is ignored if `animationController` is not animating in the shade window.
+     * @param isLaunchForActivity whether the launch is for an activity.
+     */
+    private fun wrapAnimationControllerForShadeOrStatusBar(
+        animationController: ActivityTransitionAnimator.Controller?,
+        dismissShade: Boolean,
+        isLaunchForActivity: Boolean,
+    ): ActivityTransitionAnimator.Controller? {
+        if (animationController == null) {
+            return null
+        }
+        val rootView = animationController.transitionContainer.rootView
+        val controllerFromStatusBar: Optional<ActivityTransitionAnimator.Controller> =
+            statusBarWindowController.wrapAnimationControllerIfInStatusBar(
+                rootView,
+                animationController
+            )
+        if (controllerFromStatusBar.isPresent) {
+            return controllerFromStatusBar.get()
+        }
+
+        centralSurfaces?.let {
+            // If the view is not in the status bar, then we are animating a view in the shade.
+            // We have to make sure that we collapse it when the animation ends or is cancelled.
+            if (dismissShade) {
+                return StatusBarTransitionAnimatorController(
+                    animationController,
+                    shadeAnimationInteractor,
+                    shadeControllerLazy.get(),
+                    notifShadeWindowControllerLazy.get(),
+                    commandQueue,
+                    displayId,
+                    isLaunchForActivity
+                )
+            }
+        }
+
+        return animationController
+    }
+
+    /**
+     * Wraps an animation controller so that if an activity would be launched on top of the
+     * lockscreen, the correct flags are set for it to be occluded.
+     */
+    private fun wrapAnimationControllerForLockscreen(
+        dismissShade: Boolean,
+        animationController: ActivityTransitionAnimator.Controller?
+    ): ActivityTransitionAnimator.Controller? {
+        return animationController?.let {
+            object : DelegateTransitionAnimatorController(it) {
+                override fun onIntentStarted(willAnimate: Boolean) {
+                    delegate.onIntentStarted(willAnimate)
+                    if (willAnimate) {
+                        centralSurfaces?.setIsLaunchingActivityOverLockscreen(true, dismissShade)
+                    }
+                }
+
+                override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
+                    super.onTransitionAnimationStart(isExpandingFullyAbove)
+                    if (Flags.communalHub()) {
+                        communalSceneInteractor.snapToScene(
+                            CommunalScenes.Blank,
+                            ActivityTransitionAnimator.TIMINGS.totalDuration
+                        )
+                    }
+                }
+
+                override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
+                    // Set mIsLaunchingActivityOverLockscreen to false before actually
+                    // finishing the animation so that we can assume that
+                    // mIsLaunchingActivityOverLockscreen being true means that we will
+                    // collapse the shade (or at least run the post collapse runnables)
+                    // later on.
+                    centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false)
+                    delegate.onTransitionAnimationEnd(isExpandingFullyAbove)
+                }
+
+                override fun onTransitionAnimationCancelled(newKeyguardOccludedState: Boolean?) {
+                    // Set mIsLaunchingActivityOverLockscreen to false before actually
+                    // finishing the animation so that we can assume that
+                    // mIsLaunchingActivityOverLockscreen being true means that we will
+                    // collapse the shade (or at least run the // post collapse
+                    // runnables) later on.
+                    centralSurfaces?.setIsLaunchingActivityOverLockscreen(false, false)
+                    delegate.onTransitionAnimationCancelled(newKeyguardOccludedState)
+                }
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "ActivityStarterInternalImpl"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
index 4838554..07bbca7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
@@ -31,6 +31,13 @@
      * @see android.provider.Settings.Global.DEVICE_PROVISIONED
      */
     val isDeviceProvisioned: Flow<Boolean>
+
+    /**
+     * Whether this device has been provisioned.
+     *
+     * @see android.provider.Settings.Global.DEVICE_PROVISIONED
+     */
+    fun isDeviceProvisioned(): Boolean
 }
 
 @Module
@@ -48,11 +55,15 @@
         val listener =
             object : DeviceProvisionedController.DeviceProvisionedListener {
                 override fun onDeviceProvisionedChanged() {
-                    trySend(deviceProvisionedController.isDeviceProvisioned)
+                    trySend(isDeviceProvisioned())
                 }
             }
         deviceProvisionedController.addCallback(listener)
-        trySend(deviceProvisionedController.isDeviceProvisioned)
+        trySend(isDeviceProvisioned())
         awaitClose { deviceProvisionedController.removeCallback(listener) }
     }
+
+    override fun isDeviceProvisioned(): Boolean {
+        return deviceProvisionedController.isDeviceProvisioned
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
index 66ed092..ace4ce0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
@@ -26,7 +26,7 @@
 class DeviceProvisioningInteractor
 @Inject
 constructor(
-    repository: DeviceProvisioningRepository,
+    private val repository: DeviceProvisioningRepository,
 ) {
     /**
      * Whether this device has been provisioned.
@@ -34,4 +34,8 @@
      * @see android.provider.Settings.Global.DEVICE_PROVISIONED
      */
     val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned
+
+    fun isDeviceProvisioned(): Boolean {
+        return repository.isDeviceProvisioned()
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 52af907..64eadb7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index acb005f..0407fc1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -42,7 +42,7 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 // this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.V2.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
 class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
     lateinit var manager: NotificationSectionsFeatureManager
     private val proxyFake = DeviceConfigProxyFake()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index cc2ef53..12cfdcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -35,7 +35,6 @@
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications
 import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.batteryController
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
index da956ec..8b4de2b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
@@ -22,13 +22,12 @@
  * The [modifier] function will be passed an instance of a NotificationEntryBuilder. Any
  * modifications made to the builder will be applied to the [entry].
  */
-inline fun modifyEntry(
-    entry: NotificationEntry,
+inline fun NotificationEntry.modifyEntry(
     crossinline modifier: NotificationEntryBuilder.() -> Unit
 ) {
-    val builder = NotificationEntryBuilder(entry)
+    val builder = NotificationEntryBuilder(this)
     modifier(builder)
-    builder.apply(entry)
+    builder.apply(this)
 }
 
 fun getAttachState(entry: ListEntry): ListAttachState {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
new file mode 100644
index 0000000..77d97bb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
+import com.android.systemui.util.settings.fakeSettings
+
+var Kosmos.lockScreenMinimalismCoordinator by
+    Kosmos.Fixture {
+        LockScreenMinimalismCoordinator(
+            bgDispatcher = testDispatcher,
+            dumpManager = dumpManager,
+            headsUpInteractor = headsUpNotificationInteractor,
+            logger = lockScreenMinimalismCoordinatorLogger,
+            scope = testScope.backgroundScope,
+            secureSettings = fakeSettings,
+            seenNotificationsInteractor = seenNotificationsInteractor,
+            statusBarStateController = statusBarStateController,
+            shadeInteractor = shadeInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
new file mode 100644
index 0000000..77aeb44
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLoggerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.lockScreenMinimalismCoordinatorLogger by
+    Kosmos.Fixture { LockScreenMinimalismCoordinatorLogger(logcatLogBuffer()) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
index 492e87b..7e8f1a9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt
@@ -22,14 +22,19 @@
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
 
 val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() }
 
 class FakeHeadsUpNotificationRepository : HeadsUpRepository {
     override val isHeadsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false)
-    override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null)
-    override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> =
-        MutableStateFlow(emptySet())
+
+    val orderedHeadsUpRows = MutableStateFlow(emptyList<HeadsUpRowRepository>())
+    override val topHeadsUpRow: Flow<HeadsUpRowRepository?> =
+        orderedHeadsUpRows.map { it.firstOrNull() }.distinctUntilChanged()
+    override val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> =
+        orderedHeadsUpRows.map { it.toSet() }.distinctUntilChanged()
 
     override fun setHeadsUpAnimatingAway(animatingAway: Boolean) {
         isHeadsUpAnimatingAway.value = animatingAway
@@ -38,4 +43,12 @@
     override fun snooze() {
         // do nothing
     }
+
+    fun setNotifications(notifications: List<HeadsUpRowRepository>) {
+        this.orderedHeadsUpRows.value = notifications.toList()
+    }
+
+    fun setNotifications(vararg notifications: HeadsUpRowRepository) {
+        this.orderedHeadsUpRows.value = notifications.toList()
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
deleted file mode 100644
index 9be7dfe..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2024 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.stack.data.repository
-
-import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
-
-fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) {
-    setNotifications(*notifications.toTypedArray())
-}
-
-fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) {
-    this.activeHeadsUpRows.value = notifications.toSet()
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
index 9247e88..e3176f1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
@@ -26,9 +26,14 @@
 class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository {
     private val _isDeviceProvisioned = MutableStateFlow(true)
     override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned
+
     fun setDeviceProvisioned(isProvisioned: Boolean) {
         _isDeviceProvisioned.value = isProvisioned
     }
+
+    override fun isDeviceProvisioned(): Boolean {
+        return _isDeviceProvisioned.value
+    }
 }
 
 @Module
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index ecbbd46..5d10780 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1099,14 +1099,9 @@
             final InputMethodSettings newSettings = queryInputMethodServicesInternal(mContext,
                     userId, AdditionalSubtypeMapRepository.get(userId), DirectBootAwareness.AUTO);
             InputMethodSettingsRepository.put(userId, newSettings);
-            if (!mConcurrentMultiUserModeEnabled) {
-                // We need to rebuild IMEs.
-                postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
-                updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
-            } else {
-                // TODO(b/352758479): Stop relying on initializeVisibleBackgroundUserLocked()
-                initializeVisibleBackgroundUserLocked(userId);
-            }
+            // We need to rebuild IMEs.
+            postInputMethodSettingUpdatedLocked(false /* resetDefaultEnabledIme */, userId);
+            updateInputMethodsFromSettingsLocked(true /* enabledChanged */, userId);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
index d9f11b1..05d4c82 100644
--- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java
@@ -226,6 +226,14 @@
                         : getDefaultMinAspectRatio();
     }
 
+    float getDefaultMinAspectRatioForUnresizableAppsFromConfig() {
+        return mAppCompatConfiguration.getDefaultMinAspectRatioForUnresizableApps();
+    }
+
+    boolean isSplitScreenAspectRatioForUnresizableAppsEnabled() {
+        return mAppCompatConfiguration.getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+    }
+
     private float getDisplaySizeMinAspectRatio() {
         final DisplayArea displayArea = mActivityRecord.getDisplayArea();
         if (displayArea == null) {
@@ -278,7 +286,7 @@
         return getSplitScreenAspectRatio();
     }
 
-    private float getDefaultMinAspectRatio() {
+    float getDefaultMinAspectRatio() {
         if (mActivityRecord.getDisplayArea() == null
                 || !mAppCompatConfiguration
                 .getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox()) {
diff --git a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
index f9f5058..3ecdff6 100644
--- a/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
+++ b/services/core/java/com/android/server/wm/DesktopModeBoundsCalculator.java
@@ -16,18 +16,33 @@
 
 package com.android.server.wm;
 
+import static android.content.pm.ActivityInfo.isFixedOrientationLandscape;
+import static android.content.pm.ActivityInfo.isFixedOrientationPortrait;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import static com.android.server.wm.AppCompatConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+import static com.android.server.wm.AppCompatUtils.computeAspectRatio;
 import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity;
 import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityOptions;
+import android.app.AppCompatTaskInfo;
+import android.app.TaskInfo;
 import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
 import android.graphics.Rect;
 import android.os.SystemProperties;
 import android.util.Size;
 import android.view.Gravity;
 
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
 import java.util.function.Consumer;
 
 /**
@@ -38,6 +53,8 @@
 
     public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties
             .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
+    public static final int DESKTOP_MODE_LANDSCAPE_APP_PADDING = SystemProperties
+            .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25);
 
     /**
      * Updates launch bounds for an activity with respect to its activity options, window layout,
@@ -48,12 +65,8 @@
             @NonNull Rect outBounds, @NonNull Consumer<String> logger) {
         // Use stable frame instead of raw frame to avoid launching freeform windows on top of
         // stable insets, which usually are system widgets such as sysbar & navbar.
-        final TaskDisplayArea displayArea = task.getDisplayArea();
-        final Rect screenBounds = displayArea.getBounds();
         final Rect stableBounds = new Rect();
-        displayArea.getStableRect(stableBounds);
-        final int desiredWidth = (int) (stableBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        final int desiredHeight = (int) (stableBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        task.getDisplayArea().getStableRect(stableBounds);
 
         if (options != null && options.getLaunchBounds() != null) {
             outBounds.set(options.getLaunchBounds());
@@ -63,37 +76,282 @@
             final int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
             if (layout.hasSpecifiedSize()) {
                 calculateLayoutBounds(stableBounds, layout, outBounds,
-                        new Size(desiredWidth, desiredHeight));
+                        calculateIdealSize(stableBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
                 applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
                         stableBounds);
                 logger.accept("layout specifies sizes, inheriting size and applying gravity");
             } else if (verticalGravity > 0 || horizontalGravity > 0) {
-                calculateAndCentreInitialBounds(outBounds, screenBounds);
+                outBounds.set(calculateInitialBounds(task, activity, stableBounds));
                 applyLayoutGravity(verticalGravity, horizontalGravity, outBounds,
                         stableBounds);
                 logger.accept("layout specifies gravity, applying desired bounds and gravity");
             }
         } else {
-            calculateAndCentreInitialBounds(outBounds, screenBounds);
+            outBounds.set(calculateInitialBounds(task, activity, stableBounds));
             logger.accept("layout not specified, applying desired bounds");
         }
     }
 
     /**
-     * Calculates the initial height and width of a task in desktop mode and centers it within the
-     * window bounds.
+     * Calculates the initial bounds required for an application to fill a scale of the display
+     * bounds without any letterboxing. This is done by taking into account the applications
+     * fullscreen size, aspect ratio, orientation and resizability to calculate an area this is
+     * compatible with the applications previous configuration.
      */
-    private static void calculateAndCentreInitialBounds(@NonNull Rect outBounds,
+    private static @NonNull Rect calculateInitialBounds(@NonNull Task task,
+            @NonNull ActivityRecord activity, @NonNull Rect stableBounds
+    ) {
+        final TaskInfo taskInfo = task.getTaskInfo();
+        // Display bounds not taking into account insets.
+        final TaskDisplayArea displayArea = task.getDisplayArea();
+        final Rect screenBounds = displayArea.getBounds();
+        final Size idealSize = calculateIdealSize(screenBounds, DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        if (!Flags.enableWindowingDynamicInitialBounds()) {
+            return centerInScreen(idealSize, screenBounds);
+        }
+        // TODO(b/353457301): Replace with app compat aspect ratio method when refactoring complete.
+        float appAspectRatio = calculateAspectRatio(task, activity);
+        final float tdaWidth = stableBounds.width();
+        final float tdaHeight = stableBounds.height();
+        final int activityOrientation = activity.getOverrideOrientation();
+        final Size initialSize = switch (taskInfo.configuration.orientation) {
+            case ORIENTATION_LANDSCAPE -> {
+                // Device in landscape orientation.
+                if (appAspectRatio == 0) {
+                    appAspectRatio = 1;
+                }
+                if (taskInfo.isResizeable) {
+                    if (isFixedOrientationPortrait(activityOrientation)) {
+                        // For portrait resizeable activities, respect apps fullscreen width but
+                        // apply ideal size height.
+                        yield new Size((int) ((tdaHeight / appAspectRatio) + 0.5f),
+                                idealSize.getHeight());
+                    }
+                    // For landscape resizeable activities, simply apply ideal size.
+                    yield idealSize;
+                }
+                // If activity is unresizeable, regardless of orientation, calculate maximum size
+                // (within the ideal size) maintaining original aspect ratio.
+                yield maximizeSizeGivenAspectRatio(
+                        activity.getOverrideOrientation(), idealSize, appAspectRatio);
+            }
+            case ORIENTATION_PORTRAIT -> {
+                // Device in portrait orientation.
+                final int customPortraitWidthForLandscapeApp = screenBounds.width()
+                        - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+                if (taskInfo.isResizeable) {
+                    if (isFixedOrientationLandscape(activityOrientation)) {
+                        if (appAspectRatio == 0) {
+                            appAspectRatio = tdaWidth / (tdaWidth - 1);
+                        }
+                        // For landscape resizeable activities, respect apps fullscreen height and
+                        // apply custom app width.
+                        yield new Size(customPortraitWidthForLandscapeApp,
+                                (int) ((tdaWidth / appAspectRatio) + 0.5f));
+                    }
+                    // For portrait resizeable activities, simply apply ideal size.
+                    yield idealSize;
+                }
+                if (appAspectRatio == 0) {
+                    appAspectRatio = 1;
+                }
+                if (isFixedOrientationLandscape(activityOrientation)) {
+                    // For landscape unresizeable activities, apply custom app width to ideal size
+                    // and calculate maximum size with this area while maintaining original aspect
+                    // ratio.
+                    yield maximizeSizeGivenAspectRatio(activityOrientation,
+                            new Size(customPortraitWidthForLandscapeApp, idealSize.getHeight()),
+                            appAspectRatio);
+                }
+                // For portrait unresizeable activities, calculate maximum size (within the ideal
+                // size) maintaining original aspect ratio.
+                yield maximizeSizeGivenAspectRatio(activityOrientation, idealSize, appAspectRatio);
+            }
+            default -> idealSize;
+        };
+        return centerInScreen(initialSize, screenBounds);
+    }
+
+    /**
+     * Calculates the largest size that can fit in a given area while maintaining a specific aspect
+     * ratio.
+     */
+    private static @NonNull Size maximizeSizeGivenAspectRatio(
+            @ActivityInfo.ScreenOrientation int orientation,
+            @NonNull Size targetArea,
+            float aspectRatio
+    ) {
+        final int targetHeight = targetArea.getHeight();
+        final int targetWidth = targetArea.getWidth();
+        final int finalHeight;
+        final int finalWidth;
+        if (isFixedOrientationPortrait(orientation)) {
+            // Portrait activity.
+            // Calculate required width given ideal height and aspect ratio.
+            int tempWidth = (int) (targetHeight / aspectRatio);
+            if (tempWidth <= targetWidth) {
+                // If the calculated width does not exceed the ideal width, overall size is within
+                // ideal size and can be applied.
+                finalHeight = targetHeight;
+                finalWidth = tempWidth;
+            } else {
+                // Applying target height cause overall size to exceed ideal size when maintain
+                // aspect ratio. Instead apply ideal width and calculate required height to respect
+                // aspect ratio.
+                finalWidth = targetWidth;
+                finalHeight = (int) (finalWidth * aspectRatio);
+            }
+        } else {
+            // Landscape activity.
+            // Calculate required width given ideal height and aspect ratio.
+            int tempWidth = (int) (targetHeight * aspectRatio);
+            if (tempWidth <= targetWidth) {
+                // If the calculated width does not exceed the ideal width, overall size is within
+                // ideal size and can be applied.
+                finalHeight = targetHeight;
+                finalWidth = tempWidth;
+            } else {
+                // Applying target height cause overall size to exceed ideal size when maintain
+                // aspect ratio. Instead apply ideal width and calculate required height to respect
+                // aspect ratio.
+                finalWidth = targetWidth;
+                finalHeight = (int) (finalWidth / aspectRatio);
+            }
+        }
+        return new Size(finalWidth, finalHeight);
+    }
+
+    /**
+     * Calculates the aspect ratio of an activity from its fullscreen bounds.
+     */
+    @VisibleForTesting
+    static float calculateAspectRatio(@NonNull Task task, @NonNull ActivityRecord activity) {
+        final TaskInfo taskInfo = task.getTaskInfo();
+        final float fullscreenWidth = task.getDisplayArea().getBounds().width();
+        final float fullscreenHeight = task.getDisplayArea().getBounds().height();
+        final float maxAspectRatio = activity.getMaxAspectRatio();
+        final float minAspectRatio = activity.getMinAspectRatio();
+        float desiredAspectRatio = 0;
+        if (taskInfo.isRunning) {
+            final AppCompatTaskInfo appCompatTaskInfo =  taskInfo.appCompatTaskInfo;
+            if (appCompatTaskInfo.topActivityBoundsLetterboxed) {
+                desiredAspectRatio = (float) Math.max(
+                        appCompatTaskInfo.topActivityLetterboxWidth,
+                        appCompatTaskInfo.topActivityLetterboxHeight)
+                        / Math.min(appCompatTaskInfo.topActivityLetterboxWidth,
+                        appCompatTaskInfo.topActivityLetterboxHeight);
+            } else {
+                desiredAspectRatio = Math.max(fullscreenHeight, fullscreenWidth)
+                        / Math.min(fullscreenHeight, fullscreenWidth);
+            }
+        } else {
+            final float letterboxAspectRatioOverride =
+                    getFixedOrientationLetterboxAspectRatio(activity, task);
+            if (!task.mDisplayContent.getIgnoreOrientationRequest()) {
+                desiredAspectRatio = DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
+            } else if (letterboxAspectRatioOverride
+                    > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+                desiredAspectRatio = letterboxAspectRatioOverride;
+            }
+        }
+        // If the activity matches display orientation, the display aspect ratio should be used
+        if (activityMatchesDisplayOrientation(
+                taskInfo.configuration.orientation,
+                activity.getOverrideOrientation())) {
+            desiredAspectRatio = Math.max(fullscreenWidth, fullscreenHeight)
+                    / Math.min(fullscreenWidth, fullscreenHeight);
+        }
+        if (maxAspectRatio >= 1 && desiredAspectRatio > maxAspectRatio) {
+            desiredAspectRatio = maxAspectRatio;
+        } else if (minAspectRatio >= 1 && desiredAspectRatio < minAspectRatio) {
+            desiredAspectRatio = minAspectRatio;
+        }
+        return desiredAspectRatio;
+    }
+
+    private static boolean activityMatchesDisplayOrientation(
+            @Configuration.Orientation int deviceOrientation,
+            @ActivityInfo.ScreenOrientation int activityOrientation) {
+        if (deviceOrientation == ORIENTATION_PORTRAIT) {
+            return isFixedOrientationPortrait(activityOrientation);
+        }
+        return isFixedOrientationLandscape(activityOrientation);
+    }
+
+    /**
+     * Calculates the desired initial bounds for applications in desktop windowing. This is done as
+     * a scale of the screen bounds.
+     */
+    private static @NonNull Size calculateIdealSize(@NonNull Rect screenBounds, float scale) {
+        final int width = (int) (screenBounds.width() * scale);
+        final int height = (int) (screenBounds.height() * scale);
+        return new Size(width, height);
+    }
+
+    /**
+     * Adjusts bounds to be positioned in the middle of the screen.
+     */
+    private static @NonNull Rect centerInScreen(@NonNull Size desiredSize,
             @NonNull Rect screenBounds) {
-        // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
-        // The desired dimensions that a fully resizable window should take when initially entering
-        // desktop mode. Calculated as a percentage of the available display area as defined by the
-        // DESKTOP_MODE_INITIAL_BOUNDS_SCALE.
-        final int desiredWidth = (int) (screenBounds.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        final int desiredHeight = (int) (screenBounds.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
-        outBounds.right = desiredWidth;
-        outBounds.bottom = desiredHeight;
-        outBounds.offset(screenBounds.centerX() - outBounds.centerX(),
-                screenBounds.centerY() - outBounds.centerY());
+        // TODO(b/325240051): Position apps with bottom heavy offset
+        final int heightOffset = (screenBounds.height() - desiredSize.getHeight()) / 2;
+        final int widthOffset = (screenBounds.width() - desiredSize.getWidth()) / 2;
+        final Rect resultBounds = new Rect(0, 0,
+                desiredSize.getWidth(), desiredSize.getHeight());
+        resultBounds.offset(screenBounds.left + widthOffset, screenBounds.top + heightOffset);
+        return resultBounds;
+    }
+
+    private static float getFixedOrientationLetterboxAspectRatio(@NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        return activity.shouldCreateCompatDisplayInsets()
+                ? getDefaultMinAspectRatioForUnresizableApps(activity, task)
+                : activity.mAppCompatController.getAppCompatAspectRatioOverrides()
+                        .getDefaultMinAspectRatio();
+    }
+
+    private static float getDefaultMinAspectRatioForUnresizableApps(
+            @NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        final AppCompatAspectRatioOverrides appCompatAspectRatioOverrides =
+                activity.mAppCompatController.getAppCompatAspectRatioOverrides();
+        if (appCompatAspectRatioOverrides.isSplitScreenAspectRatioForUnresizableAppsEnabled()) {
+            // Default letterbox aspect ratio for unresizable apps.
+            return getSplitScreenAspectRatio(activity, task);
+        }
+
+        if (appCompatAspectRatioOverrides.getDefaultMinAspectRatioForUnresizableAppsFromConfig()
+                > MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO) {
+            return appCompatAspectRatioOverrides
+                    .getDefaultMinAspectRatioForUnresizableAppsFromConfig();
+        }
+
+        return appCompatAspectRatioOverrides.getDefaultMinAspectRatio();
+    }
+
+    /**
+     * Calculates the aspect ratio of the available display area when an app enters split-screen on
+     * a given device, taking into account any dividers and insets.
+     */
+    private static float getSplitScreenAspectRatio(@NonNull ActivityRecord activity,
+            @NonNull Task task) {
+        final int dividerWindowWidth =
+                activity.mWmService.mContext.getResources().getDimensionPixelSize(
+                        R.dimen.docked_stack_divider_thickness);
+        final int dividerInsets =
+                activity.mWmService.mContext.getResources().getDimensionPixelSize(
+                        R.dimen.docked_stack_divider_insets);
+        final int dividerSize = dividerWindowWidth - dividerInsets * 2;
+        final Rect bounds = new Rect(0, 0,
+                task.mDisplayContent.getDisplayInfo().appWidth,
+                task.mDisplayContent.getDisplayInfo().appHeight);
+        if (bounds.width() >= bounds.height()) {
+            bounds.inset(/* dx */ dividerSize / 2, /* dy */ 0);
+            bounds.right = bounds.centerX();
+        } else {
+            bounds.inset(/* dx */ 0, /* dy */ dividerSize / 2);
+            bounds.bottom = bounds.centerY();
+        }
+        return computeAspectRatio(bounds);
     }
 }
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index aacd3c6..548addb 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -25,7 +25,6 @@
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
-import android.os.SystemProperties;
 import android.util.Slog;
 
 import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
@@ -38,19 +37,9 @@
             TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM;
     private static final boolean DEBUG = false;
 
-    public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
-            SystemProperties
-                    .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f;
-
-    /**
-     * Flag to indicate whether to restrict desktop mode to supported devices.
-     */
-    private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
-            "persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
-
     private StringBuilder mLogBuilder;
 
-    private final Context mContext;
+    @NonNull private final Context mContext;
 
     DesktopModeLaunchParamsModifier(@NonNull Context context) {
         mContext = context;
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
index 72883e2..5bd919f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
@@ -23,6 +23,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.testng.AssertJUnit.assertFalse;
 
 import android.os.Environment;
 import android.os.FileUtils;
@@ -51,6 +52,7 @@
     private static final int USER_ID_1 = 10;
     private static final int USER_ID_2 = 11;
     private static final int USER_ID_3 = 12;
+    private static final int USER_ID_SYSTEM = 0;
     private static final long TEST_TIMESTAMP = 150_000;
     private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry
             .getInstrumentation().getContext().getDataDir(), "alarmsTestDir");
@@ -110,6 +112,14 @@
     }
 
     @Test
+    public void testAddWakeupForSystemUser_shouldDoNothing() {
+        mUserWakeupStore.addUserWakeup(USER_ID_SYSTEM, TEST_TIMESTAMP - 19_000);
+        assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+        final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml");
+        assertFalse(file.exists());
+    }
+
+    @Test
     public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() {
         final long finalAlarmTime = TEST_TIMESTAMP - 13_000;
         mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000);
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index 23a88a1..b687042 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -21,9 +21,19 @@
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
+import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.util.DisplayMetrics.DENSITY_DEFAULT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.server.wm.DesktopModeLaunchParamsModifier.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
+import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_INITIAL_BOUNDS_SCALE;
+import static com.android.server.wm.DesktopModeBoundsCalculator.DESKTOP_MODE_LANDSCAPE_APP_PADDING;
+import static com.android.server.wm.DesktopModeBoundsCalculator.calculateAspectRatio;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
@@ -59,6 +69,10 @@
 @RunWith(WindowTestRunner.class)
 public class DesktopModeLaunchParamsModifierTests extends
         LaunchParamsModifierTestsBase<DesktopModeLaunchParamsModifier> {
+    private static final Rect LANDSCAPE_DISPLAY_BOUNDS = new Rect(0, 0, 2560, 1600);
+    private static final Rect PORTRAIT_DISPLAY_BOUNDS = new Rect(0, 0, 1600, 2560);
+    private static final float LETTERBOX_ASPECT_RATIO = 1.3f;
+
     @Before
     public void setUp() throws Exception {
         mActivity = new ActivityBuilder(mAtm).build();
@@ -158,6 +172,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+    @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
     public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -169,6 +184,209 @@
                 (int) (DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
         final int desiredHeight =
                 (int) (DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_resizable_undefinedOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_resizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testResizablePortraitBounds_landscapeDevice_resizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true);
+
+        final int desiredWidth =
+                (int) ((LANDSCAPE_DISPLAY_BOUNDS.height() / LETTERBOX_ASPECT_RATIO) + 0.5f);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultLandscapeBounds_landscapeDevice_unResizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false);
+
+        final int desiredWidth =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testUnResizablePortraitBounds_landscapeDevice_unResizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_LANDSCAPE,
+                LANDSCAPE_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false);
+
+        final int desiredHeight =
+                (int) (LANDSCAPE_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredWidth = (int) (desiredHeight / LETTERBOX_ASPECT_RATIO);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_resizable_undefinedOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_UNSPECIFIED, true);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_resizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, true);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testResizableLandscapeBounds_portraitDevice_resizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, true);
+
+        final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
+                - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+        final int desiredHeight = (int)
+                ((PORTRAIT_DISPLAY_BOUNDS.width() / LETTERBOX_ASPECT_RATIO) + 0.5f);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+            Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS})
+    public void testDefaultPortraitBounds_portraitDevice_unResizable_portraitOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_PORTRAIT, false);
+
+        final int desiredWidth =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+        final int desiredHeight =
+                (int) (PORTRAIT_DISPLAY_BOUNDS.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE);
+
+        assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
+        assertEquals(desiredWidth, mResult.mBounds.width());
+        assertEquals(desiredHeight, mResult.mBounds.height());
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+    public void testUnResizableLandscapeBounds_portraitDevice_unResizable_landscapeOrientation() {
+        setupDesktopModeLaunchParamsModifier();
+        doReturn(LETTERBOX_ASPECT_RATIO).when(()
+                -> calculateAspectRatio(any(), any()));
+
+        final TestDisplayContent display = createDisplayContent(ORIENTATION_PORTRAIT,
+                PORTRAIT_DISPLAY_BOUNDS);
+        final Task task = createTask(display, SCREEN_ORIENTATION_LANDSCAPE, false);
+
+        final int desiredWidth = PORTRAIT_DISPLAY_BOUNDS.width()
+                - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2);
+        final int desiredHeight = (int) (desiredWidth / LETTERBOX_ASPECT_RATIO);
+
         assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(task).calculate());
         assertEquals(desiredWidth, mResult.mBounds.width());
         assertEquals(desiredHeight, mResult.mBounds.height());
@@ -192,6 +410,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_CenterToDisplay() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -207,6 +426,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_LeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -222,6 +442,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_TopGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -237,6 +458,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_TopLeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -252,6 +474,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_RightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -267,6 +490,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_BottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -282,6 +506,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBounds_RightBottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -297,6 +522,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutFractionBounds() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -312,6 +538,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_LeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -327,6 +554,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -342,6 +570,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopLeftGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -359,6 +588,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_RightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -374,6 +604,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -389,6 +620,7 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
     public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomRightGravity() {
         setupDesktopModeLaunchParamsModifier();
 
@@ -422,6 +654,38 @@
         assertEquals(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode);
     }
 
+    private Task createTask(DisplayContent display, int orientation, Boolean isResizeable) {
+        final int resizeMode = isResizeable ? RESIZE_MODE_RESIZEABLE
+                : RESIZE_MODE_UNRESIZEABLE;
+        final Task task = new TaskBuilder(mSupervisor).setActivityType(
+                ACTIVITY_TYPE_STANDARD).setDisplay(display).build();
+        task.setResizeMode(resizeMode);
+        mActivity = new ActivityBuilder(task.mAtmService)
+                .setTask(task)
+                .setScreenOrientation(orientation)
+                .setOnTop(true).build();
+
+        mActivity.onDisplayChanged(display);
+        mActivity.setOccludesParent(true);
+        mActivity.setVisible(true);
+        mActivity.setVisibleRequested(true);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(/* ignoreOrientationRequest */ true);
+
+        return task;
+    }
+
+    private TestDisplayContent createDisplayContent(int orientation, Rect displayBounds) {
+        final TestDisplayContent display = new TestDisplayContent
+                .Builder(mAtm, displayBounds.width(), displayBounds.height())
+                .setPosition(DisplayContent.POSITION_TOP).build();
+        display.setBounds(displayBounds);
+        display.getConfiguration().densityDpi = DENSITY_DEFAULT;
+        display.getConfiguration().orientation = ORIENTATION_LANDSCAPE;
+        display.getDefaultTaskDisplayArea().setWindowingMode(orientation);
+
+        return display;
+    }
+
     private void setupDesktopModeLaunchParamsModifier() {
         setupDesktopModeLaunchParamsModifier(/*isDesktopModeSupported=*/ true,
                 /*enforceDeviceRestrictions=*/ true);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index a816aa9..d5d2847 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -203,6 +203,7 @@
                 .mockStatic(LockGuard.class, mockStubOnly)
                 .mockStatic(Watchdog.class, mockStubOnly)
                 .spyStatic(DesktopModeHelper.class)
+                .spyStatic(DesktopModeBoundsCalculator.class)
                 .strictness(Strictness.LENIENT)
                 .startMocking();