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

Commit db07eb01 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Move DisplayRepository from SystemUI to displaylib" into main

parents 541535c3 803c1079
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ java_library {
        "kotlinx_coroutines_android",
        "dagger2",
        "jsr330",
        "//frameworks/libs/systemui:tracinglib-platform",
    ],
    plugins: ["dagger2-compiler"],
    srcs: ["src/**/*.kt"],
+22 −4
Original line number Diff line number Diff line
@@ -15,10 +15,15 @@
 */
package com.android.app.displaylib

import android.hardware.display.DisplayManager
import android.os.Handler
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
import dagger.Module
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope

/**
 * Component that creates all classes in displaylib.
@@ -33,7 +38,12 @@ interface DisplayLibComponent {

    @Component.Factory
    interface Factory {
        fun create(): DisplayLibComponent
        fun create(
            @BindsInstance displayManager: DisplayManager,
            @BindsInstance bgHandler: Handler,
            @BindsInstance bgApplicationScope: CoroutineScope,
            @BindsInstance backgroundCoroutineDispatcher: CoroutineDispatcher,
        ): DisplayLibComponent
    }

    val displayRepository: DisplayRepository
@@ -47,8 +57,16 @@ interface DisplayLibModule {
/**
 * Just a wrapper to make the generated code to create the component more explicit.
 *
 * This should be called only once per process.
 * This should be called only once per process. Note that [bgHandler], [bgApplicationScope] and
 * [backgroundCoroutineDispatcher] are expected to be backed by background threads. In the future
 * this might throw an exception if they are tied to the main thread!
 */
