Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3214adb6 authored by Nicolo' Mazzucato's avatar Nicolo' Mazzucato
Browse files

Introduce ConnectedDisplayInteractor

This class provides the status of external connected devices though a
Flow.

Used in child cls to add a status bar icon when an external display is
connected.

Bug: 286186256
Test: ConnectedDisplayInteractorTest
Change-Id: Ib31a9bcbd2062488eed4b4933b814929704a4ff2
parent f328c537
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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,
+35 −0
Original line number Diff line number Diff line
/*
 * 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
}
+73 −0
Original line number Diff line number Diff line
/*
 * 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<State>

    /** Possible connected display state. */
    enum class State {
        DISCONNECTED,
        CONNECTED,
        CONNECTED_SECURE,
    }
}

@SysUISingleton
class ConnectedDisplayInteractorImpl
@Inject
constructor(
    displayRepository: DisplayRepository,
) : ConnectedDisplayInteractor {

    override val connectedDisplayState: Flow<State> =
        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()
}
+148 −0
Original line number Diff line number Diff line
/*
 * 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<State?> =
        collectLastValue(connectedDisplayStateProvider.connectedDisplayState)

    private fun display(type: Int, flags: Int = 0): Display {
        return mock<Display>().also { mockDisplay ->
            whenever(mockDisplay.type).thenReturn(type)
            whenever(mockDisplay.flags).thenReturn(flags)
        }
    }

    private class FakeDisplayRepository : DisplayRepository {
        private val flow = MutableSharedFlow<Set<Display>>()
        suspend fun emit(value: Set<Display>) = flow.emit(value)
        override val displays: Flow<Set<Display>>
            get() = flow
    }
}