Update Rear Display Mode UX

Updates the Rear Display Mode UX such that after switching to the
Rear Display Mode, the inner display shows a dialog letting the user
know that the content has moved to the other display.

Bug: 371095273

Flag: android.hardware.devicestate.feature.flags.device_state_rdm_v2

Test: demo app
Test: atest com.android.systemui.reardisplay
Test: atest com.android.systemui.display.domain.interactor

Change-Id: I499f268f8a4cf1c290501951bdc387c133a63f22
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index bffda8b..d2f3ff1 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -211,7 +211,9 @@
         "tests/src/**/systemui/qs/tiles/DreamTileTest.java",
         "tests/src/**/systemui/qs/FgsManagerControllerTest.java",
         "tests/src/**/systemui/qs/QSPanelTest.kt",
+        "tests/src/**/systemui/reardisplay/RearDisplayCoreStartableTest.kt",
         "tests/src/**/systemui/reardisplay/RearDisplayDialogControllerTest.java",
+        "tests/src/**/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt",
         "tests/src/**/systemui/statusbar/KeyboardShortcutListSearchTest.java",
         "tests/src/**/systemui/statusbar/KeyboardShortcutsTest.java",
         "tests/src/**/systemui/statusbar/KeyguardIndicationControllerWithCoroutinesTest.kt",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/display/domain/interactor/RearDisplayStateInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/domain/interactor/RearDisplayStateInteractorTest.kt
