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

Commit 00088588 authored by Chris Göllner's avatar Chris Göllner
Browse files

Create DisplayScopeRepository for display specific coroutine scopes

Test: DisplayScopeRepositoryImplTest.kt
Bug: 367592591
Flag: com.android.systemui.status_bar_connected_displays
Change-Id: Id8a7b5263d89f7500dfc5a76368a66b069251f7d
parent 8aac8f1a
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) {