Loading src/com/android/settings/connecteddevice/display/ConnectedDisplayInjector.kt +41 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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. */ Loading Loading @@ -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 Loading src/com/android/settings/connecteddevice/display/DisplayBlock.kt +97 −16 Original line number Diff line number Diff line Loading @@ -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) } Loading @@ -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) } /** Loading @@ -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. Loading @@ -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" } } src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +7 −13 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading tests/robotests/src/com/android/settings/connecteddevice/display/DisplayBlockTest.kt 0 → 100644 +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") } } tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt +1 −8 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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>) { Loading Loading
src/com/android/settings/connecteddevice/display/ConnectedDisplayInjector.kt +41 −4 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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. */ Loading Loading @@ -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 Loading
src/com/android/settings/connecteddevice/display/DisplayBlock.kt +97 −16 Original line number Diff line number Diff line Loading @@ -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) } Loading @@ -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) } /** Loading @@ -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. Loading @@ -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" } }
src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +7 −13 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading
tests/robotests/src/com/android/settings/connecteddevice/display/DisplayBlockTest.kt 0 → 100644 +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") } }
tests/robotests/src/com/android/settings/connecteddevice/display/DisplayTopologyPreferenceTest.kt +1 −8 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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>) { Loading