fun createDisplayLibComponent(): DisplayLibComponent {
    return DaggerDisplayLibComponent.factory().create()
fun createDisplayLibComponent(
    displayManager: DisplayManager,
    bgHandler: Handler,
    bgApplicationScope: CoroutineScope,
    backgroundCoroutineDispatcher: CoroutineDispatcher,
): DisplayLibComponent {
    return DaggerDisplayLibComponent.factory()
        .create(displayManager, bgHandler, bgApplicationScope, backgroundCoroutineDispatcher)
}
+437 −2
Original line number Diff line number Diff line
@@ -15,19 +15,454 @@
 */
package com.android.app.displaylib

import android.hardware.display.DisplayManager
import android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED
import android.hardware.display.DisplayManager.DisplayListener
import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_ADDED
import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED
import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_REMOVED
import android.os.Handler
import android.util.Log
import android.view.Display
import com.android.app.tracing.FlowTracing.traceEach
import com.android.app.tracing.traceSection
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.stateIn

/** Repository for providing access to display related information and events. */
interface DisplayRepository {
    /** Provides the current set of displays. */
    val displays: StateFlow<Set<Display>>

    /** Display change event indicating a change to the given displayId has occurred. */
    val displayChangeEvent: Flow<Int>

    /** 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 display ids.
     *
     * Note that it is preferred to use this instead of [displays] if only the
     * [Display.getDisplayId] is needed.
     */
    val displayIds: StateFlow<Set<Int>>

    /**
     * Pending display id that can be enabled/disabled.
     *
     * When `null`, it means there is no pending display waiting to be enabled.
     */
    val pendingDisplay: Flow<PendingDisplay?>

    /** Whether the default display is currently off. */
    val defaultDisplayOff: Flow<Boolean>

    /**
     * Given a display ID int, return the corresponding Display object, or null if none exist.
     *
     * This method is guaranteed to not result in any binder call.
     */
    fun getDisplay(displayId: Int): Display? =
        displays.value.firstOrNull { it.displayId == displayId }

    /** Represents a connected display that has not been enabled yet. */
    interface PendingDisplay {
        /** Id of the pending display. */
        val id: Int

        /** Enables the display, making it available to the system. */
        suspend fun enable()

        /**
         * Ignores the pending display. When called, this specific display id doesn't appear as
         * pending anymore until the display is disconnected and reconnected again.
         */
        suspend fun ignore()

        /** Disables the display, making it unavailable to the system. */
        suspend fun disable()
    }
}

@Singleton
class DisplayRepositoryImpl @Inject constructor() : DisplayRepository {
    override val displays: StateFlow<Set<Display>> = MutableStateFlow(emptySet())
class DisplayRepositoryImpl
@Inject
constructor(
    private val displayManager: DisplayManager,
    backgroundHandler: Handler,
    bgApplicationScope: CoroutineScope,
    backgroundCoroutineDispatcher: CoroutineDispatcher,
) : DisplayRepository {
    private val allDisplayEvents: Flow<DisplayEvent> =
        callbackFlow {
                val callback =
                    object : DisplayListener {
                        override fun onDisplayAdded(displayId: Int) {
                            trySend(DisplayEvent.Added(displayId))
                        }

                        override fun onDisplayRemoved(displayId: Int) {
                            trySend(DisplayEvent.Removed(displayId))
                        }

                        override fun onDisplayChanged(displayId: Int) {
                            trySend(DisplayEvent.Changed(displayId))
                        }
                    }
                displayManager.registerDisplayListener(
                    callback,
                    backgroundHandler,
                    EVENT_TYPE_DISPLAY_ADDED or
                        EVENT_TYPE_DISPLAY_CHANGED or
                        EVENT_TYPE_DISPLAY_REMOVED,
                )
                awaitClose { displayManager.unregisterDisplayListener(callback) }
            }
            .conflate()
            .onStart { emit(DisplayEvent.Changed(Display.DEFAULT_DISPLAY)) }
            .debugLog("allDisplayEvents")
            .flowOn(backgroundCoroutineDispatcher)

    override val displayChangeEvent: Flow<Int> =
        allDisplayEvents.filterIsInstance<DisplayEvent.Changed>().map { event -> event.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).
    private val initialDisplays: Set<Display> =
        traceSection("$TAG#initialDisplays") { displayManager.displays?.toSet() ?: emptySet() }
    private val initialDisplayIds = initialDisplays.map { display -> display.displayId }.toSet()

    /** Propagate to the listeners only enabled displays */
    private val enabledDisplayIds: StateFlow<Set<Int>> =
        allDisplayEvents
            .scan(initial = initialDisplayIds) { previousIds: Set<Int>, event: DisplayEvent ->
                val id = event.displayId
                when (event) {
                    is DisplayEvent.Removed -> previousIds - id
                    is DisplayEvent.Added,
                    is DisplayEvent.Changed -> previousIds + id
                }
            }
            .distinctUntilChanged()
            .debugLog("enabledDisplayIds")
            .stateIn(bgApplicationScope, SharingStarted.WhileSubscribed(), initialDisplayIds)

    private val defaultDisplay by lazy {
        getDisplayFromDisplayManager(Display.DEFAULT_DISPLAY)
            ?: error("Unable to get default display.")
    }
    /**
     * Represents displays that went though the [DisplayListener.onDisplayAdded] callback.
     *
     * Those are commonly the ones provided by [DisplayManager.getDisplays] by default.
     */
    private val enabledDisplays: StateFlow<Set<Display>> =
        enabledDisplayIds
            .mapElementsLazily { displayId -> getDisplayFromDisplayManager(displayId) }
            .onEach {
                if (it.isEmpty()) Log.wtf(TAG, "No enabled displays. This should never happen.")
            }
            .flowOn(backgroundCoroutineDispatcher)
            .debugLog("enabledDisplays")
            .stateIn(
                bgApplicationScope,
                started = SharingStarted.WhileSubscribed(),
                // This triggers a single binder call on the UI thread per process. The
                // alternative would be to use sharedFlows, but they are prohibited due to
                // performance concerns.
                // Ultimately, this is a trade-off between a one-time UI thread binder call and
                // the constant overhead of sharedFlows.
                initialValue = initialDisplays,
            )

    /**
     * Represents displays that went though the [DisplayListener.onDisplayAdded] callback.
     *
     * Those are commonly the ones provided by [DisplayManager.getDisplays] by default.
     */
    override val displays: StateFlow<Set<Display>> = enabledDisplays

    override val displayIds: StateFlow<Set<Int>> = enabledDisplayIds

    /**
     * Implementation that maps from [displays], instead of [allDisplayEvents] for 2 reasons:
     * 1. Guarantee that it emits __after__ [displays] emitted. This way it is guaranteed that
     *    calling [getDisplay] for the newly added display will be non-null.
     * 2. Reuse the existing instance of [Display] without a new call to [DisplayManager].
     */
    override val displayAdditionEvent: Flow<Display?> =
        displays
            .pairwiseBy { previousDisplays, currentDisplays -> currentDisplays - previousDisplays }
            .flatMapLatest { it.asFlow() }

    val _ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet())
    private val ignoredDisplayIds: Flow<Set<Int>> = _ignoredDisplayIds.debugLog("ignoredDisplayIds")

    private fun getInitialConnectedDisplays(): Set<Int> =
        traceSection("$TAG#getInitialConnectedDisplays") {
            displayManager
                .getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED)
                .map { it.displayId }
                .toSet()
                .also {
                    if (DEBUG) {
                        Log.d(TAG, "getInitialConnectedDisplays: $it")
                    }
                }
        }

    /* keeps connected displays until they are disconnected. */
    private val connectedDisplayIds: StateFlow<Set<Int>> =
        callbackFlow {
                val connectedIds = getInitialConnectedDisplays().toMutableSet()
                val callback =
                    object : DisplayConnectionListener {
                        override fun onDisplayConnected(id: Int) {
                            if (DEBUG) {
                                Log.d(TAG, "display with id=$id connected.")
                            }
                            connectedIds += id
                            _ignoredDisplayIds.value -= id
                            trySend(connectedIds.toSet())
                        }

                        override fun onDisplayDisconnected(id: Int) {
                            connectedIds -= id
                            if (DEBUG) {
                                Log.d(TAG, "display with id=$id disconnected.")
                            }
                            _ignoredDisplayIds.value -= id
                            trySend(connectedIds.toSet())
                        }
                    }
                trySend(connectedIds.toSet())
                displayManager.registerDisplayListener(
                    callback,
                    backgroundHandler,
                    /* eventFlags */ 0,
                    DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED,
                )
                awaitClose { displayManager.unregisterDisplayListener(callback) }
            }
            .conflate()
            .distinctUntilChanged()
            .debugLog("connectedDisplayIds")
            .stateIn(
                bgApplicationScope,
                started = SharingStarted.WhileSubscribed(),
                // The initial value is set to empty, but connected displays are gathered as soon as
                // the flow starts being collected. This is to ensure the call to get displays (an
                // IPC) happens in the background instead of when this object
                // is instantiated.
                initialValue = emptySet(),
            )

    private val connectedExternalDisplayIds: Flow<Set<Int>> =
        connectedDisplayIds
            .map { connectedDisplayIds ->
                traceSection("$TAG#filteringExternalDisplays") {
                    connectedDisplayIds
                        .filter { id -> getDisplayType(id) == Display.TYPE_EXTERNAL }
                        .toSet()
                }
            }
            .flowOn(backgroundCoroutineDispatcher)
            .debugLog("connectedExternalDisplayIds")

    private fun getDisplayType(displayId: Int): Int? =
        traceSection("$TAG#getDisplayType") { displayManager.getDisplay(displayId)?.type }

    private fun getDisplayFromDisplayManager(displayId: Int): Display? =
        traceSection("$TAG#getDisplay") { displayManager.getDisplay(displayId) }

    /**
     * Pending displays are the ones connected, but not enabled and not ignored.
     *
     * A connected display is ignored after the user makes the decision to use it or not. For now,
     * the initial decision from the user is final and not reversible.
     */
    private val pendingDisplayIds: Flow<Set<Int>> =
        combine(enabledDisplayIds, connectedExternalDisplayIds, ignoredDisplayIds) {
                enabledDisplaysIds,
                connectedExternalDisplayIds,
                ignoredDisplayIds ->
                if (DEBUG) {
                    Log.d(
                        TAG,
                        "combining enabled=$enabledDisplaysIds, " +
                            "connectedExternalDisplayIds=$connectedExternalDisplayIds, " +
                            "ignored=$ignoredDisplayIds",
                    )
                }
                connectedExternalDisplayIds - enabledDisplaysIds - ignoredDisplayIds
            }
            .debugLog("allPendingDisplayIds")

    /** Which display id should be enabled among the pending ones. */
    private val pendingDisplayId: Flow<Int?> =
        pendingDisplayIds.map { it.maxOrNull() }.distinctUntilChanged().debugLog("pendingDisplayId")

    override val pendingDisplay: Flow<DisplayRepository.PendingDisplay?> =
        pendingDisplayId
            .map { displayId ->
                val id = displayId ?: return@map null
                object : DisplayRepository.PendingDisplay {
                    override val id = id

                    override suspend fun enable() {
                        traceSection("DisplayRepository#enable($id)") {
                            if (DEBUG) {
                                Log.d(TAG, "Enabling display with id=$id")
                            }
                            displayManager.enableConnectedDisplay(id)
                        }
                        // After the display has been enabled, it is automatically ignored.
                        ignore()
                    }

                    override suspend fun ignore() {
                        traceSection("DisplayRepository#ignore($id)") {
                            _ignoredDisplayIds.value += id
                        }
                    }

                    override suspend fun disable() {
                        ignore()
                        traceSection("DisplayRepository#disable($id)") {
                            if (DEBUG) {
                                Log.d(TAG, "Disabling display with id=$id")
                            }
                            displayManager.disableConnectedDisplay(id)
                        }
                    }
                }
            }
            .debugLog("pendingDisplay")

    override val defaultDisplayOff: Flow<Boolean> =
        displayChangeEvent
            .filter { it == Display.DEFAULT_DISPLAY }
            .map { defaultDisplay.state == Display.STATE_OFF }
            .distinctUntilChanged()

    private fun <T> Flow<T>.debugLog(flowName: String): Flow<T> {
        return if (DEBUG) {
            traceEach(flowName, logcat = true, traceEmissionCount = true)
        } else {
            this
        }
    }

    /**
     * Maps a set of T to a set of V, minimizing the number of `createValue` calls taking into
     * account the diff between each root flow emission.
     *
     * This is needed to minimize the number of [getDisplayFromDisplayManager] in this class. Note
     * that if the [createValue] returns a null element, it will not be added in the output set.
     */
    private fun <T, V> Flow<Set<T>>.mapElementsLazily(createValue: (T) -> V?): Flow<Set<V>> {
        data class State<T, V>(
            val previousSet: Set<T>,
            // Caches T values from the previousSet that were already converted to V
            val valueMap: Map<T, V>,
            val resultSet: Set<V>,
        )

        val emptyInitialState = State(emptySet<T>(), emptyMap(), emptySet<V>())
        return this.scan(emptyInitialState) { state, currentSet ->
                if (currentSet == state.previousSet) {
                    state
                } else {
                    val removed = state.previousSet - currentSet
                    val added = currentSet - state.previousSet
                    val newMap = state.valueMap.toMutableMap()

                    added.forEach { key -> createValue(key)?.let { newMap[key] = it } }
                    removed.forEach { key -> newMap.remove(key) }

                    val resultSet = newMap.values.toSet()
                    State(currentSet, newMap, resultSet)
                }
            }
            .filter { it != emptyInitialState }
            .map { it.resultSet }
    }

    private companion object {
        const val TAG = "DisplayRepository"
        val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
    }
}

