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>() }