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

Commit 4c486186 authored by Matthew DeVore's avatar Matthew DeVore
Browse files

CD topology: block shows non-image wallpaper

Mirror the wallpaper surface using an API that is not specific to the
wallpaper type.

This has a problem in that if the wallpaper is not visible because of a
fullscreen app covering it, the wallpaper will not be rendered in the
display block. This is documented in b/412755041 and will be addressed
in the next CL.

Flag: com.android.settings.flags.display_topology_pane_in_display_list
Test: set emoji wallpaper on main display and add two other displays, and verify pane refreshes after a change
Test: DisplayBlockTest
Bug: b/397231553
Bug: b/412755041
Bug: b/416249965
Change-Id: I1548872c74822d7930682cdf4b605cbc1e44ab96
parent 78616b10
Loading
Loading
Loading
Loading
+41 −4
Original line number Diff line number Diff line
@@ -16,9 +16,7 @@

package com.android.settings.connecteddevice.display

import android.app.WallpaperManager
import android.content.Context
import android.graphics.Bitmap
import android.hardware.display.DisplayManager
import android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED
import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_ADDED
@@ -37,6 +35,8 @@ import android.view.Display
import android.view.Display.INVALID_DISPLAY
import android.view.DisplayInfo
import android.view.IWindowManager
import android.view.SurfaceControl
import android.view.SurfaceView
import android.view.WindowManagerGlobal
import com.android.server.display.feature.flags.Flags.enableModeLimitForExternalDisplay
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY
@@ -76,6 +76,22 @@ open class ConnectedDisplayInjector(open val context: Context?) {
                && sysProp == display.ownerPackageName
    }

    /**
     * Reparents surface to the SurfaceControl of wallpaperView, so that view will render `surface`.
     * Any surfaces which may be parented to wallpaperView already should be passed in oldSurfaces
     * and they will be removed from the wallpaperView's hierarchy and released.
     */
    open fun updateSurfaceView(oldSurfaces: List<SurfaceControl>, surface: SurfaceControl,
            wallpaperView: SurfaceView, surfaceScale: Float) {
        val t = SurfaceControl.Transaction()
        t.reparent(surface, wallpaperView.surfaceControl)
        t.setScale(surface, surfaceScale, surfaceScale)
        oldSurfaces.forEach { t.remove(it) }
        t.apply(true)

        oldSurfaces.forEach { it.release() }
    }

    /**
     * @return all displays including disabled.
     */
