Loading res/drawable/display_block_unselected_background.xml→res/drawable/display_block_background.xml +39 −0 Original line number Diff line number Diff line Loading @@ -15,9 +15,25 @@ ~ --> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- A non-rounded rectangle is needed to prevent the enclosed image from leaking out behind the rounded corners. --> <item> <shape android:shape="rectangle"> <stroke android:color="@color/display_topology_background_color" android:width="@dimen/display_block_padding" /> </shape> </item> <item> <shape android:shape="rectangle"> <stroke android:color="@color/display_topology_background_color" android:width="@dimen/display_block_padding" /> <corners android:radius="@dimen/display_block_corner_radius" /> </shape> </item> </layer-list> res/drawable/display_block_selection_marker_background.xml +6 −22 Original line number Diff line number Diff line Loading @@ -15,25 +15,9 @@ ~ --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Inner border --> <item> <shape android:shape="rectangle"> <stroke android:color="@color/display_topology_background_color" android:width="@dimen/display_block_padding" /> <corners android:radius="@dimen/display_block_corner_radius" /> </shape> </item> <!-- Outer border --> <item> <shape android:shape="rectangle"> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <stroke android:color="@color/system_secondary" android:width="@dimen/display_block_highlight_width" /> <corners android:radius="@dimen/display_block_corner_radius" /> </shape> </item> </layer-list> src/com/android/settings/connecteddevice/display/ConnectedDisplayInjector.kt 0 → 100644 +233 −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.app.WallpaperManager import android.content.Context import android.content.Context.DISPLAY_SERVICE 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 import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_REMOVED import android.hardware.display.DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED import android.hardware.display.DisplayManagerGlobal import android.hardware.display.DisplayTopology import android.os.Handler import android.os.Looper import android.os.RemoteException import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.view.Display import android.view.Display.INVALID_DISPLAY import android.view.DisplayInfo import android.view.IWindowManager 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 import com.android.settings.flags.FeatureFlags import com.android.settings.flags.FeatureFlagsImpl import java.util.function.Consumer open class ConnectedDisplayInjector(open val context: Context?) { open val flags: FeatureFlags by lazy { DesktopExperienceFlags(FeatureFlagsImpl()) } open val handler: Handler by lazy { Handler(Looper.getMainLooper()) } /** * @param name of a system property. * @return the value of the system property. */ open fun getSystemProperty(name: String): String = SystemProperties.get(name) /** The display manager instance, or null if context is null. */ val displayManager: DisplayManager? by lazy { context?.getSystemService(DisplayManager::class.java) } /** The window manager instance, or null if it cannot be retrieved. */ val windowManager: IWindowManager? by lazy { WindowManagerGlobal.getWindowManagerService() } private fun wrapDmDisplay(display: Display, isEnabled: DisplayIsEnabled): DisplayDevice = DisplayDevice(display.displayId, display.name, display.mode, display.getSupportedModes().asList(), isEnabled) private fun isDisplayAllowed(display: Display): Boolean = display.type == Display.TYPE_EXTERNAL || display.type == Display.TYPE_OVERLAY || isVirtualDisplayAllowed(display); private fun isVirtualDisplayAllowed(display: Display): Boolean { val sysProp = getSystemProperty(VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY) return !sysProp.isEmpty() && display.type == Display.TYPE_VIRTUAL && sysProp == display.ownerPackageName } /** * @return all displays including disabled. */ open fun getConnectedDisplays(): List<DisplayDevice> { val dm = displayManager ?: return emptyList() val enabledIds = dm.getDisplays().map { it.getDisplayId() }.toSet() return dm.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) .filter { isDisplayAllowed(it) } .map { val isEnabled = if (enabledIds.contains(it.displayId)) DisplayIsEnabled.YES else DisplayIsEnabled.NO wrapDmDisplay(it, isEnabled) } .toList() } /** * @param displayId which must be returned * @return display object for the displayId, or null if display is not a connected display, * the ID was not found, or the ID was invalid */ open fun getDisplay(displayId: Int): DisplayDevice? { if (displayId == INVALID_DISPLAY) { return null } val display = displayManager?.getDisplay(displayId) ?: return null return if (isDisplayAllowed(display)) { wrapDmDisplay(display, DisplayIsEnabled.UNKNOWN) } else { null } } /** * Register display listener. */ open fun registerDisplayListener(listener: DisplayManager.DisplayListener) { displayManager?.registerDisplayListener(listener, handler, EVENT_TYPE_DISPLAY_ADDED or EVENT_TYPE_DISPLAY_CHANGED or EVENT_TYPE_DISPLAY_REMOVED, PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED) } /** * Unregister display listener. */ open fun unregisterDisplayListener(listener: DisplayManager.DisplayListener) { displayManager?.unregisterDisplayListener(listener) } /** * Enable connected display. */ open fun enableConnectedDisplay(displayId: Int): Boolean { val dm = displayManager ?: return false dm.enableConnectedDisplay(displayId) return true } /** * Disable connected display. */ open fun disableConnectedDisplay(displayId: Int): Boolean { val dm = displayManager ?: return false dm.disableConnectedDisplay(displayId) return true } /** * Get display rotation * @param displayId display identifier * @return rotation */ open fun getDisplayUserRotation(displayId: Int): Int { val wm = windowManager ?: return 0 try { return wm.getDisplayUserRotation(displayId) } catch (e: RemoteException) { Log.e(TAG, "Error getting user rotation of display $displayId", e) return 0 } } /** * Freeze rotation of the display in the specified rotation. * @param displayId display identifier * @param rotation [0, 1, 2, 3] * @return true if successful */ open fun freezeDisplayRotation(displayId: Int, rotation: Int): Boolean { val wm = windowManager ?: return false try { wm.freezeDisplayRotation(displayId, rotation, "ExternalDisplayPreferenceFragment") return true } catch (e: RemoteException) { Log.e(TAG, "Error while freezing user rotation of display $displayId", e) return false } } /** * Enforce display mode on the given display. */ open fun setUserPreferredDisplayMode(displayId: Int, mode: Display.Mode) { DisplayManagerGlobal.getInstance().setUserPreferredDisplayMode(displayId, mode) } /** * @return true if the display mode limit flag enabled. */ open fun isModeLimitForExternalDisplayEnabled(): Boolean = enableModeLimitForExternalDisplay() open var displayTopology : DisplayTopology? get() = displayManager?.displayTopology set(value) { displayManager?.let { it.displayTopology = value } } open val wallpaper: Bitmap? get() = WallpaperManager.getInstance(context).bitmap /** * This density is the density of the current display (showing the Settings app UI). It is * necessary to use this density here because the topology pane coordinates are in physical * pixels, and the display bounds and accessibility constraints are in density-independent * pixels. */ open val densityDpi: Int by lazy { val c = context val info = DisplayInfo() if (c != null && c.display.getDisplayInfo(info)) { info.logicalDensityDpi } else { DisplayMetrics.DENSITY_DEFAULT } } open fun registerTopologyListener(listener: Consumer<DisplayTopology>) { val executor = context?.mainExecutor if (executor != null) displayManager?.registerTopologyListener(executor, listener) } open fun unregisterTopologyListener(listener: Consumer<DisplayTopology>) { displayManager?.unregisterTopologyListener(listener) } private companion object { private const val TAG = "ConnectedDisplayInjector" } } src/com/android/settings/connecteddevice/display/DisplayBlock.kt +56 −29 Original line number Diff line number Diff line Loading @@ -24,23 +24,25 @@ import android.graphics.Color import android.graphics.PointF import android.graphics.RectF import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.LayerDrawable import android.widget.Button import android.view.View import android.widget.FrameLayout import androidx.annotation.VisibleForTesting /** Represents a draggable block in the topology pane. */ class DisplayBlock(context : Context) : Button(context) { @VisibleForTesting var mSelectedImage: Drawable = ColorDrawable(Color.BLACK) @VisibleForTesting var mUnselectedImage: Drawable = ColorDrawable(Color.BLACK) class DisplayBlock(context : Context) : FrameLayout(context) { @VisibleForTesting val mHighlightPx = context.resources.getDimensionPixelSize( R.dimen.display_block_highlight_width) private val mSelectedBg = context.getDrawable( R.drawable.display_block_selection_marker_background)!! private val mUnselectedBg = context.getDrawable( R.drawable.display_block_unselected_background)!! private val mInsetPx = context.resources.getDimensionPixelSize(R.dimen.display_block_padding) val mWallpaperView = View(context) private val mBackgroundView = View(context).apply { background = context.getDrawable(R.drawable.display_block_background) } @VisibleForTesting val mSelectionMarkerView = View(context).apply { background = context.getDrawable(R.drawable.display_block_selection_marker_background) } init { isScrollContainer = false Loading @@ -49,27 +51,33 @@ class DisplayBlock(context : Context) : Button(context) { // Prevents shadow from appearing around edge of button. stateListAnimator = null } /** Sets position of the block given unpadded coordinates. */ fun place(topLeft: PointF) { x = topLeft.x y = topLeft.y addView(mWallpaperView) addView(mBackgroundView) addView(mSelectionMarkerView) } fun setWallpaper(wallpaper: Bitmap?) { val wallpaperDrawable = BitmapDrawable(context.resources, wallpaper ?: return) fun framedBy(bg: Drawable): Drawable = LayerDrawable(arrayOf(wallpaperDrawable, bg)).apply { setLayerInsetRelative(0, mInsetPx, mInsetPx, mInsetPx, mInsetPx) mWallpaperView.background = BitmapDrawable(context.resources, wallpaper ?: return) } mSelectedImage = framedBy(mSelectedBg) mUnselectedImage = framedBy(mUnselectedBg) /** * The coordinates of the upper-left corner of the block in pane coordinates, not including the * highlight border. */ var positionInPane: PointF get() = PointF(x + mHighlightPx, y + mHighlightPx) set(value: PointF) { x = value.x - mHighlightPx y = value.y - mHighlightPx } fun setHighlighted(value: Boolean) { background = if (value) mSelectedImage else mUnselectedImage mSelectionMarkerView.visibility = if (value) View.VISIBLE else View.INVISIBLE // The highlighted block must be draw last so that its highlight shows over the borders of // other displays. z = if (value) 2f else 1f } /** Sets position and size of the block given unpadded bounds. */ Loading @@ -77,9 +85,28 @@ class DisplayBlock(context : Context) : Button(context) { val topLeft = scale.displayToPaneCoor(bounds.left, bounds.top) val bottomRight = scale.displayToPaneCoor(bounds.right, bounds.bottom) val layout = layoutParams layout.width = (bottomRight.x - topLeft.x).toInt() layout.height = (bottomRight.y - topLeft.y).toInt() 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 place(topLeft) positionInPane = topLeft // The highlight is the outermost border. The highlight is shown outside of the parent // FrameLayout so that it consumes the padding between the blocks. mWallpaperView.layoutParams.let { it.width = newWidth it.height = newHeight if (it is MarginLayoutParams) { it.leftMargin = mHighlightPx it.topMargin = mHighlightPx it.bottomMargin = mHighlightPx it.topMargin = mHighlightPx } mWallpaperView.layoutParams = it } // The other two child views are MATCH_PARENT by default so will resize to fill up the // FrameLayout. } } src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +11 −54 Original line number Diff line number Diff line Loading @@ -24,10 +24,8 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.PointF import android.graphics.RectF import android.hardware.display.DisplayManager import android.hardware.display.DisplayTopology import android.util.DisplayMetrics import android.view.DisplayInfo import android.view.MotionEvent import android.view.ViewTreeObserver import android.widget.FrameLayout Loading @@ -45,14 +43,13 @@ import kotlin.math.abs * DisplayTopologyPreference allows the user to change the display topology * when there is one or more extended display attached. */ class DisplayTopologyPreference(context : Context) : Preference(context), ViewTreeObserver.OnGlobalLayoutListener, GroupSectionDividerMixin { class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : Preference(injector.context!!), ViewTreeObserver.OnGlobalLayoutListener, GroupSectionDividerMixin { @VisibleForTesting lateinit var mPaneContent : FrameLayout @VisibleForTesting lateinit var mPaneHolder : FrameLayout @VisibleForTesting lateinit var mTopologyHint : TextView @VisibleForTesting var injector : Injector /** * How many physical pixels to move in pane coordinates (Pythagorean distance) before a drag is * considered non-trivial and intentional. Loading Loading @@ -84,8 +81,6 @@ class DisplayTopologyPreference(context : Context) isPersistent = false isCopyingEnabled = false injector = Injector(context) } override fun onBindViewHolder(holder: PreferenceViewHolder) { Loading Loading @@ -125,44 +120,6 @@ class DisplayTopologyPreference(context : Context) } } open class Injector(val context : Context) { /** * Lazy property for Display Manager, to prevent eagerly getting the service in unit tests. */ private val displayManager : DisplayManager by lazy { context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager } open var displayTopology : DisplayTopology? get() = displayManager.displayTopology set(value) { displayManager.displayTopology = value } open val wallpaper: Bitmap? get() = WallpaperManager.getInstance(context).bitmap /** * This density is the density of the current display (showing the topology pane). It is * necessary to use this density here because the topology pane coordinates are in physical * pixels, and the display coordinates are in density-independent pixels. */ open val densityDpi: Int by lazy { val info = DisplayInfo() if (context.display.getDisplayInfo(info)) { info.logicalDensityDpi } else { DisplayMetrics.DENSITY_DEFAULT } } open fun registerTopologyListener(listener: Consumer<DisplayTopology>) { displayManager.registerTopologyListener(context.mainExecutor, listener) } open fun unregisterTopologyListener(listener: Consumer<DisplayTopology>) { displayManager.unregisterTopologyListener(listener) } } /** * Holds information about the current system topology. * @param positions list of displays comprised of the display ID and position Loading Loading @@ -311,9 +268,10 @@ class DisplayTopologyPreference(context : Context) // We have to use rawX and rawY for the coordinates since the view receiving the event is // also the view that is moving. We need coordinates relative to something that isn't // moving, and the raw coordinates are relative to the screen. val initialTopLeft = block.positionInPane mDrag = BlockDrag( stationaryDisps.toList(), block, displayId, displayPos.width(), displayPos.height(), initialBlockX = block.x, initialBlockY = block.y, initialBlockX = initialTopLeft.x, initialBlockY = initialTopLeft.y, initialTouchX = ev.rawX, initialTouchY = ev.rawY, startTimeMs = ev.eventTime, ) Loading @@ -335,7 +293,7 @@ class DisplayTopologyPreference(context : Context) dispDragCoor.x + drag.displayWidth, dispDragCoor.y + drag.displayHeight) val snapRect = clampPosition(drag.stationaryDisps.map { it.second }, dispDragRect) drag.display.place(topology.scaling.displayToPaneCoor(snapRect.left, snapRect.top)) drag.display.positionInPane = topology.scaling.displayToPaneCoor(snapRect.left, snapRect.top) return true } Loading @@ -346,18 +304,17 @@ class DisplayTopologyPreference(context : Context) mPaneContent.requestDisallowInterceptTouchEvent(false) drag.display.setHighlighted(false) val dropTopLeft = drag.display.positionInPane val netPxDragged = Math.hypot( (drag.initialBlockX - drag.display.x).toDouble(), (drag.initialBlockY - drag.display.y).toDouble()) (drag.initialBlockX - dropTopLeft.x).toDouble(), (drag.initialBlockY - dropTopLeft.y).toDouble()) val timeDownMs = ev.eventTime - drag.startTimeMs if (netPxDragged < accidentalDragDistancePx && timeDownMs < accidentalDragTimeLimitMs) { drag.display.x = drag.initialBlockX drag.display.y = drag.initialBlockY drag.display.positionInPane = PointF(drag.initialBlockX, drag.initialBlockY) return true } val newCoor = topology.scaling.paneToDisplayCoor( drag.display.x, drag.display.y) val newCoor = topology.scaling.paneToDisplayCoor(dropTopLeft.x, dropTopLeft.y) val newTopology = topology.topology.copy() val newPositions = drag.stationaryDisps.map { (id, pos) -> id to PointF(pos.left, pos.top) } .plus(drag.displayId to newCoor) Loading Loading
res/drawable/display_block_unselected_background.xml→res/drawable/display_block_background.xml +39 −0 Original line number Diff line number Diff line Loading @@ -15,9 +15,25 @@ ~ --> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- A non-rounded rectangle is needed to prevent the enclosed image from leaking out behind the rounded corners. --> <item> <shape android:shape="rectangle"> <stroke android:color="@color/display_topology_background_color" android:width="@dimen/display_block_padding" /> </shape> </item> <item> <shape android:shape="rectangle"> <stroke android:color="@color/display_topology_background_color" android:width="@dimen/display_block_padding" /> <corners android:radius="@dimen/display_block_corner_radius" /> </shape> </item> </layer-list>
res/drawable/display_block_selection_marker_background.xml +6 −22 Original line number Diff line number Diff line Loading @@ -15,25 +15,9 @@ ~ --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Inner border --> <item> <shape android:shape="rectangle"> <stroke android:color="@color/display_topology_background_color" android:width="@dimen/display_block_padding" /> <corners android:radius="@dimen/display_block_corner_radius" /> </shape> </item> <!-- Outer border --> <item> <shape android:shape="rectangle"> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <stroke android:color="@color/system_secondary" android:width="@dimen/display_block_highlight_width" /> <corners android:radius="@dimen/display_block_corner_radius" /> </shape> </item> </layer-list>
src/com/android/settings/connecteddevice/display/ConnectedDisplayInjector.kt 0 → 100644 +233 −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.app.WallpaperManager import android.content.Context import android.content.Context.DISPLAY_SERVICE 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 import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_CHANGED import android.hardware.display.DisplayManager.EVENT_TYPE_DISPLAY_REMOVED import android.hardware.display.DisplayManager.PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED import android.hardware.display.DisplayManagerGlobal import android.hardware.display.DisplayTopology import android.os.Handler import android.os.Looper import android.os.RemoteException import android.os.SystemProperties import android.util.DisplayMetrics import android.util.Log import android.view.Display import android.view.Display.INVALID_DISPLAY import android.view.DisplayInfo import android.view.IWindowManager 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 import com.android.settings.flags.FeatureFlags import com.android.settings.flags.FeatureFlagsImpl import java.util.function.Consumer open class ConnectedDisplayInjector(open val context: Context?) { open val flags: FeatureFlags by lazy { DesktopExperienceFlags(FeatureFlagsImpl()) } open val handler: Handler by lazy { Handler(Looper.getMainLooper()) } /** * @param name of a system property. * @return the value of the system property. */ open fun getSystemProperty(name: String): String = SystemProperties.get(name) /** The display manager instance, or null if context is null. */ val displayManager: DisplayManager? by lazy { context?.getSystemService(DisplayManager::class.java) } /** The window manager instance, or null if it cannot be retrieved. */ val windowManager: IWindowManager? by lazy { WindowManagerGlobal.getWindowManagerService() } private fun wrapDmDisplay(display: Display, isEnabled: DisplayIsEnabled): DisplayDevice = DisplayDevice(display.displayId, display.name, display.mode, display.getSupportedModes().asList(), isEnabled) private fun isDisplayAllowed(display: Display): Boolean = display.type == Display.TYPE_EXTERNAL || display.type == Display.TYPE_OVERLAY || isVirtualDisplayAllowed(display); private fun isVirtualDisplayAllowed(display: Display): Boolean { val sysProp = getSystemProperty(VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY) return !sysProp.isEmpty() && display.type == Display.TYPE_VIRTUAL && sysProp == display.ownerPackageName } /** * @return all displays including disabled. */ open fun getConnectedDisplays(): List<DisplayDevice> { val dm = displayManager ?: return emptyList() val enabledIds = dm.getDisplays().map { it.getDisplayId() }.toSet() return dm.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED) .filter { isDisplayAllowed(it) } .map { val isEnabled = if (enabledIds.contains(it.displayId)) DisplayIsEnabled.YES else DisplayIsEnabled.NO wrapDmDisplay(it, isEnabled) } .toList() } /** * @param displayId which must be returned * @return display object for the displayId, or null if display is not a connected display, * the ID was not found, or the ID was invalid */ open fun getDisplay(displayId: Int): DisplayDevice? { if (displayId == INVALID_DISPLAY) { return null } val display = displayManager?.getDisplay(displayId) ?: return null return if (isDisplayAllowed(display)) { wrapDmDisplay(display, DisplayIsEnabled.UNKNOWN) } else { null } } /** * Register display listener. */ open fun registerDisplayListener(listener: DisplayManager.DisplayListener) { displayManager?.registerDisplayListener(listener, handler, EVENT_TYPE_DISPLAY_ADDED or EVENT_TYPE_DISPLAY_CHANGED or EVENT_TYPE_DISPLAY_REMOVED, PRIVATE_EVENT_TYPE_DISPLAY_CONNECTION_CHANGED) } /** * Unregister display listener. */ open fun unregisterDisplayListener(listener: DisplayManager.DisplayListener) { displayManager?.unregisterDisplayListener(listener) } /** * Enable connected display. */ open fun enableConnectedDisplay(displayId: Int): Boolean { val dm = displayManager ?: return false dm.enableConnectedDisplay(displayId) return true } /** * Disable connected display. */ open fun disableConnectedDisplay(displayId: Int): Boolean { val dm = displayManager ?: return false dm.disableConnectedDisplay(displayId) return true } /** * Get display rotation * @param displayId display identifier * @return rotation */ open fun getDisplayUserRotation(displayId: Int): Int { val wm = windowManager ?: return 0 try { return wm.getDisplayUserRotation(displayId) } catch (e: RemoteException) { Log.e(TAG, "Error getting user rotation of display $displayId", e) return 0 } } /** * Freeze rotation of the display in the specified rotation. * @param displayId display identifier * @param rotation [0, 1, 2, 3] * @return true if successful */ open fun freezeDisplayRotation(displayId: Int, rotation: Int): Boolean { val wm = windowManager ?: return false try { wm.freezeDisplayRotation(displayId, rotation, "ExternalDisplayPreferenceFragment") return true } catch (e: RemoteException) { Log.e(TAG, "Error while freezing user rotation of display $displayId", e) return false } } /** * Enforce display mode on the given display. */ open fun setUserPreferredDisplayMode(displayId: Int, mode: Display.Mode) { DisplayManagerGlobal.getInstance().setUserPreferredDisplayMode(displayId, mode) } /** * @return true if the display mode limit flag enabled. */ open fun isModeLimitForExternalDisplayEnabled(): Boolean = enableModeLimitForExternalDisplay() open var displayTopology : DisplayTopology? get() = displayManager?.displayTopology set(value) { displayManager?.let { it.displayTopology = value } } open val wallpaper: Bitmap? get() = WallpaperManager.getInstance(context).bitmap /** * This density is the density of the current display (showing the Settings app UI). It is * necessary to use this density here because the topology pane coordinates are in physical * pixels, and the display bounds and accessibility constraints are in density-independent * pixels. */ open val densityDpi: Int by lazy { val c = context val info = DisplayInfo() if (c != null && c.display.getDisplayInfo(info)) { info.logicalDensityDpi } else { DisplayMetrics.DENSITY_DEFAULT } } open fun registerTopologyListener(listener: Consumer<DisplayTopology>) { val executor = context?.mainExecutor if (executor != null) displayManager?.registerTopologyListener(executor, listener) } open fun unregisterTopologyListener(listener: Consumer<DisplayTopology>) { displayManager?.unregisterTopologyListener(listener) } private companion object { private const val TAG = "ConnectedDisplayInjector" } }
src/com/android/settings/connecteddevice/display/DisplayBlock.kt +56 −29 Original line number Diff line number Diff line Loading @@ -24,23 +24,25 @@ import android.graphics.Color import android.graphics.PointF import android.graphics.RectF import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.LayerDrawable import android.widget.Button import android.view.View import android.widget.FrameLayout import androidx.annotation.VisibleForTesting /** Represents a draggable block in the topology pane. */ class DisplayBlock(context : Context) : Button(context) { @VisibleForTesting var mSelectedImage: Drawable = ColorDrawable(Color.BLACK) @VisibleForTesting var mUnselectedImage: Drawable = ColorDrawable(Color.BLACK) class DisplayBlock(context : Context) : FrameLayout(context) { @VisibleForTesting val mHighlightPx = context.resources.getDimensionPixelSize( R.dimen.display_block_highlight_width) private val mSelectedBg = context.getDrawable( R.drawable.display_block_selection_marker_background)!! private val mUnselectedBg = context.getDrawable( R.drawable.display_block_unselected_background)!! private val mInsetPx = context.resources.getDimensionPixelSize(R.dimen.display_block_padding) val mWallpaperView = View(context) private val mBackgroundView = View(context).apply { background = context.getDrawable(R.drawable.display_block_background) } @VisibleForTesting val mSelectionMarkerView = View(context).apply { background = context.getDrawable(R.drawable.display_block_selection_marker_background) } init { isScrollContainer = false Loading @@ -49,27 +51,33 @@ class DisplayBlock(context : Context) : Button(context) { // Prevents shadow from appearing around edge of button. stateListAnimator = null } /** Sets position of the block given unpadded coordinates. */ fun place(topLeft: PointF) { x = topLeft.x y = topLeft.y addView(mWallpaperView) addView(mBackgroundView) addView(mSelectionMarkerView) } fun setWallpaper(wallpaper: Bitmap?) { val wallpaperDrawable = BitmapDrawable(context.resources, wallpaper ?: return) fun framedBy(bg: Drawable): Drawable = LayerDrawable(arrayOf(wallpaperDrawable, bg)).apply { setLayerInsetRelative(0, mInsetPx, mInsetPx, mInsetPx, mInsetPx) mWallpaperView.background = BitmapDrawable(context.resources, wallpaper ?: return) } mSelectedImage = framedBy(mSelectedBg) mUnselectedImage = framedBy(mUnselectedBg) /** * The coordinates of the upper-left corner of the block in pane coordinates, not including the * highlight border. */ var positionInPane: PointF get() = PointF(x + mHighlightPx, y + mHighlightPx) set(value: PointF) { x = value.x - mHighlightPx y = value.y - mHighlightPx } fun setHighlighted(value: Boolean) { background = if (value) mSelectedImage else mUnselectedImage mSelectionMarkerView.visibility = if (value) View.VISIBLE else View.INVISIBLE // The highlighted block must be draw last so that its highlight shows over the borders of // other displays. z = if (value) 2f else 1f } /** Sets position and size of the block given unpadded bounds. */ Loading @@ -77,9 +85,28 @@ class DisplayBlock(context : Context) : Button(context) { val topLeft = scale.displayToPaneCoor(bounds.left, bounds.top) val bottomRight = scale.displayToPaneCoor(bounds.right, bounds.bottom) val layout = layoutParams layout.width = (bottomRight.x - topLeft.x).toInt() layout.height = (bottomRight.y - topLeft.y).toInt() 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 place(topLeft) positionInPane = topLeft // The highlight is the outermost border. The highlight is shown outside of the parent // FrameLayout so that it consumes the padding between the blocks. mWallpaperView.layoutParams.let { it.width = newWidth it.height = newHeight if (it is MarginLayoutParams) { it.leftMargin = mHighlightPx it.topMargin = mHighlightPx it.bottomMargin = mHighlightPx it.topMargin = mHighlightPx } mWallpaperView.layoutParams = it } // The other two child views are MATCH_PARENT by default so will resize to fill up the // FrameLayout. } }
src/com/android/settings/connecteddevice/display/DisplayTopologyPreference.kt +11 −54 Original line number Diff line number Diff line Loading @@ -24,10 +24,8 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.PointF import android.graphics.RectF import android.hardware.display.DisplayManager import android.hardware.display.DisplayTopology import android.util.DisplayMetrics import android.view.DisplayInfo import android.view.MotionEvent import android.view.ViewTreeObserver import android.widget.FrameLayout Loading @@ -45,14 +43,13 @@ import kotlin.math.abs * DisplayTopologyPreference allows the user to change the display topology * when there is one or more extended display attached. */ class DisplayTopologyPreference(context : Context) : Preference(context), ViewTreeObserver.OnGlobalLayoutListener, GroupSectionDividerMixin { class DisplayTopologyPreference(val injector: ConnectedDisplayInjector) : Preference(injector.context!!), ViewTreeObserver.OnGlobalLayoutListener, GroupSectionDividerMixin { @VisibleForTesting lateinit var mPaneContent : FrameLayout @VisibleForTesting lateinit var mPaneHolder : FrameLayout @VisibleForTesting lateinit var mTopologyHint : TextView @VisibleForTesting var injector : Injector /** * How many physical pixels to move in pane coordinates (Pythagorean distance) before a drag is * considered non-trivial and intentional. Loading Loading @@ -84,8 +81,6 @@ class DisplayTopologyPreference(context : Context) isPersistent = false isCopyingEnabled = false injector = Injector(context) } override fun onBindViewHolder(holder: PreferenceViewHolder) { Loading Loading @@ -125,44 +120,6 @@ class DisplayTopologyPreference(context : Context) } } open class Injector(val context : Context) { /** * Lazy property for Display Manager, to prevent eagerly getting the service in unit tests. */ private val displayManager : DisplayManager by lazy { context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager } open var displayTopology : DisplayTopology? get() = displayManager.displayTopology set(value) { displayManager.displayTopology = value } open val wallpaper: Bitmap? get() = WallpaperManager.getInstance(context).bitmap /** * This density is the density of the current display (showing the topology pane). It is * necessary to use this density here because the topology pane coordinates are in physical * pixels, and the display coordinates are in density-independent pixels. */ open val densityDpi: Int by lazy { val info = DisplayInfo() if (context.display.getDisplayInfo(info)) { info.logicalDensityDpi } else { DisplayMetrics.DENSITY_DEFAULT } } open fun registerTopologyListener(listener: Consumer<DisplayTopology>) { displayManager.registerTopologyListener(context.mainExecutor, listener) } open fun unregisterTopologyListener(listener: Consumer<DisplayTopology>) { displayManager.unregisterTopologyListener(listener) } } /** * Holds information about the current system topology. * @param positions list of displays comprised of the display ID and position Loading Loading @@ -311,9 +268,10 @@ class DisplayTopologyPreference(context : Context) // We have to use rawX and rawY for the coordinates since the view receiving the event is // also the view that is moving. We need coordinates relative to something that isn't // moving, and the raw coordinates are relative to the screen. val initialTopLeft = block.positionInPane mDrag = BlockDrag( stationaryDisps.toList(), block, displayId, displayPos.width(), displayPos.height(), initialBlockX = block.x, initialBlockY = block.y, initialBlockX = initialTopLeft.x, initialBlockY = initialTopLeft.y, initialTouchX = ev.rawX, initialTouchY = ev.rawY, startTimeMs = ev.eventTime, ) Loading @@ -335,7 +293,7 @@ class DisplayTopologyPreference(context : Context) dispDragCoor.x + drag.displayWidth, dispDragCoor.y + drag.displayHeight) val snapRect = clampPosition(drag.stationaryDisps.map { it.second }, dispDragRect) drag.display.place(topology.scaling.displayToPaneCoor(snapRect.left, snapRect.top)) drag.display.positionInPane = topology.scaling.displayToPaneCoor(snapRect.left, snapRect.top) return true } Loading @@ -346,18 +304,17 @@ class DisplayTopologyPreference(context : Context) mPaneContent.requestDisallowInterceptTouchEvent(false) drag.display.setHighlighted(false) val dropTopLeft = drag.display.positionInPane val netPxDragged = Math.hypot( (drag.initialBlockX - drag.display.x).toDouble(), (drag.initialBlockY - drag.display.y).toDouble()) (drag.initialBlockX - dropTopLeft.x).toDouble(), (drag.initialBlockY - dropTopLeft.y).toDouble()) val timeDownMs = ev.eventTime - drag.startTimeMs if (netPxDragged < accidentalDragDistancePx && timeDownMs < accidentalDragTimeLimitMs) { drag.display.x = drag.initialBlockX drag.display.y = drag.initialBlockY drag.display.positionInPane = PointF(drag.initialBlockX, drag.initialBlockY) return true } val newCoor = topology.scaling.paneToDisplayCoor( drag.display.x, drag.display.y) val newCoor = topology.scaling.paneToDisplayCoor(dropTopLeft.x, dropTopLeft.y) val newTopology = topology.topology.copy() val newPositions = drag.stationaryDisps.map { (id, pos) -> id to PointF(pos.left, pos.top) } .plus(drag.displayId to newCoor) Loading