new file mode 100644
index 0000000..7891787
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/domain/interactor/RearDisplayStateInteractorTest.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.display.domain.interactor
+
+import android.hardware.display.defaultDisplay
+import android.hardware.display.rearDisplay
+import android.view.Display
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.display.data.repository.FakeDeviceStateRepository
+import com.android.systemui.display.data.repository.FakeDisplayRepository
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.whenever
+
+/** atest RearDisplayStateInteractorTest */
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class RearDisplayStateInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val fakeDisplayRepository = FakeDisplayRepository()
+    private val fakeDeviceStateRepository = FakeDeviceStateRepository()
+    private val rearDisplayStateInteractor =
+        RearDisplayStateInteractorImpl(
+            fakeDisplayRepository,
+            fakeDeviceStateRepository,
+            kosmos.testDispatcher,
+        )
+    private val emissionTracker = EmissionTracker(rearDisplayStateInteractor, kosmos.testScope)
+
+    @Before
+    fun setup() {
+        whenever(kosmos.rearDisplay.flags).thenReturn(Display.FLAG_REAR)
+    }
+
+    @Test
+    fun enableRearDisplayWhenDisplayImmediatelyAvailable() =
+        kosmos.runTest {
+            emissionTracker.use { tracker ->
+                fakeDisplayRepository.addDisplay(kosmos.rearDisplay)
+                assertThat(tracker.enabledCount).isEqualTo(0)
+                fakeDeviceStateRepository.emit(
+                    DeviceStateRepository.DeviceState.REAR_DISPLAY_OUTER_DEFAULT
+                )
+
+                assertThat(tracker.enabledCount).isEqualTo(1)
+                assertThat(tracker.lastDisplay).isEqualTo(kosmos.rearDisplay)
+            }
+        }
+
+    @Test
+    fun enableAndDisableRearDisplay() =
+        kosmos.runTest {
+            emissionTracker.use { tracker ->
+                // The fake FakeDeviceStateRepository will always start with state UNKNOWN, thus
+                // triggering one initial emission
+                assertThat(tracker.disabledCount).isEqualTo(1)
+
+                fakeDeviceStateRepository.emit(
+                    DeviceStateRepository.DeviceState.REAR_DISPLAY_OUTER_DEFAULT
+                )
+
+                // Adding a non-rear display does not trigger an emission
+                fakeDisplayRepository.addDisplay(kosmos.defaultDisplay)
+                assertThat(tracker.enabledCount).isEqualTo(0)
+
+                // Adding a rear display triggers the emission
+                fakeDisplayRepository.addDisplay(kosmos.rearDisplay)
+                assertThat(tracker.enabledCount).isEqualTo(1)
+                assertThat(tracker.lastDisplay).isEqualTo(kosmos.rearDisplay)
+
+                fakeDeviceStateRepository.emit(DeviceStateRepository.DeviceState.UNFOLDED)
+                assertThat(tracker.disabledCount).isEqualTo(2)
+            }
+        }
+
+    @Test
+    fun enableRearDisplayShouldOnlyReactToFirstRearDisplay() =
+        kosmos.runTest {
+            emissionTracker.use { tracker ->
+                fakeDeviceStateRepository.emit(
+                    DeviceStateRepository.DeviceState.REAR_DISPLAY_OUTER_DEFAULT
+                )
+
+                // Adding a rear display triggers the emission
+                fakeDisplayRepository.addDisplay(kosmos.rearDisplay)
+                assertThat(tracker.enabledCount).isEqualTo(1)
+
+                // Adding additional rear displays does not trigger additional emissions
+                fakeDisplayRepository.addDisplay(kosmos.rearDisplay)
+                assertThat(tracker.enabledCount).isEqualTo(1)
+            }
+        }
+
+    @Test
+    fun rearDisplayAddedWhenNoLongerInRdm() =
+        kosmos.runTest {
+            emissionTracker.use { tracker ->
+                fakeDeviceStateRepository.emit(
+                    DeviceStateRepository.DeviceState.REAR_DISPLAY_OUTER_DEFAULT
+                )
+                fakeDeviceStateRepository.emit(DeviceStateRepository.DeviceState.UNFOLDED)
+
+                // Adding a rear display when no longer in the correct device state does not trigger
+                // an emission
+                fakeDisplayRepository.addDisplay(kosmos.rearDisplay)
+                assertThat(tracker.enabledCount).isEqualTo(0)
+            }
+        }
+
+    @Test
+    fun rearDisplayDisabledDoesNotSpam() =
+        kosmos.runTest {
+            emissionTracker.use { tracker ->
+                fakeDeviceStateRepository.emit(DeviceStateRepository.DeviceState.UNFOLDED)
+                assertThat(tracker.disabledCount).isEqualTo(1)
+
+                // No additional emission
+                fakeDeviceStateRepository.emit(DeviceStateRepository.DeviceState.FOLDED)
+                assertThat(tracker.disabledCount).isEqualTo(1)
+            }
+        }
+
+    class EmissionTracker(rearDisplayInteractor: RearDisplayStateInteractor, scope: TestScope) :
+        AutoCloseable {
+        var enabledCount = 0
+        var disabledCount = 0
+        var lastDisplay: Display? = null
+
+        val job: Job
+
+        init {
+            val channel = Channel<RearDisplayStateInteractor.State>(Channel.UNLIMITED)
+            job =
+                scope.launch {
+                    rearDisplayInteractor.state.collect {
+                        channel.send(it)
+                        if (it is RearDisplayStateInteractor.State.Enabled) {
+                            enabledCount++
+                            lastDisplay = it.innerDisplay
+                        }
+                        if (it is RearDisplayStateInteractor.State.Disabled) {
+                            disabledCount++
+                        }
+                    }
+                }
+        }
+
+        override fun close() {
+            job.cancel()
+        }
+    }
+}
diff --git a/packages/SystemUI/res/layout/activity_rear_display_front_screen_on.xml b/packages/SystemUI/res/layout/activity_rear_display_front_screen_on.xml
new file mode 100644
index 0000000..a8d4d2e
--- /dev/null
+++ b/packages/SystemUI/res/layout/activity_rear_display_front_screen_on.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:gravity="center"
+    android:paddingStart="@dimen/dialog_side_padding"
+    android:paddingEnd="@dimen/dialog_side_padding"
+    android:paddingTop="@dimen/dialog_top_padding"
+    android:paddingBottom="@dimen/dialog_bottom_padding">
+
+    <androidx.cardview.widget.CardView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:cardElevation="0dp"
+        app:cardCornerRadius="28dp"
+        app:cardBackgroundColor="@color/rear_display_overlay_animation_background_color">
+
+        <com.android.systemui.reardisplay.RearDisplayEducationLottieViewWrapper
+            android:id="@+id/rear_display_folded_animation"
+            android:importantForAccessibility="no"
+            android:layout_width="@dimen/rear_display_animation_width_opened"
+            android:layout_height="@dimen/rear_display_animation_height_opened"
+            android:layout_gravity="center"
+            android:contentDescription="@string/rear_display_accessibility_unfolded_animation"
+            android:scaleType="fitXY"
+            app:lottie_rawRes="@raw/rear_display_turnaround"
+            app:lottie_autoPlay="true"
+            app:lottie_repeatMode="reverse"/>
+    </androidx.cardview.widget.CardView>
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/rear_display_unfolded_front_screen_on"
+        android:textAppearance="@style/TextAppearance.Dialog.Title"
+        android:lineSpacingExtra="2sp"
+        android:translationY="-1.24sp"
+        android:gravity="center_horizontal" />
+
+    <!-- Buttons -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:layout_marginTop="36dp">
+        <Space
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"/>
+        <TextView
+            android:id="@+id/button_cancel"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0"
+            android:layout_gravity="start"
+            android:text="@string/cancel"
+            style="@style/Widget.Dialog.Button.BorderButton" />
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 53ab686..5871b2b 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3564,6 +3564,8 @@
     <string name="rear_display_accessibility_folded_animation">Foldable device being unfolded</string>
     <!-- Text for education page content description for unfolded animation. [CHAR_LIMIT=NONE] -->
     <string name="rear_display_accessibility_unfolded_animation">Foldable device being flipped around</string>
