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

Commit 1de76a2a authored by Nergi Rahardi's avatar Nergi Rahardi
Browse files

Handle display pane update from DisplayListener

In mirroring mode, DisplayTopology will only contain DEFAULT_DISPLAY,
and hence, no more update will be sent even when a display is added /
removed. To support this, DisplayTopologyPreference needs to listen from
2 sources: DisplayTopologyListener and DisplayListener.

To handle overlapping updates, we should focus DisplayTopologyListener
only to handle updates when mirroring mode or showStackingMirroring
flag is off (to fallback to the existing impl when flag is off).

Meanwhile, if mirroring mode and flag is on, it should rely on
onDisplayUpdate to properly update displayPane.

When switching from mirroring back to non-mirroring, it should update
normally as `topologyInfo` has been set to null in the mirroring update
handling.

Recording: http://shortn/_1TMIECohGv

Bug: 401059862
Test: atest DisplayBlockTest DisplayTopologyPreferenceTest
Flag: com.android.settings.flags.show_stacked_mirroring_display_connected_display_setting

Change-Id: Ie3c6caba02a16c79750dc05fb251b3ad854b432c
parent e11a9396
Loading
Loading
Loading
Loading
+66 −29
Original line number Diff line number Diff line
@@ -60,6 +60,13 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :

    private val topologyListener = Consumer<DisplayTopology> { applyTopology(it) }

    private val displayListener =
        object : ExternalDisplaySettingsConfiguration.DisplayListener() {
            override fun update(displayId: Int) {
                applyDisplayUpdateInMirroringMode()
            }
        }

    private val paneContentLayoutListener =
        object : View.OnLayoutChangeListener {
            override fun onLayoutChange(
@@ -117,6 +124,7 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :
    override fun onAttached() {
        super.onAttached()
        injector.registerTopologyListener(topologyListener)
        injector.registerDisplayListener(displayListener)
    }

    override fun onDetached() {
@@ -128,6 +136,7 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :
        revealedWallpapers = listOf()

        injector.unregisterTopologyListener(topologyListener)
        injector.unregisterDisplayListener(displayListener)
    }

    /**
@@ -200,6 +209,7 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :
        }

        applyTopology(topology)
        applyDisplayUpdateInMirroringMode()
    }

    @VisibleForTesting var timesRefreshedBlocks = 0
@@ -212,26 +222,19 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :
     * 4. Ensure wallpapers are revealed
     */
    private fun applyTopology(topology: DisplayTopology) {
        // Step 1
        val showStackedMirroringDisplay =
            isDisplayInMiroringMode(context) &&
                injector.flags.showStackedMirroringDisplayConnectedDisplaySetting()
        topologyHint.text =
            if (showStackedMirroringDisplay) {
                ""
            } else {
                context.getString(R.string.external_display_topology_hint)
        // If stacked mirroring display is turned on, updates will come from DisplayListener since
        // there's no more topology update when display is added / removed
        if (showStackedMirroringDisplay()) {
            return
        }

        val idToNode = topology.allNodesIdMap()
        val logicalDisplaySizeFetcher = LogicalDisplaySizeFetcher(injector, idToNode)

        // Step 1
        topologyHint.text = context.getString(R.string.external_display_topology_hint)
        // Step 2
        val oldBounds = topologyInfo?.positions
        val newBounds =
            if (showStackedMirroringDisplay)
                processDisplayBoundsMirroringMode(logicalDisplaySizeFetcher)
            else processDisplayBounds(topology)
        val newBounds = buildList {
            val bounds = topology.absoluteBounds
            (0..bounds.size() - 1).forEach { add(Pair(bounds.keyAt(it), bounds.valueAt(it))) }
        }
        if (
            oldBounds != null &&
                oldBounds.size == newBounds.size &&
@@ -241,8 +244,9 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :
        ) {
            return
        }

        // Step 3
        val idToNode = topology.allNodesIdMap()
        val logicalDisplaySizeFetcher = LogicalDisplaySizeFetcher(injector, idToNode)
        val scaling =
            TopologyScale(
                paneContent.width,
@@ -254,16 +258,50 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :
            scaling,
            newBounds,
            logicalDisplaySizeFetcher,
            showStackedMirroringDisplay,
            /* isMirroring= */ false,
        )
        topologyInfo = TopologyInfo(topology, scaling, newBounds)
        // Step 4
        revealWallpapers(idToNode.keys.toSet())
    }

    /**
     * Updating DisplayTopology pane consists of multiple steps:
     * 1. Remove hint text
     * 2. Prepare display blocks positioning
     * 3. Adjust display blocks bounds and scale within the pane
     * 4. Ensure wallpapers are revealed for mirrored display and removed for other displays
     */
    private fun applyDisplayUpdateInMirroringMode() {
        // If stacked mirroring display is turned off, update will be handled by topology update
        if (!showStackedMirroringDisplay()) {
            return
        }
        // Step 1
        topologyHint.text = ""
        // Step 2
        val logicalDisplaySizeFetcher = LogicalDisplaySizeFetcher(injector, emptyMap())
        val newBounds = processDisplayBoundsMirroringMode(logicalDisplaySizeFetcher)
        // Step 3
        val scaling =
            TopologyScale(
                paneContent.width,
                minEdgeLength = DisplayTopology.dpToPx(MIN_EDGE_LENGTH_DP, injector.densityDpi),
                maxEdgeLength = DisplayTopology.dpToPx(MAX_EDGE_LENGTH_DP, injector.densityDpi),
                newBounds.map { it.second },
            )
        setupDisplayPaneAndBlocks(
            scaling,
            newBounds,
            logicalDisplaySizeFetcher,
            /* isMirroring= */ true,
        )
        topologyInfo = null
        // Step 4
        val displayIdsToRevealWallpaper =
            if (showStackedMirroringDisplay) setOf(DEFAULT_DISPLAY)
            else {
                idToNode.keys.toSet()
        revealWallpapers(setOf(DEFAULT_DISPLAY))
    }

    private fun revealWallpapers(displayIdsToRevealWallpaper: Set<Int>) {
        // Construct a map containing revealers that we want to keep (keepRevealing). Then create a
        // list comprised of the values of that map as well as new revealers (revealedWallpapers).
        val keepRevealing =
@@ -283,11 +321,6 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :
                .toList()
    }

    private fun processDisplayBounds(topology: DisplayTopology) = buildList {
        val bounds = topology.absoluteBounds
        (0..bounds.size() - 1).forEach { add(Pair(bounds.keyAt(it), bounds.valueAt(it))) }
    }

    private fun processDisplayBoundsMirroringMode(
        logicalDisplaySizeFetcher: LogicalDisplaySizeFetcher
    ): List<Pair<Int, RectF>> {
@@ -473,6 +506,10 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) :
        return true
    }

    private fun showStackedMirroringDisplay() =
        isDisplayInMiroringMode(context) &&
            injector.flags.showStackedMirroringDisplayConnectedDisplaySetting()

    /**
     * A simple wrapper class to fetch logical display size from either DisplayTopology or directly
     * from DisplayManager. This should used as a temporary variable only for the current
+100 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.settings.connecteddevice.display

import android.content.Context
import android.graphics.RectF
import android.hardware.display.DisplayManager
import android.hardware.display.DisplayTopology
import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM
import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
@@ -65,6 +66,7 @@ class DisplayTopologyPreferenceTest {
        var topology: DisplayTopology? = null

        var topologyListener: Consumer<DisplayTopology>? = null
        var displayListener: DisplayManager.DisplayListener? = null

        override var displayTopology: DisplayTopology?
            get() = topology
@@ -101,6 +103,22 @@ class DisplayTopologyPreferenceTest {
            topologyListener = null
        }

        override fun registerDisplayListener(listener: DisplayManager.DisplayListener) {
            if (displayListener != null) {
                throw IllegalStateException(
                    "already have a listener registered: ${displayListener}"
                )
            }
            displayListener = listener
        }

        override fun unregisterDisplayListener(listener: DisplayManager.DisplayListener) {
            if (displayListener != listener) {
                throw IllegalStateException("no such listener registered: ${listener}")
            }
            displayListener = null
        }

        override fun revealWallpaper(displayId: Int): RevealedWallpaper {
            val childView = View(context)
            val viewManager =
@@ -476,6 +494,39 @@ class DisplayTopologyPreferenceTest {
            )
    }

    @Test
    fun changeToMirroringModeRemoveExistingRevealedWallpapersExceptDefault() {
        setupPaneWithTwoDisplays()
        assertThat(injector.revealLog)
            .containsExactly(
                "revealWallpaper invoked for display $DISPLAY_ID_1",
                "revealWallpaper invoked for display $DISPLAY_ID_2",
            )
        injector.revealLog.clear()

        setMirroringMode(true)
        preference.refreshPane()
        assertThat(injector.revealLog)
            .containsExactly("removed wallpaper revealer for display $DISPLAY_ID_2")
        injector.revealLog.clear()
    }

    @Test
    fun stopMirroringModeRevealWallpapers() {
        setMirroringMode(true)
        setupPaneWithTwoDisplays()
        preference.refreshPane()
        assertThat(injector.revealLog)
            .containsExactly("revealWallpaper invoked for display $DISPLAY_ID_1")
        injector.revealLog.clear()

        setMirroringMode(false)
        preference.refreshPane()
        assertThat(injector.revealLog)
            .containsExactly("revealWallpaper invoked for display $DISPLAY_ID_2")
        injector.revealLog.clear()
    }

    @Test
    fun applyNewTopologyViaListenerUpdate() {
        setupPaneWithTwoDisplays()
@@ -737,6 +788,55 @@ class DisplayTopologyPreferenceTest {
        assertThat(preference.timesRefreshedBlocks).isEqualTo(1)
    }

    @Test
    fun nonMirroringMode_updateOnlyFromDisplayTopologyUpdate() {
        setupPaneWithTwoDisplays()
        assertThat(getPaneChildren()).hasSize(2)

        val newDisplaySize = Size(500, 500)
        injector.topology!!.addDisplay(
            DISPLAY_ID_3,
            newDisplaySize.width,
            newDisplaySize.height,
            DISPLAY_DENSITY,
        )
        injector.displaysSize.put(DISPLAY_ID_3, newDisplaySize)

        injector.displayListener!!.onDisplayAdded(DISPLAY_ID_3)
        // In non-mirroring mode display listener update should be ignored, update will come from
        // DisplayTopologyListener update
        assertThat(getPaneChildren()).hasSize(2)

        injector.topologyListener!!.accept(injector.topology!!)
        // Pane updated
        assertThat(getPaneChildren()).hasSize(3)
    }

    @Test
    fun mirroringMode_noUpdateFromDisplayListenerUpdate() {
        setMirroringMode(true)
        setupPaneWithTwoDisplays()
        assertThat(getPaneChildren()).hasSize(2)

        val newDisplaySize = Size(500, 500)
        injector.topology!!.addDisplay(
            DISPLAY_ID_3,
            newDisplaySize.width,
            newDisplaySize.height,
            DISPLAY_DENSITY,
        )
        injector.displaysSize.put(DISPLAY_ID_3, newDisplaySize)

        injector.topologyListener!!.accept(injector.topology!!)
        // In mirroring mode display topology update should be ignored, update will come from
        // DisplayListener update
        assertThat(getPaneChildren()).hasSize(2)

        injector.displayListener!!.onDisplayAdded(DISPLAY_ID_3)
        // Pane updated
        assertThat(getPaneChildren()).hasSize(3)
    }

    private companion object {
        private const val DISPLAY_ID_1 = DEFAULT_DISPLAY
        private const val DISPLAY_ID_2 = 123