@@ -195,8 +211,29 @@ open class ConnectedDisplayInjector(open val context: Context?) {
        get() = displayManager?.displayTopology
        set(value) { displayManager?.let { it.displayTopology = value } }

    open val wallpaper: Bitmap?
        get() = WallpaperManager.getInstance(context).bitmap
    /**
     * Mirrors the wallpaper of the given display.
     *
     * @return a SurfaceControl for the top of the new hierarchy, or null if an exception occurred.
     */
    open fun wallpaper(displayId: Int): SurfaceControl? {
        try {
            val surface = WindowManagerGlobal.getInstance().mirrorWallpaperSurface(displayId)
            if (surface == null) {
                Log.e(TAG, "mirrorWallpaperSurface returned null SurfaceControl")
            }
            return surface
        } catch (e: RemoteException) {
            Log.e(TAG, "Error while mirroring wallpaper of display $displayId", e)
            return null
        } catch (e: NullPointerException) {
            // This can happen if the display has been detached (b/416291830). The caller should
            // check if the display is still attached, but let's keep this here to prevent the app
            // from crashing.
            Log.e(TAG, "NPE while mirroring wallpaper of display $displayId - already detached?", e)
            return null
        }
    }

    /**
     * This density is the density of the current display (showing the Settings app UI). It is
+97 −16
Original line number Diff line number Diff line
@@ -22,20 +22,71 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.PointF
import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.util.Log
import android.view.SurfaceControl
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.widget.FrameLayout

import androidx.annotation.VisibleForTesting

/** Represents a draggable block in the topology pane. */
class DisplayBlock(context : Context) : FrameLayout(context) {
class DisplayBlock(val injector: ConnectedDisplayInjector) : FrameLayout(injector.context!!) {
    @VisibleForTesting
    val mHighlightPx = context.resources.getDimensionPixelSize(
            R.dimen.display_block_highlight_width)

    val mWallpaperView = View(context)
    private var mDisplayId: Int? = null

    /** Scale of the mirrored wallpaper to the actual wallpaper size. */
    private var mSurfaceScale: Float? = null

    val displayId: Int?
        get() = mDisplayId

    // These are surfaces which must be removed from the display block hierarchy and released once
    // the new surface is put in place. This list can have more than one item because we may get
    // two reset calls before we get a single surfaceChange callback.
    private val mOldSurfaces = mutableListOf<SurfaceControl>()
    private var mWallpaperSurface: SurfaceControl? = null

    private val mUpdateSurfaceView = Runnable { updateSurfaceView() }

    @VisibleForTesting fun updateSurfaceView() {
        val displayId = mDisplayId ?: return

        if (parent == null) {
            Log.i(TAG, "View for display $displayId has no parent - cancelling update")
            return
        }

        var surface = mWallpaperSurface
        if (surface == null) {
            surface = injector.wallpaper(displayId)
            if (surface == null) {
                injector.handler.postDelayed(mUpdateSurfaceView, /* delayMillis= */ 500)
                return
            }
            mWallpaperSurface = surface
        }

        val surfaceScale = mSurfaceScale ?: return
        injector.updateSurfaceView(mOldSurfaces, surface, mWallpaperView, surfaceScale)
        mOldSurfaces.clear()
    }

    private val mHolderCallback = object : SurfaceHolder.Callback {
        override fun surfaceCreated(h: SurfaceHolder) {}

        override fun surfaceChanged(h: SurfaceHolder, format: Int, newWidth: Int, newHeight: Int) {
            updateSurfaceView()
        }

        override fun surfaceDestroyed(h: SurfaceHolder) {}
    }

    val mWallpaperView = SurfaceView(context)
    private val mBackgroundView = View(context).apply {
        background = context.getDrawable(R.drawable.display_block_background)
    }
@@ -55,10 +106,8 @@ class DisplayBlock(context : Context) : FrameLayout(context) {
        addView(mWallpaperView)
        addView(mBackgroundView)
        addView(mSelectionMarkerView)
    }

    fun setWallpaper(wallpaper: Bitmap?) {
        mWallpaperView.background = BitmapDrawable(context.resources, wallpaper ?: return)
        mWallpaperView.holder.addCallback(mHolderCallback)
    }

    /**
@@ -80,17 +129,45 @@ class DisplayBlock(context : Context) : FrameLayout(context) {
        z = if (value) 2f else 1f
    }

    /** Sets position and size of the block given unpadded bounds. */
    fun placeAndSize(bounds : RectF, scale : TopologyScale) {
        val topLeft = scale.displayToPaneCoor(bounds.left, bounds.top)
        val bottomRight = scale.displayToPaneCoor(bounds.right, bounds.bottom)
        val layout = layoutParams
    /**
     * Sets position and size of the block given coordinates in pane space.
     *
     * @param displayId ID of display this block represents, needed for fetching wallpaper
     * @param topLeft coordinates of top left corner of the block, not including highlight border
     * @param bottomRight coordinates of bottom right corner of the block, not including highlight
     *                    border
     * @param surfaceScale scale in pixels of the size of the wallpaper mirror to the actual
     *                     wallpaper on the screen - should be less than one to indicate scaling to
     *                     smaller size
     */
    fun reset(displayId: Int, topLeft: PointF, bottomRight: PointF, surfaceScale: Float) {
        mWallpaperSurface?.let { mOldSurfaces.add(it) }
        injector.handler.removeCallbacks(mUpdateSurfaceView)
        mWallpaperSurface = null
        setHighlighted(false)
        positionInPane = topLeft

        mDisplayId = displayId
        mSurfaceScale = surfaceScale

        val newWidth = (bottomRight.x - topLeft.x).toInt()
        val newHeight = (bottomRight.y - topLeft.y).toInt()
        layout.width = newWidth + 2*mHighlightPx
        layout.height = newHeight + 2*mHighlightPx
        layoutParams = layout
        positionInPane = topLeft

        val paddedWidth = newWidth + 2*mHighlightPx
        val paddedHeight = newHeight + 2*mHighlightPx

        if (width == paddedWidth && height == paddedHeight) {
            // Will not receive a surfaceChanged callback, so in case the wallpaper is different,
            // apply it.
            updateSurfaceView()
            return
        }

        layoutParams.let {
            it.width = paddedWidth
            it.height = paddedHeight
            layoutParams = it
        }

        // The highlight is the outermost border. The highlight is shown outside of the parent
        // FrameLayout so that it consumes the padding between the blocks.
@@ -109,4 +186,8 @@ class DisplayBlock(context : Context) : FrameLayout(context) {
        // The other two child views are MATCH_PARENT by default so will resize to fill up the
        // FrameLayout.
    }

    private companion object {
        private const val TAG = "DisplayBlock"
    }
}
+7 −13
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.settings.connecteddevice.display
import com.android.settings.R
import com.android.settingslib.widget.GroupSectionDividerMixin

import android.app.WallpaperManager
import android.content.Context
import android.graphics.Bitmap
import android.graphics.PointF
@@ -219,22 +218,17 @@ class DisplayTopologyPreference(val injector: ConnectedDisplayInjector)
            }
        }

        var wallpaperBitmap : Bitmap? = null

        val idToNode = topology.allNodesIdMap()
        newBounds.forEach { (id, pos) ->
            val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(context).apply {
                if (wallpaperBitmap == null) {
                    wallpaperBitmap = injector.wallpaper
                }
                // We need a separate wallpaper Drawable for each display block, since each needs to
                // be drawn at a separate size.
                setWallpaper(wallpaperBitmap)

            val block = recycleableBlocks.removeFirstOrNull() ?: DisplayBlock(injector).apply {
                mPaneContent.addView(this)
            }
            block.setHighlighted(false)

            block.placeAndSize(pos, scaling)
            idToNode.get(id)?.let {
                val topLeft = scaling.displayToPaneCoor(pos.left, pos.top)
                val bottomRight = scaling.displayToPaneCoor(pos.right, pos.bottom)
                block.reset(id, topLeft, bottomRight, (bottomRight.x - topLeft.x) / it.logicalWidth)
            }
            block.setOnTouchListener { view, ev ->
                when (ev.actionMasked) {
                    MotionEvent.ACTION_DOWN -> onBlockTouchDown(id, pos, block, ev)
+165 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settings.connecteddevice.display

import android.content.Context
import android.graphics.PointF
import android.os.Handler
import android.view.Display
import android.view.SurfaceControl
import android.view.SurfaceView
import android.widget.FrameLayout
import androidx.preference.PreferenceViewHolder
import androidx.test.core.app.ApplicationProvider
import androidx.test.core.view.MotionEventBuilder

import com.android.settings.R
import com.google.common.truth.Truth.assertThat

import java.util.function.Consumer
import java.util.function.Supplier

import kotlin.math.abs

import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class DisplayBlockTest {
    val context = ApplicationProvider.getApplicationContext<Context>()
    val injector = TestInjector(context)
    val block = DisplayBlock(injector)
    val parentView = FrameLayout(context)

    class TestInjector(context: Context) : ConnectedDisplayInjector(context) {
        /**
         * Return value to use for wallpaper(), by display ID. If an ID is missing, null will be
         * returned.
         */
        var wallpapers = mutableMapOf<Int, SurfaceControl>()
        val updateLog = StringBuilder()

        internal val testHandler = TestHandler(context.mainThreadHandler)

        override val handler: Handler
            get() = testHandler

        override fun updateSurfaceView(oldSurfaces: List<SurfaceControl>, surface: SurfaceControl,
                wallpaperView: SurfaceView, surfaceScale: Float) {
            updateLog.append("oldSurfaces: $oldSurfaces, surface: $surface, surfaceScale: ")
                    .append("%.2f\n".format(surfaceScale))
        }

        override fun wallpaper(displayId: Int): SurfaceControl? = wallpapers.remove(displayId)
    }

    @Test
    fun normalUpdateFlow() {
        val displayId = 42
        val wallpaper42 = SurfaceControl.Builder().setName("wallpaper42").build()

        injector.wallpapers.put(displayId, wallpaper42)

        parentView.addView(block)
        block.reset(displayId, PointF(10f, 10f), PointF(20f, 20f), 0.5f)

        block.updateSurfaceView()

        assertThat(injector.updateLog.toString()).isEqualTo(
                "oldSurfaces: [], surface: $wallpaper42, surfaceScale: 0.50\n")
    }

    @Test
    fun resetTwiceBeforeSurfaceUpdate() {
        val displayId = 42
        val wallpaperA = SurfaceControl.Builder().setName("wallpaperA").build()
        val wallpaperB = SurfaceControl.Builder().setName("wallpaperB").build()

        injector.wallpapers.put(displayId, wallpaperA)

        parentView.addView(block)
        block.reset(displayId, PointF(10f, 10f), PointF(20f, 20f), 0.25f)

        // Should not have fetched wallpaper info yet. Replace wallpaper setting with wallpaperB.
        assertThat(injector.wallpapers.put(displayId, wallpaperB)).isEqualTo(wallpaperA)

        block.reset(displayId, PointF(10f, 10f), PointF(30f, 30f), 0.4f)

        // Should not have fetched wallpaper or display info yet.
        assertThat(injector.wallpapers.get(displayId)).isEqualTo(wallpaperB)
        block.updateSurfaceView()
        assertThat(injector.wallpapers.get(displayId)).isNull()

        assertThat(injector.updateLog.toString()).isEqualTo(
                "oldSurfaces: [], surface: $wallpaperB, surfaceScale: 0.40\n")
    }

    private fun applyRequestedSize() {
        block.right = block.left + block.layoutParams.width
        block.bottom = block.top + block.layoutParams.height
    }

    @Test
    fun resetWithUnchangedSizeCausesImmediateUpdate() {
        val displayId = 42
        val wallpaperX = SurfaceControl.Builder().setName("wallpaperX").build()
        val wallpaperY = SurfaceControl.Builder().setName("wallpaperY").build()

        injector.wallpapers.put(displayId, wallpaperX)

        parentView.addView(block)
        block.reset(displayId, PointF(10f, 10f), PointF(20f, 20f), 0.5f)
        block.updateSurfaceView()

        assertThat(injector.updateLog.toString()).isEqualTo(
                "oldSurfaces: [], surface: $wallpaperX, surfaceScale: 0.50\n")
        applyRequestedSize()
        injector.updateLog.setLength(0)

        // Same size and scale as before, but a new wallpaper and different position in parent view.
        injector.wallpapers.put(displayId, wallpaperY)
        block.reset(displayId, PointF(60f, 10f), PointF(70f, 20f), 0.5f)
        assertThat(injector.updateLog.toString()).isEqualTo(
                "oldSurfaces: [$wallpaperX], surface: $wallpaperY, surfaceScale: 0.50\n")
        applyRequestedSize()
        injector.updateLog.setLength(0)

        // Repeat the pattern, but with a new scale and reverting back to wallpaperX.
        injector.wallpapers.put(displayId, wallpaperX)
        block.reset(displayId, PointF(60f, 30f), PointF(70f, 40f), 0.2f)
        assertThat(injector.updateLog.toString()).isEqualTo(
                "oldSurfaces: [$wallpaperY], surface: $wallpaperX, surfaceScale: 0.20\n")
    }

    @Test
    fun retryIfWallpaperNotReady() {
        val displayId = 42
        val wallpaperW = SurfaceControl.Builder().setName("wallpaperW").build()

        parentView.addView(block)
        block.reset(displayId, PointF(10f, 10f), PointF(20f, 20f), 0.5f)
        block.updateSurfaceView()

        assertThat(injector.updateLog.toString()).isEqualTo("")
        injector.wallpapers.put(displayId, wallpaperW)

        injector.testHandler.flush()
        assertThat(injector.updateLog.toString()).isEqualTo(
                "oldSurfaces: [], surface: $wallpaperW, surfaceScale: 0.50\n")
    }
}
+1 −8
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.RectF
import android.hardware.display.DisplayTopology
@@ -51,26 +50,20 @@ class DisplayTopologyPreferenceTest {
    val preference = DisplayTopologyPreference(injector)
    val rootView = View.inflate(context, preference.layoutResource, /*parent=*/ null)
    val holder = PreferenceViewHolder.createInstanceForTests(rootView)
    val wallpaper = Bitmap.createBitmap(
            intArrayOf(Color.MAGENTA), /*width=*/ 1, /*height=*/ 1, Bitmap.Config.RGB_565)

    init {
        injector.systemWallpaper = wallpaper
        preference.onBindViewHolder(holder)
    }

    class TestInjector(context : Context) : ConnectedDisplayInjector(context) {
        var topology: DisplayTopology? = null
        var systemWallpaper: Bitmap? = null

        var topologyListener: Consumer<DisplayTopology>? = null

        override var displayTopology : DisplayTopology?
            get() = topology
            set(value) { topology = value }

        override val wallpaper: Bitmap?
            get() = systemWallpaper!!

        override val densityDpi = DisplayMetrics.DENSITY_DEFAULT

        override fun registerTopologyListener(listener: Consumer<DisplayTopology>) {