+    <!-- Text for a dialog telling the user that the front screen is turned on. [CHAR_LIMIT=NONE] -->
+    <string name="rear_display_unfolded_front_screen_on">Front screen turned on</string>
 
     <!-- QuickSettings: Additional label for the auto-rotation quicksettings tile indicating that the setting corresponds to the folded posture for a foldable device [CHAR LIMIT=32] -->
     <string name="quick_settings_rotation_posture_folded">folded</string>
diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
index 589dbf9..e862525 100644
--- a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
@@ -30,6 +30,8 @@
 import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl
 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl
+import com.android.systemui.display.domain.interactor.RearDisplayStateInteractor
+import com.android.systemui.display.domain.interactor.RearDisplayStateInteractorImpl
 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
 import dagger.Binds
 import dagger.Lazy
@@ -46,6 +48,11 @@
         provider: ConnectedDisplayInteractorImpl
     ): ConnectedDisplayInteractor
 
+    @Binds
+    fun bindRearDisplayStateInteractor(
+        provider: RearDisplayStateInteractorImpl
+    ): RearDisplayStateInteractor
+
     @Binds fun bindsDisplayRepository(displayRepository: DisplayRepositoryImpl): DisplayRepository
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt
index 1da5351..29044d0 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DeviceStateRepository.kt
@@ -20,6 +20,7 @@
 import android.hardware.devicestate.DeviceState as PlatformDeviceState
 import android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT
 import android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY
+import android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT
 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY
 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY
 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN
@@ -49,6 +50,15 @@
         UNFOLDED,
         /** Device state that corresponds to the device being in rear display mode */
         REAR_DISPLAY,
+        /**
+         * Device state that corresponds to the device being in rear display mode with the inner
+         * display showing a system-provided affordance to cancel the mode.
+         *
+         * TODO(b/371095273): This state will be removed after the RDM_V2 flag lifecycle is complete
+         *   at which point the REAR_DISPLAY state will be the will be the new and only rear display
+         *   mode.
+         */
+        REAR_DISPLAY_OUTER_DEFAULT,
         /** Device state in that corresponds to the device being in concurrent display mode */
         CONCURRENT_DISPLAY,
         /** Device state in none of the other arrays. */
@@ -62,7 +72,7 @@
     val context: Context,
     val deviceStateManager: DeviceStateManager,
     @Background bgScope: CoroutineScope,
