diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 9ab9e7a76a25e..8f3c3d6e1dd50 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -47,6 +47,7 @@ import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.SystemUser; import com.android.systemui.demomode.dagger.DemoModeModule; +import com.android.systemui.display.DisplayModule; import com.android.systemui.doze.dagger.DozeComponent; import com.android.systemui.dreams.dagger.DreamModule; import com.android.systemui.dump.DumpManager; @@ -163,6 +164,7 @@ import javax.inject.Named; ClipboardOverlayModule.class, ClockRegistryModule.class, CommonRepositoryModule.class, + DisplayModule.class, ConnectivityModule.class, CoroutinesModule.class, DreamModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt new file mode 100644 index 0000000000000..65cd84bc4da1f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.display + +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.data.repository.DisplayRepositoryImpl +import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor +import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractorImpl +import dagger.Binds +import dagger.Module + +/** Module binding display related classes. */ +@Module +interface DisplayModule { + @Binds + fun bindConnectedDisplayInteractor( + provider: ConnectedDisplayInteractorImpl + ): ConnectedDisplayInteractor + + @Binds fun bindsDisplayRepository(displayRepository: DisplayRepositoryImpl): DisplayRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt new file mode 100644 index 0000000000000..4b957c7f435cd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractor.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.display.domain.interactor + +import android.view.Display +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** Provides information about an external connected display. */ +interface ConnectedDisplayInteractor { + /** + * Provides the current external display state. + * + * The state is: + * - [State.CONNECTED] when there is at least one display with [TYPE_EXTERNAL]. + * - [State.CONNECTED_SECURE] when is at least one display with both [TYPE_EXTERNAL] AND + * [Display.FLAG_SECURE] set + */ + val connectedDisplayState: Flow + + /** Possible connected display state. */ + enum class State { + DISCONNECTED, + CONNECTED, + CONNECTED_SECURE, + } +} + +@SysUISingleton +class ConnectedDisplayInteractorImpl +@Inject +constructor( + displayRepository: DisplayRepository, +) : ConnectedDisplayInteractor { + + override val connectedDisplayState: Flow = + displayRepository.displays + .map { displays -> + val externalDisplays = + displays.filter { display -> display.type == Display.TYPE_EXTERNAL } + + val secureExternalDisplays = + externalDisplays.filter { it.flags and Display.FLAG_SECURE != 0 } + + if (externalDisplays.isEmpty()) { + State.DISCONNECTED + } else if (!secureExternalDisplays.isEmpty()) { + State.CONNECTED_SECURE + } else { + State.CONNECTED + } + } + .distinctUntilChanged() +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt new file mode 100644 index 0000000000000..1b597f44d23e7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/display/domain/interactor/ConnectedDisplayInteractorTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.display.domain.interactor + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.Display +import android.view.Display.TYPE_EXTERNAL +import android.view.Display.TYPE_INTERNAL +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.FlowValue +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor.State +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class ConnectedDisplayInteractorTest : SysuiTestCase() { + + private val fakeDisplayRepository = FakeDisplayRepository() + private val connectedDisplayStateProvider: ConnectedDisplayInteractor = + ConnectedDisplayInteractorImpl(fakeDisplayRepository) + private val testScope = TestScope(UnconfinedTestDispatcher()) + + @Test + fun displayState_nullDisplays_disconnected() = + testScope.runTest { + val value by lastValue() + + fakeDisplayRepository.emit(emptySet()) + + assertThat(value).isEqualTo(State.DISCONNECTED) + } + + @Test + fun displayState_emptyDisplays_disconnected() = + testScope.runTest { + val value by lastValue() + + fakeDisplayRepository.emit(emptySet()) + + assertThat(value).isEqualTo(State.DISCONNECTED) + } + + @Test + fun displayState_internalDisplay_disconnected() = + testScope.runTest { + val value by lastValue() + + fakeDisplayRepository.emit(setOf(display(type = TYPE_INTERNAL))) + + assertThat(value).isEqualTo(State.DISCONNECTED) + } + + @Test + fun displayState_externalDisplay_connected() = + testScope.runTest { + val value by lastValue() + + fakeDisplayRepository.emit(setOf(display(type = TYPE_EXTERNAL))) + + assertThat(value).isEqualTo(State.CONNECTED) + } + + @Test + fun displayState_multipleExternalDisplays_connected() = + testScope.runTest { + val value by lastValue() + + fakeDisplayRepository.emit( + setOf(display(type = TYPE_EXTERNAL), display(type = TYPE_EXTERNAL)) + ) + + assertThat(value).isEqualTo(State.CONNECTED) + } + + @Test + fun displayState_externalSecure_connectedSecure() = + testScope.runTest { + val value by lastValue() + + fakeDisplayRepository.emit( + setOf(display(type = TYPE_EXTERNAL, flags = Display.FLAG_SECURE)) + ) + + assertThat(value).isEqualTo(State.CONNECTED_SECURE) + } + + @Test + fun displayState_multipleExternal_onlyOneSecure_connectedSecure() = + testScope.runTest { + val value by lastValue() + + fakeDisplayRepository.emit( + setOf( + display(type = TYPE_EXTERNAL, flags = Display.FLAG_SECURE), + display(type = TYPE_EXTERNAL, flags = 0) + ) + ) + + assertThat(value).isEqualTo(State.CONNECTED_SECURE) + } + + private fun TestScope.lastValue(): FlowValue = + collectLastValue(connectedDisplayStateProvider.connectedDisplayState) + + private fun display(type: Int, flags: Int = 0): Display { + return mock().also { mockDisplay -> + whenever(mockDisplay.type).thenReturn(type) + whenever(mockDisplay.flags).thenReturn(flags) + } + } + + private class FakeDisplayRepository : DisplayRepository { + private val flow = MutableSharedFlow>() + suspend fun emit(value: Set) = flow.emit(value) + override val displays: Flow> + get() = flow + } +}