Loading packages/SystemUI/Android.bp +2 −0 Original line number Diff line number Diff line Loading @@ -208,7 +208,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", Loading packages/SystemUI/multivalentTests/src/com/android/systemui/display/domain/interactor/RearDisplayStateInteractorTest.kt 0 → 100644 +181 −0 Original line number Diff line number Diff line /* * 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() } } } packages/SystemUI/res/layout/activity_rear_display_front_screen_on.xml 0 → 100644 +78 −0 Original line number Diff line number Diff line <?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> packages/SystemUI/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -3568,6 +3568,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> Loading packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -46,6 +48,11 @@ interface DisplayModule { provider: ConnectedDisplayInteractorImpl ): ConnectedDisplayInteractor @Binds fun bindRearDisplayStateInteractor( provider: RearDisplayStateInteractorImpl ): RearDisplayStateInteractor @Binds fun bindsDisplayRepository(displayRepository: DisplayRepositoryImpl): DisplayRepository @Binds Loading Loading
packages/SystemUI/Android.bp +2 −0 Original line number Diff line number Diff line Loading @@ -208,7 +208,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", Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/display/domain/interactor/RearDisplayStateInteractorTest.kt 0 → 100644 +181 −0 Original line number Diff line number Diff line /* * 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() } } }
packages/SystemUI/res/layout/activity_rear_display_front_screen_on.xml 0 → 100644 +78 −0 Original line number Diff line number Diff line <?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>
packages/SystemUI/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -3568,6 +3568,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> Loading
packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -46,6 +48,11 @@ interface DisplayModule { provider: ConnectedDisplayInteractorImpl ): ConnectedDisplayInteractor @Binds fun bindRearDisplayStateInteractor( provider: RearDisplayStateInteractorImpl ): RearDisplayStateInteractor @Binds fun bindsDisplayRepository(displayRepository: DisplayRepositoryImpl): DisplayRepository @Binds Loading