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

Commit 3796c202 authored by Matthew DeVore's avatar Matthew DeVore
Browse files

connected display: merge injector classes into one

Merge the three injector classes into a single class, rewriting the
two older ones in Kotlin for a more compact implementation.

The injector classes didn't seem to be divided in a meaningful
manner, and I somewhat regret creating a new one only for the topology
pane. The first two classes are in a hierarchy together, but the base
class was never created as itself, so these did not need to be separate
either.

Having the classes separate was not a problem when they were mostly only
used in one file each (DisplayTopologyPreference and
ExternalDisplayPreferenceFragment) but I will soon need to pass an
injector to DisplayBlock so having Injectors that belonged only to
a single class each will probably be confusing. Hence this CL.

Bug: b/397231553
Test: atest ExternalDisplayUpdaterTest.java
Test: atest ExternalDisplayTestBase.java
Test: atest ResolutionPreferenceFragmentTest.java
Test: atest TopologyClampTest.kt
Test: atest TopologyScaleTest.kt
Test: atest ExternalDisplayPreferenceFragmentTest.java
Test: atest DisplayTopologyPreferenceTest.kt
Flag: EXEMPT refactor
Change-Id: I7ceb29644e3d07c852da6e9cfd2a5b41ee3b482a
parent 273b726b
Loading
Loading
Loading
Loading
+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"
    }
}
+3 −46
Original line number Diff line number Diff line
@@ -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
@@ -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.
@@ -84,8 +81,6 @@ class DisplayTopologyPreference(context : Context)
        isPersistent = false

        isCopyingEnabled = false

        injector = Injector(context)
    }

    override fun onBindViewHolder(holder: PreferenceViewHolder) {
@@ -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
+7 −9
Original line number Diff line number Diff line
@@ -46,7 +46,6 @@ import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragmentBase;
import com.android.settings.accessibility.TextReadingPreferenceFragment;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
import com.android.settings.core.SubSettingLauncher;
import com.android.settingslib.widget.FooterPreference;
import com.android.settingslib.widget.IllustrationPreference;
@@ -141,7 +140,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
    @Nullable
    private Preference mBuiltinDisplaySizeAndTextPreference;
    @Nullable
    private Injector mInjector;
    private ConnectedDisplayInjector mInjector;
    @Nullable
    private String[] mRotationEntries;
    @Nullable
@@ -158,7 +157,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
    public ExternalDisplayPreferenceFragment() {}

    @VisibleForTesting
    ExternalDisplayPreferenceFragment(@NonNull Injector injector) {
    ExternalDisplayPreferenceFragment(@NonNull ConnectedDisplayInjector injector) {
        mInjector = injector;
    }

@@ -175,7 +174,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
    @Override
    public void onCreateCallback(@Nullable Bundle icicle) {
        if (mInjector == null) {
            mInjector = new Injector(getPrefContext());
            mInjector = new ConnectedDisplayInjector(getPrefContext());
        }
        addPreferencesFromResource(EXTERNAL_DISPLAY_SETTINGS_RESOURCE);
    }
@@ -328,9 +327,9 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
        return mBuiltinDisplaySizeAndTextPreference;
    }

    @NonNull Preference getDisplayTopologyPreference(@NonNull Context context) {
    @NonNull Preference getDisplayTopologyPreference() {
        if (mDisplayTopologyPreference == null) {
            mDisplayTopologyPreference = new DisplayTopologyPreference(context);
            mDisplayTopologyPreference = new DisplayTopologyPreference(mInjector);
            PrefBasics.DISPLAY_TOPOLOGY.apply(mDisplayTopologyPreference, /* nth= */ null);
        }
        return mDisplayTopologyPreference;
@@ -374,8 +373,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen
    }

    private void updateScreen(final PrefRefresh screen, Context context) {
        final var displaysToShow = mInjector == null
                ? List.<DisplayDevice>of() : mInjector.getConnectedDisplays();
        final var displaysToShow = mInjector.getConnectedDisplays();

        if (displaysToShow.isEmpty()) {
            showTextWhenNoDisplaysToShow(screen, context, /* position= */ 0);
@@ -450,7 +448,7 @@ public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmen

    private void maybeAddV2Components(Context context, PrefRefresh screen) {
        if (isTopologyPaneEnabled(mInjector)) {
            screen.addPreference(getDisplayTopologyPreference(context));
            screen.addPreference(getDisplayTopologyPreference());
            addMirrorPreference(context, screen);

            // If topology is shown, we also show a preference for the built-in display for
+5 −276

File changed.

Preview size limit exceeded, changes collapsed.

+3 −4
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.connecteddevice.DevicePreferenceCallback;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.RestrictedLockUtils;
@@ -49,7 +48,7 @@ public class ExternalDisplayUpdater {
    @Nullable
    private RestrictedPreference mPreference;
    @Nullable
    private Injector mInjector;
    private ConnectedDisplayInjector mInjector;
    private final DisplayListener mListener =  new DisplayListener() {
        @Override
        public void update(int displayId) {
@@ -67,11 +66,11 @@ public class ExternalDisplayUpdater {
     * Set the context to generate the {@link Preference}, so it could get the correct theme.
     */
    public void initPreference(@NonNull Context context) {
        initPreference(context, new Injector(context));
        initPreference(context, new ConnectedDisplayInjector(context));
    }

    @VisibleForTesting
    void initPreference(@NonNull Context context, Injector injector) {
    void initPreference(@NonNull Context context, ConnectedDisplayInjector injector) {
        mInjector = injector;
        mPreference = new RestrictedPreference(context, null /* AttributeSet */);
        mPreference.setTitle(R.string.external_display_settings_title);
Loading