-    @Background executor: Executor
+    @Background executor: Executor,
 ) : DeviceStateRepository {
 
     override val state: StateFlow<DeviceState> =
@@ -105,6 +115,12 @@
      */
     private fun PlatformDeviceState.toDeviceStateEnum(): DeviceState {
         return when {
+            hasProperties(
+                PROPERTY_FEATURE_REAR_DISPLAY,
+                PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT,
+            ) -> {
+                DeviceState.REAR_DISPLAY_OUTER_DEFAULT
+            }
             hasProperty(PROPERTY_FEATURE_REAR_DISPLAY) -> DeviceState.REAR_DISPLAY
             hasProperty(PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT) -> {
                 DeviceState.CONCURRENT_DISPLAY
@@ -112,7 +128,7 @@
             hasProperty(PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY) -> DeviceState.FOLDED
             hasProperties(
                 PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY,
-                PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN
+                PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN,
             ) -> DeviceState.HALF_FOLDED
             hasProperty(PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY) -> {
                 DeviceState.UNFOLDED
diff --git a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/RearDisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/RearDisplayStateInteractor.kt
new file mode 100644
index 0000000..b743377
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/RearDisplayStateInteractor.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.display.domain.interactor
+
+import android.view.Display
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DeviceStateRepository
+import com.android.systemui.display.data.repository.DisplayRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+
+/** Provides information about the status of Rear Display Mode. */
+interface RearDisplayStateInteractor {
+
+    /** A flow notifying the subscriber of Rear Display state changes */
+    val state: Flow<State>
+
+    sealed class State {
+        /** Indicates that the rear display is disabled */
+        data object Disabled : State()
+
+        /**
+         * Indicates that the device is in Rear Display Mode, and that the inner display is ready to
+         * show a system-provided affordance allowing the user to cancel out of the Rear Display
+         * Mode.
+         */
+        data class Enabled(val innerDisplay: Display) : State()
+    }
+}
+
+@SysUISingleton
+class RearDisplayStateInteractorImpl
+@Inject
+constructor(
+    displayRepository: DisplayRepository,
+    deviceStateRepository: DeviceStateRepository,
+    @Background backgroundCoroutineDispatcher: CoroutineDispatcher,
+) : RearDisplayStateInteractor {
+
+    override val state: Flow<RearDisplayStateInteractor.State> =
+        deviceStateRepository.state
+            .combineTransform(displayRepository.displays) { state, displays ->
+                val innerDisplay = displays.find { it.flags and Display.FLAG_REAR != 0 }
+
+                if (state != DeviceStateRepository.DeviceState.REAR_DISPLAY_OUTER_DEFAULT) {
+                    emit(RearDisplayStateInteractor.State.Disabled)
+                } else if (innerDisplay != null) {
+                    emit(RearDisplayStateInteractor.State.Enabled(innerDisplay))
+                }
+            }
+            .distinctUntilChanged()
+            .flowOn(backgroundCoroutineDispatcher)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
new file mode 100644
index 0000000..bc15bbb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayCoreStartable.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.reardisplay
+
+import android.content.Context
+import android.hardware.devicestate.DeviceStateManager
+import android.hardware.devicestate.feature.flags.Flags
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.display.domain.interactor.RearDisplayStateInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+
+/**
+ * Provides a {@link com.android.systemui.statusbar.phone.SystemUIDialog} to be shown on the inner
+ * display when the device enters Rear Display Mode, containing an UI affordance to let the user
+ * know that the main content has moved to the outer display, as well as an UI affordance to cancel
+ * the Rear Display Mode.
+ */
+@SysUISingleton
+class RearDisplayCoreStartable
+@Inject
+internal constructor(
+    private val context: Context,
+    private val deviceStateManager: DeviceStateManager,
+    private val rearDisplayStateInteractor: RearDisplayStateInteractor,
+    private val rearDisplayInnerDialogDelegateFactory: RearDisplayInnerDialogDelegate.Factory,
+    @Application private val scope: CoroutineScope,
+) : CoreStartable, AutoCloseable {
+
+    companion object {
+        private const val TAG: String = "RearDisplayCoreStartable"
+    }
+
+    @VisibleForTesting var stateChangeListener: Job? = null
+
+    override fun close() {
+        stateChangeListener?.cancel()
+    }
+
+    override fun start() {
+        if (Flags.deviceStateRdmV2()) {
+            var dialog: SystemUIDialog? = null
+
+            stateChangeListener =
+                rearDisplayStateInteractor.state
+                    .map {
+                        when (it) {
+                            is RearDisplayStateInteractor.State.Enabled -> {
+                                val rearDisplayContext =
+                                    context.createDisplayContext(it.innerDisplay)
+                                val delegate =
+                                    rearDisplayInnerDialogDelegateFactory.create(
+                                        rearDisplayContext,
+                                        deviceStateManager::cancelStateRequest,
+                                    )
+                                dialog = delegate.createDialog().apply { show() }
+                            }
+
+                            is RearDisplayStateInteractor.State.Disabled -> {
+                                dialog?.dismiss()
+                                dialog = null
+                            }
+                        }
+                    }
+                    .launchIn(scope)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
new file mode 100644
index 0000000..2d6181a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegate.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.reardisplay
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/**
+ * A {@link com.android.systemui.statusbar.phone.SystemUIDialog.Delegate} providing a dialog which
+ * lets the user know that the Rear Display Mode is active, and that the content has moved to the
+ * outer display.
+ */
+class RearDisplayInnerDialogDelegate
+@AssistedInject
+internal constructor(
+    private val systemUIDialogFactory: SystemUIDialog.Factory,
+    @Assisted private val rearDisplayContext: Context,
+    @Assisted private val onCanceledRunnable: Runnable,
+) : SystemUIDialog.Delegate {
+
+    @AssistedFactory
+    interface Factory {
+        fun create(
+            rearDisplayContext: Context,
+            onCanceledRunnable: Runnable,
+        ): RearDisplayInnerDialogDelegate
+    }
+
+    override fun createDialog(): SystemUIDialog {
+        return systemUIDialogFactory.create(this, rearDisplayContext)
+    }
+
+    override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        dialog.apply {
+            setContentView(R.layout.activity_rear_display_front_screen_on)
+            setCanceledOnTouchOutside(false)
+            requireViewById<View>(R.id.button_cancel).setOnClickListener {
+                onCanceledRunnable.run()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayModule.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayModule.kt
index 6ab294d..5fb9cb2 100644
--- a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayModule.kt
@@ -41,4 +41,10 @@
     fun bindRearDisplayDialogControllerConfigChanges(
         impl: RearDisplayDialogController
     ): ConfigurationListener
+
+    /** Start RearDisplayCoreStartable. */
+    @Binds
+    @IntoMap
+    @ClassKey(RearDisplayCoreStartable::class)
+    abstract fun bindRearDisplayCoreStartable(impl: RearDisplayCoreStartable): CoreStartable
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
new file mode 100644
index 0000000..c8faa81
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.reardisplay
+
+import android.hardware.devicestate.feature.flags.Flags.FLAG_DEVICE_STATE_RDM_V2
+import android.hardware.display.rearDisplay
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.Display
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.deviceStateManager
+import com.android.systemui.display.domain.interactor.RearDisplayStateInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.rearDisplayInnerDialogDelegateFactory
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import org.junit.Before
+import org.junit.Test
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** atest SystemUITests:com.android.systemui.reardisplay.RearDisplayCoreStartableTest */
+@SmallTest
+@kotlinx.coroutines.ExperimentalCoroutinesApi
+class RearDisplayCoreStartableTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+    private val mockDelegate: RearDisplayInnerDialogDelegate = mock()
+    private val mockDialog: SystemUIDialog = mock()
+
+    private val fakeRearDisplayStateInteractor = FakeRearDisplayStateInteractor(kosmos)
+    private val impl =
+        RearDisplayCoreStartable(
+            mContext,
+            kosmos.deviceStateManager,
+            fakeRearDisplayStateInteractor,
+            kosmos.rearDisplayInnerDialogDelegateFactory,
+            kosmos.testScope,
+        )
+
+    @Before
+    fun setup() {
+        whenever(kosmos.rearDisplay.flags).thenReturn(Display.FLAG_REAR)
+        whenever(kosmos.rearDisplay.displayAdjustments)
+            .thenReturn(mContext.display.displayAdjustments)
+        whenever(kosmos.rearDisplayInnerDialogDelegateFactory.create(any(), any()))
+            .thenReturn(mockDelegate)
+        whenever(mockDelegate.createDialog()).thenReturn(mockDialog)
+    }
+
+    @Test
+    @DisableFlags(FLAG_DEVICE_STATE_RDM_V2)
+    fun testWhenFlagDisabled() =
+        kosmos.runTest {
+            impl.use {
+                it.start()
+                assertThat(impl.stateChangeListener).isNull()
+            }
+        }
+
+    @Test
+    @EnableFlags(FLAG_DEVICE_STATE_RDM_V2)
+    fun testShowAndDismissDialog() =
+        kosmos.runTest {
+            impl.use {
+                it.start()
+                fakeRearDisplayStateInteractor.emitRearDisplay()
+                verify(mockDialog).show()
+                verify(mockDialog, never()).dismiss()
+
+                fakeRearDisplayStateInteractor.emitDisabled()
+                verify(mockDialog).dismiss()
+            }
+        }
+
+    private class FakeRearDisplayStateInteractor(private val kosmos: Kosmos) :
+        RearDisplayStateInteractor {
+        private val stateFlow = MutableSharedFlow<RearDisplayStateInteractor.State>()
+
+        suspend fun emitRearDisplay() =
+            stateFlow.emit(RearDisplayStateInteractor.State.Enabled(kosmos.rearDisplay))
+
+        suspend fun emitDisabled() = stateFlow.emit(RearDisplayStateInteractor.State.Disabled)
+
+        override val state: Flow<RearDisplayStateInteractor.State>
+            get() = stateFlow
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
new file mode 100644
index 0000000..6058880
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayInnerDialogDelegateTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.reardisplay
+
+import android.testing.TestableLooper
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.systemUIDialogDotFactory
+import com.android.systemui.testKosmos
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
+
+/** atest SystemUITests:com.android.systemui.reardisplay.RearDisplayInnerDialogDelegateTest */
+@SmallTest
+@TestableLooper.RunWithLooper
+class RearDisplayInnerDialogDelegateTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    @Test
+    fun testShowAndDismissDialog() {
+        val dialogDelegate =
+            RearDisplayInnerDialogDelegate(kosmos.systemUIDialogDotFactory, mContext) {}
+
+        val dialog = dialogDelegate.createDialog()
+        dialog.show()
+        assertTrue(dialog.isShowing)
+
+        dialog.dismiss()
+        assertFalse(dialog.isShowing)
+    }
+
+    @Test
+    fun testCancel() {
+        val mockCallback = mock<Runnable>()
+        RearDisplayInnerDialogDelegate(kosmos.systemUIDialogDotFactory, mContext) {
+                mockCallback.run()
+            }
+            .createDialog()
+            .apply {
+                show()
+                findViewById<View>(R.id.button_cancel).performClick()
+                verify(mockCallback).run()
+            }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt
index 796ec94..45dcb28 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/display/DisplayManagerKosmos.kt
@@ -16,7 +16,12 @@
 
 package android.hardware.display
 
+import android.view.Display
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import org.mockito.kotlin.mock
 
 val Kosmos.displayManager by Kosmos.Fixture { mock<DisplayManager>() }
+
+val Kosmos.defaultDisplay: Display by Kosmos.Fixture { mock<Display>() }
+
+val Kosmos.rearDisplay: Display by Kosmos.Fixture { mock<Display>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/DeviceStateManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/DeviceStateManagerKosmos.kt
index 9c55820..b8a095e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/DeviceStateManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/DeviceStateManagerKosmos.kt
@@ -18,6 +18,7 @@
 
 import android.hardware.devicestate.DeviceState
 import android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY
+import android.hardware.devicestate.DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT
 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY
 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY
 import android.hardware.devicestate.DeviceState.PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED
@@ -44,7 +45,7 @@
                     .setSystemProperties(
                         setOf(
                             PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY,
-                            PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP
+                            PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP,
                         )
                     )
                     .setPhysicalProperties(
@@ -57,7 +58,7 @@
                     .setSystemProperties(
                         setOf(
                             PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY,
-                            PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP
+                            PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP,
                         )
                     )
                     .setPhysicalProperties(
@@ -70,14 +71,14 @@
                     .setSystemProperties(
                         setOf(
                             PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY,
-                            PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP
+                            PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP,
                         )
                     )
                     .setPhysicalProperties(
                         setOf(PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED)
                     )
                     .build()
-            )
+            ),
         )
     }
 
@@ -88,7 +89,7 @@
                 .setSystemProperties(
                     setOf(
                         PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY,
-                        PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE
+                        PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE,
                     )
                 )
                 .setPhysicalProperties(
@@ -105,7 +106,7 @@
                 .setSystemProperties(
                     setOf(
                         PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY,
-                        PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE
+                        PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE,
                     )
                 )
                 .setPhysicalProperties(setOf(PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN))
@@ -120,7 +121,22 @@
                 .setSystemProperties(
                     setOf(
                         PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY,
-                        PROPERTY_FEATURE_REAR_DISPLAY
+                        PROPERTY_FEATURE_REAR_DISPLAY,
+                    )
+                )
+                .build()
+        )
+    }
+
+val Kosmos.rearDisplayOuterDefaultDeviceState by
+    Kosmos.Fixture {
+        DeviceState(
+            DeviceState.Configuration.Builder(5 /* identifier */, "REAR_DISPLAY")
+                .setSystemProperties(
+                    setOf(
+                        PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY,
+                        PROPERTY_FEATURE_REAR_DISPLAY,
+                        PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT,
                     )
                 )
                 .build()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/RearDisplayInnerDialogDelegateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/RearDisplayInnerDialogDelegateKosmos.kt
new file mode 100644
index 0000000..6f59855
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/RearDisplayInnerDialogDelegateKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.reardisplay.RearDisplayInnerDialogDelegate
+import org.mockito.kotlin.mock
+
+val Kosmos.rearDisplayInnerDialogDelegateFactory by
+    Kosmos.Fixture { mock<RearDisplayInnerDialogDelegate.Factory>() }