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

Commit 6556d35c authored by Chris Göllner's avatar Chris Göllner Committed by Android (Google) Code Review
Browse files

Merge "Create DisplayScopeRepository for display specific coroutine scopes" into main

parents 9b348ee2 00088588
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
@@ -16,16 +16,25 @@

package com.android.systemui.display

import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.DeviceStateRepository
import com.android.systemui.display.data.repository.DeviceStateRepositoryImpl
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.data.repository.DisplayRepositoryImpl
import com.android.systemui.display.data.repository.DisplayScopeRepository
import com.android.systemui.display.data.repository.DisplayScopeRepositoryImpl
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.statusbar.core.StatusBarConnectedDisplays
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap

/** Module binding display related classes. */
@Module
@@ -46,4 +55,22 @@ interface DisplayModule {
    fun bindsFocusedDisplayRepository(
        focusedDisplayRepository: FocusedDisplayRepositoryImpl
    ): FocusedDisplayRepository

    @Binds fun displayScopeRepository(impl: DisplayScopeRepositoryImpl): DisplayScopeRepository

    companion object {
        @Provides
        @SysUISingleton
        @IntoMap
        @ClassKey(DisplayScopeRepositoryImpl::class)
        fun displayScopeRepoCoreStartable(
            repoImplLazy: Lazy<DisplayScopeRepositoryImpl>
        ): CoreStartable {
            return if (StatusBarConnectedDisplays.isEnabled) {
                repoImplLazy.get()
            } else {
                CoreStartable.NOP
            }
        }
    }
}
+11 −5
Original line number Diff line number Diff line
@@ -61,8 +61,11 @@ interface DisplayRepository {
    /** Display addition event indicating a new display has been added. */
    val displayAdditionEvent: Flow<Display?>

    /** Display removal event indicating a display has been removed. */
    val displayRemovalEvent: Flow<Int>

    /** Provides the current set of displays. */
    val displays: Flow<Set<Display>>
    val displays: StateFlow<Set<Display>>

    /**
     * Pending display id that can be enabled/disabled.
@@ -79,8 +82,8 @@ interface DisplayRepository {
     *
     * This method is guaranteed to not result in any binder call.
     */
    suspend fun getDisplay(displayId: Int): Display? =
        displays.first().firstOrNull { it.displayId == displayId }
    fun getDisplay(displayId: Int): Display? =
        displays.value.firstOrNull { it.displayId == displayId }

    /** Represents a connected display that has not been enabled yet. */
    interface PendingDisplay {
@@ -148,6 +151,9 @@ constructor(
            getDisplayFromDisplayManager(it.displayId)
        }

    override val displayRemovalEvent: Flow<Int> =
        allDisplayEvents.filterIsInstance<DisplayEvent.Removed>().map { it.displayId }

    // This is necessary because there might be multiple displays, and we could
    // have missed events for those added before this process or flow started.
    // Note it causes a binder call from the main thread (it's traced).
@@ -180,7 +186,7 @@ constructor(
     *
     * Those are commonly the ones provided by [DisplayManager.getDisplays] by default.
     */
    private val enabledDisplays: Flow<Set<Display>> =
    private val enabledDisplays: StateFlow<Set<Display>> =
        enabledDisplayIds
            .mapElementsLazily { displayId -> getDisplayFromDisplayManager(displayId) }
            .onEach {
@@ -204,7 +210,7 @@ constructor(
     *
     * Those are commonly the ones provided by [DisplayManager.getDisplays] by default.
     */
    override val displays: Flow<Set<Display>> = enabledDisplays
    override val displays: StateFlow<Set<Display>> = enabledDisplays

    val _ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet())
    private val ignoredDisplayIds: Flow<Set<Int>> = _ignoredDisplayIds.debugLog("ignoredDisplayIds")
+76 −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.data.repository

import android.view.Display
import com.android.app.tracing.coroutines.createCoroutineTracingContext
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

/**
 * Provides per display instances of [CoroutineScope]. These will remain active as long as the
 * display is connected, and automatically cancelled when the display is removed.
 */
interface DisplayScopeRepository {
    fun scopeForDisplay(displayId: Int): CoroutineScope
}

@SysUISingleton
class DisplayScopeRepositoryImpl
@Inject
constructor(
    @Background private val backgroundApplicationScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val displayRepository: DisplayRepository,
) : DisplayScopeRepository, CoreStartable {

    private val perDisplayScopes = ConcurrentHashMap<Int, CoroutineScope>()

    override fun scopeForDisplay(displayId: Int): CoroutineScope {
        return perDisplayScopes.computeIfAbsent(displayId) { createScopeForDisplay(displayId) }
    }

    override fun start() {
        StatusBarConnectedDisplays.assertInNewMode()
        backgroundApplicationScope.launch {
            displayRepository.displayRemovalEvent.collect { displayId ->
                val scope = perDisplayScopes.remove(displayId)
                scope?.cancel("Display $displayId has been removed.")
            }
        }
    }

    private fun createScopeForDisplay(displayId: Int): CoroutineScope {
        return if (displayId == Display.DEFAULT_DISPLAY) {
            // The default display is connected all the time, therefore we can optimise by reusing
            // the application scope, and don't need to create a new scope.
            backgroundApplicationScope
        } else {
            CoroutineScope(
                backgroundDispatcher + createCoroutineTracingContext("DisplayScope$displayId")
            )
        }
    }
}
+100 −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.data.repository

import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.unconfinedTestDispatcher
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.isActive
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
@RunWith(AndroidJUnit4::class)
@SmallTest
class DisplayScopeRepositoryImplTest : SysuiTestCase() {

    private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
    private val testScope = kosmos.testScope
    private val fakeDisplayRepository = kosmos.displayRepository

    private val repo =
        DisplayScopeRepositoryImpl(
            kosmos.applicationCoroutineScope,
            kosmos.testDispatcher,
            fakeDisplayRepository,
        )

    @Before
    fun setUp() {
        repo.start()
    }

    @Test
    fun scopeForDisplay_multipleCallsForSameDisplayId_returnsSameInstance() {
        val scopeForDisplay = repo.scopeForDisplay(displayId = 1)

        assertThat(repo.scopeForDisplay(displayId = 1)).isSameInstanceAs(scopeForDisplay)
    }

    @Test
    fun scopeForDisplay_differentDisplayId_returnsNewInstance() {
        val scopeForDisplay1 = repo.scopeForDisplay(displayId = 1)
        val scopeForDisplay2 = repo.scopeForDisplay(displayId = 2)

        assertThat(scopeForDisplay1).isNotSameInstanceAs(scopeForDisplay2)
    }

    @Test
    fun scopeForDisplay_activeByDefault() =
        testScope.runTest {
            val scopeForDisplay = repo.scopeForDisplay(displayId = 1)

            assertThat(scopeForDisplay.isActive).isTrue()
        }

    @Test
    fun scopeForDisplay_afterDisplayRemoved_scopeIsCancelled() =
        testScope.runTest {
            val scopeForDisplay = repo.scopeForDisplay(displayId = 1)

            fakeDisplayRepository.removeDisplay(displayId = 1)

            assertThat(scopeForDisplay.isActive).isFalse()
        }

    @Test
    fun scopeForDisplay_afterDisplayRemoved_returnsNewInstance() =
        testScope.runTest {
            val initialScope = repo.scopeForDisplay(displayId = 1)

            fakeDisplayRepository.removeDisplay(displayId = 1)

            val newScope = repo.scopeForDisplay(displayId = 1)
            assertThat(newScope).isNotSameInstanceAs(initialScope)
        }
}
+19 −9
Original line number Diff line number Diff line
@@ -24,16 +24,12 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.mockito.Mockito.`when` as whenever

/** Creates a mock display. */
fun display(
    type: Int,
    flags: Int = 0,
    id: Int = 0,
    state: Int? = null,
): Display {
fun display(type: Int, flags: Int = 0, id: Int = 0, state: Int? = null): Display {
    return mock {
        whenever(this.displayId).thenReturn(id)
        whenever(this.type).thenReturn(type)
@@ -51,10 +47,21 @@ fun createPendingDisplay(id: Int = 0): DisplayRepository.PendingDisplay =
@SysUISingleton
/** Fake [DisplayRepository] implementation for testing. */
class FakeDisplayRepository @Inject constructor() : DisplayRepository {
    private val flow = MutableSharedFlow<Set<Display>>(replay = 1)
    private val flow = MutableStateFlow<Set<Display>>(emptySet())
    private val pendingDisplayFlow =
        MutableSharedFlow<DisplayRepository.PendingDisplay?>(replay = 1)
    private val displayAdditionEventFlow = MutableSharedFlow<Display?>(replay = 1)
    private val displayAdditionEventFlow = MutableSharedFlow<Display?>(replay = 0)
    private val displayRemovalEventFlow = MutableSharedFlow<Int>(replay = 0)

    suspend fun addDisplay(display: Display) {
        flow.value += display
        displayAdditionEventFlow.emit(display)
    }

    suspend fun removeDisplay(displayId: Int) {
        flow.value = flow.value.filter { it.displayId != displayId }.toSet()
        displayRemovalEventFlow.emit(displayId)
    }

    /** Emits [value] as [displayAdditionEvent] flow value. */
    suspend fun emit(value: Display?) = displayAdditionEventFlow.emit(value)
@@ -65,7 +72,7 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository {
    /** Emits [value] as [pendingDisplay] flow value. */
    suspend fun emit(value: DisplayRepository.PendingDisplay?) = pendingDisplayFlow.emit(value)

    override val displays: Flow<Set<Display>>
    override val displays: StateFlow<Set<Display>>
        get() = flow

    override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?>
@@ -78,8 +85,11 @@ class FakeDisplayRepository @Inject constructor() : DisplayRepository {
    override val displayAdditionEvent: Flow<Display?>
        get() = displayAdditionEventFlow

    override val displayRemovalEvent: Flow<Int> = displayRemovalEventFlow

    private val _displayChangeEvent = MutableSharedFlow<Int>(replay = 1)
    override val displayChangeEvent: Flow<Int> = _displayChangeEvent

    suspend fun emitDisplayChangeEvent(displayId: Int) = _displayChangeEvent.emit(displayId)

    fun setDefaultDisplayOff(defaultDisplayOff: Boolean) {