diff options
| author | 2024-11-08 21:01:38 +0000 | |
|---|---|---|
| committer | 2024-11-15 21:42:20 +0000 | |
| commit | 31d2f51ce4c8ad04a96d66c1b9152a85ba7f263d (patch) | |
| tree | 5e32ac368edf43156d006e6afedfb626124580d7 | |
| parent | 30ac01b752230191a003d7b010c9c9e056cb04a7 (diff) | |
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
15 files changed, 747 insertions, 10 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index bffda8bcae65..d2f3ff1c969e 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -211,7 +211,9 @@ filegroup { "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 000000000000..789178728f18 --- /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 000000000000..a8d4d2ece07f --- /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 53ab686ff0d7..5871b2bab3ff 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 589dbf92de38..e862525623fe 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.FocusedDisplayRepository 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 @@ interface DisplayModule { 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 1da5351ac2a3..29044d017d2d 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.content.Context 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 @@ interface DeviceStateRepository { 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 @@ constructor( 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 @@ constructor( */ 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 @@ constructor( 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 000000000000..b743377881bf --- /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 000000000000..bc15bbb5e57d --- /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 000000000000..2d6181aa04af --- /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 6ab294dd9818..5fb9cb27f90f 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 @@ interface RearDisplayModule { 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 000000000000..c8faa81adffa --- /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 000000000000..60588802ffa9 --- /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 796ec9400249..45dcb28b23ee 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 9c55820b797c..b8a095eae23e 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 @@ package com.android.systemui 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 @@ val Kosmos.foldedDeviceStateList by .setSystemProperties( setOf( PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY, - PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP + PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP, ) ) .setPhysicalProperties( @@ -57,7 +58,7 @@ val Kosmos.foldedDeviceStateList by .setSystemProperties( setOf( PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_OUTER_PRIMARY, - PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP + PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP, ) ) .setPhysicalProperties( @@ -70,14 +71,14 @@ val Kosmos.foldedDeviceStateList by .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 @@ val Kosmos.halfFoldedDeviceState by .setSystemProperties( setOf( PROPERTY_FOLDABLE_DISPLAY_CONFIGURATION_INNER_PRIMARY, - PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE + PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE, ) ) .setPhysicalProperties( @@ -105,7 +106,7 @@ val Kosmos.unfoldedDeviceState by .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 @@ val Kosmos.rearDisplayDeviceState by .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 000000000000..6f5985536fe7 --- /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>() } |