/** Used to provide default implementations for all methods. */
private interface DisplayConnectionListener : DisplayListener {

    override fun onDisplayConnected(id: Int) {}

    override fun onDisplayDisconnected(id: Int) {}

    override fun onDisplayAdded(id: Int) {}

    override fun onDisplayRemoved(id: Int) {}

    override fun onDisplayChanged(id: Int) {}
}

private sealed interface DisplayEvent {
    val displayId: Int

    data class Added(override val displayId: Int) : DisplayEvent

    data class Removed(override val displayId: Int) : DisplayEvent

    data class Changed(override val displayId: Int) : DisplayEvent
}

/**
 * Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
 * Note that the new Flow will not start emitting until it has received two emissions from the
 * upstream Flow.
 *
 * Useful for code that needs to compare the current value to the previous value.
 */
// TODO b/401305290 - This should be moved to a shared lib, as it's also used by SystemUI.
fun <T, R> Flow<T>.pairwiseBy(transform: suspend (old: T, new: T) -> R): Flow<R> = flow {
    val noVal = Any()
    var previousValue: Any? = noVal
    collect { newVal ->
        if (previousValue != noVal) {
            @Suppress("UNCHECKED_CAST") emit(transform(previousValue as T, newVal))
        }
        previousValue = newVal
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ android_test {
        "androidx.test.ext.junit",
        "androidx.test.rules",
        "truth",
        "//frameworks/libs/systemui:tracinglib-platform",
    ],
    srcs: [
        "tests/src/**/*.kt",
+1 −6
Original line number Diff line number Diff line
@@ -17,16 +17,11 @@ package com.android.app.displaylib

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class DisplayRepositoryTest {

    @Test
    fun displays_empty() {
        assertThat(DisplayRepositoryImpl().displays.value).isEmpty()
    }
    // TODO b/401305290 - Move tests from The SystemUI DisplayRepositoryImpl to here.
}