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

Commit d59f5e46 authored by Piotr Wilczyński's avatar Piotr Wilczyński
Browse files

Default display topology initialization

Move the topology tree to a separate class for code readability.

The first external display is placed above the default display.

The subsequent displays are placed to the right of the rightmost display.

Bug: 364909693
Flag: com.android.server.display.feature.flags.display_topology
Test: DisplayManagerServiceTest, DisplayTopologyCoordinatorTest, DisplayTopologyTest, ExternalDisplayStatsServiceTest
Change-Id: I57012e52d327ade8dd8ecaebbbb9e197fb79c7d6
parent 435ec085
Loading
Loading
Loading
Loading
+16 −2
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import static android.hardware.display.HdrConversionMode.HDR_CONVERSION_UNSUPPOR
import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL;
import static android.os.Process.FIRST_APPLICATION_UID;
import static android.os.Process.ROOT_UID;
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
import static android.provider.Settings.Secure.RESOLUTION_MODE_FULL;
import static android.provider.Settings.Secure.RESOLUTION_MODE_HIGH;
import static android.provider.Settings.Secure.RESOLUTION_MODE_UNKNOWN;
@@ -644,12 +645,14 @@ public final class DisplayManagerService extends SystemService {
        mExtraDisplayLoggingPackageName = DisplayProperties.debug_vri_package().orElse(null);
        mExtraDisplayEventLogging = !TextUtils.isEmpty(mExtraDisplayLoggingPackageName);

        mExternalDisplayStatsService = new ExternalDisplayStatsService(mContext, mHandler);
        mExternalDisplayStatsService = new ExternalDisplayStatsService(mContext, mHandler,
                this::isExtendedDisplayEnabled);
        mDisplayNotificationManager = new DisplayNotificationManager(mFlags, mContext,
                mExternalDisplayStatsService);
        mExternalDisplayPolicy = new ExternalDisplayPolicy(new ExternalDisplayPolicyInjector());
        if (mFlags.isDisplayTopologyEnabled()) {
            mDisplayTopologyCoordinator = new DisplayTopologyCoordinator();
            mDisplayTopologyCoordinator =
                    new DisplayTopologyCoordinator(this::isExtendedDisplayEnabled);
        } else {
            mDisplayTopologyCoordinator = null;
        }
@@ -2153,6 +2156,17 @@ public final class DisplayManagerService extends SystemService {
        updateLogicalDisplayState(display);
    }

    private boolean isExtendedDisplayEnabled() {
        try {
            return 0 != Settings.Global.getInt(
                    mContext.getContentResolver(),
                    DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0);
        } catch (Throwable e) {
            // Some services might not be initialised yet to be able to call getInt
            return false;
        }
    }

    @SuppressLint("AndroidFrameworkRequiresPermission")
    private void handleLogicalDisplayAddedLocked(LogicalDisplay display) {
        final int displayId = display.getDisplayIdLocked();
+201 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.server.display;

import android.annotation.Nullable;
import android.util.IndentingPrintWriter;
import android.util.Pair;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Represents the relative placement of extended displays.
 */
class DisplayTopology {
    private static final String TAG = "DisplayTopology";

    /**
     * The topology tree
     */
    @Nullable
    @VisibleForTesting
    TreeNode mRoot;

    /**
     * The logical display ID of the primary display that will show certain UI elements.
     * This is not necessarily the same as the default display.
     */
    @VisibleForTesting
    int mPrimaryDisplayId;

    /**
     * Add a display to the topology.
     * If this is the second display in the topology, it will be placed above the first display.
     * Subsequent displays will be places to the left or right of the second display.
     * @param displayId The ID of the display
     * @param width The width of the display
     * @param height The height of the display
     */
    void addDisplay(int displayId, double width, double height) {
        if (mRoot == null) {
            mRoot = new TreeNode(displayId, width, height, /* position= */ null, /* offset= */ 0);
            mPrimaryDisplayId = displayId;
            Slog.i(TAG, "First display added: " + mRoot);
        } else if (mRoot.mChildren.isEmpty()) {
            // This is the 2nd display. Align the middles of the top and bottom edges.
            double offset = mRoot.mWidth / 2 - width / 2;
            TreeNode display = new TreeNode(displayId, width, height,
                    TreeNode.Position.POSITION_TOP, offset);
            mRoot.mChildren.add(display);
            Slog.i(TAG, "Second display added: " + display + ", parent ID: " + mRoot.mDisplayId);
        } else {
            TreeNode rightMostDisplay = findRightMostDisplay(mRoot, mRoot.mWidth).first;
            TreeNode newDisplay = new TreeNode(displayId, width, height,
                    TreeNode.Position.POSITION_RIGHT, /* offset= */ 0);
            rightMostDisplay.mChildren.add(newDisplay);
            Slog.i(TAG, "Display added: " + newDisplay + ", parent ID: "
                    + rightMostDisplay.mDisplayId);
        }
    }

    /**
     * Print the object's state and debug information into the given stream.
     * @param pw The stream to dump information to.
     */
    void dump(PrintWriter pw) {
        pw.println("DisplayTopology:");
        pw.println("--------------------");
        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
        ipw.increaseIndent();

        ipw.println("mPrimaryDisplayId: " + mPrimaryDisplayId);

        ipw.println("Topology tree:");
        if (mRoot != null) {
            ipw.increaseIndent();
            mRoot.dump(ipw);
            ipw.decreaseIndent();
        }
    }

    /**
     * @param display The display from which the search should start.
     * @param xPos The x position of the right edge of that display.
     * @return The display that is the furthest to the right and the x position of the right edge
     * of that display
     */
    private Pair<TreeNode, Double> findRightMostDisplay(TreeNode display, double xPos) {
        Pair<TreeNode, Double> result = new Pair<>(display, xPos);
        for (TreeNode child : display.mChildren) {
            // The x position of the right edge of the child
            double childXPos;
            switch (child.mPosition) {
                case POSITION_LEFT -> childXPos = xPos - display.mWidth;
                case POSITION_TOP, POSITION_BOTTOM ->
                        childXPos = xPos - display.mWidth + child.mOffset + child.mWidth;
                case POSITION_RIGHT -> childXPos = xPos + child.mWidth;
                default -> throw new IllegalStateException("Unexpected value: " + child.mPosition);
            }

            // Recursive call - find the rightmost display starting from the child
            Pair<TreeNode, Double> childResult = findRightMostDisplay(child, childXPos);
            // Check if the one found is further right
            if (childResult.second > result.second) {
                result = new Pair<>(childResult.first, childResult.second);
            }
        }
        return result;
    }

    @VisibleForTesting
    static class TreeNode {

        /**
         * The logical display ID
         */
        @VisibleForTesting
        final int mDisplayId;

        /**
         * The width of the display in density-independent pixels (dp).
         */
        @VisibleForTesting
        double mWidth;

        /**
         * The height of the display in density-independent pixels (dp).
         */
        @VisibleForTesting
        double mHeight;

        /**
         * The position of this display relative to its parent.
         */
        @VisibleForTesting
        Position mPosition;

        /**
         * The distance from the top edge of the parent display to the top edge of this display (in
         * case of POSITION_LEFT or POSITION_RIGHT) or from the left edge of the parent display
         * to the left edge of this display (in case of POSITION_TOP or POSITION_BOTTOM). The unit
         * used is density-independent pixels (dp).
         */
        @VisibleForTesting
        double mOffset;

        @VisibleForTesting
        final List<TreeNode> mChildren = new ArrayList<>();

        TreeNode(int displayId, double width, double height, Position position,
                double offset) {
            mDisplayId = displayId;
            mWidth = width;
            mHeight = height;
            mPosition = position;
            mOffset = offset;
        }

        /**
         * Print the object's state and debug information into the given stream.
         * @param ipw The stream to dump information to.
         */
        void dump(IndentingPrintWriter ipw) {
            ipw.println(this);
            ipw.increaseIndent();
            for (TreeNode child : mChildren) {
                child.dump(ipw);
            }
            ipw.decreaseIndent();
        }

        @Override
        public String toString() {
            return "Display {id=" + mDisplayId + ", width=" + mWidth + ", height=" + mHeight
                    + ", position=" + mPosition + ", offset=" + mOffset + "}";
        }

        @VisibleForTesting
        enum Position {
            POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM
        }
    }
}
+64 −62
Original line number Diff line number Diff line
@@ -16,89 +16,91 @@

package com.android.server.display;

import android.annotation.Nullable;
import android.util.IndentingPrintWriter;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.DisplayInfo;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;

/**
 * This class manages the relative placement (topology) of extended displays. It is responsible for
 * updating and persisting the topology.
 * Manages the relative placement (topology) of extended displays. Responsible for updating and
 * persisting the topology.
 */
class DisplayTopologyCoordinator {

    /**
     * The topology tree
     */
    @Nullable
    private TopologyTreeNode mRoot;
    @GuardedBy("mLock")
    private final DisplayTopology mTopology;

    /**
     * The logical display ID of the primary display that will show certain UI elements.
     * This is not necessarily the same as the default display.
     * Check if extended displays are enabled. If not, a topology is not needed.
     */
    private int mPrimaryDisplayId;
    private final BooleanSupplier mIsExtendedDisplayEnabled;

    /**
     * Print the object's state and debug information into the given stream.
     * @param pw The stream to dump information to.
     */
    public void dump(PrintWriter pw) {
        pw.println("DisplayTopologyCoordinator:");
        pw.println("--------------------");
        IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
        ipw.increaseIndent();

        ipw.println("mPrimaryDisplayId: " + mPrimaryDisplayId);

        ipw.println("Topology tree:");
        if (mRoot != null) {
            ipw.increaseIndent();
            mRoot.dump(ipw);
            ipw.decreaseIndent();
        }
    private final Object mLock = new Object();

    DisplayTopologyCoordinator(BooleanSupplier isExtendedDisplayEnabled) {
        this(new Injector(), isExtendedDisplayEnabled);
    }

    private static class TopologyTreeNode {
    @VisibleForTesting
    DisplayTopologyCoordinator(Injector injector, BooleanSupplier isExtendedDisplayEnabled) {
        mTopology = injector.getTopology();
        mIsExtendedDisplayEnabled = isExtendedDisplayEnabled;
    }

    /**
         * The logical display ID
     * Add a display to the topology.
     * @param info The display info
     */
        private int mDisplayId;

        private final List<TopologyTreeNode> mChildren = new ArrayList<>();
    void onDisplayAdded(DisplayInfo info) {
        if (!isDisplayAllowedInTopology(info)) {
            return;
        }
        synchronized (mLock) {
            mTopology.addDisplay(info.displayId, getWidth(info), getHeight(info));
        }
    }

    /**
         * The position of this display relative to its parent.
     * Print the object's state and debug information into the given stream.
     * @param pw The stream to dump information to.
     */
        private Position mPosition;
    void dump(PrintWriter pw) {
        synchronized (mLock) {
            mTopology.dump(pw);
        }
    }

    /**
         * The distance from the top edge of the parent display to the top edge of this display (in
         * case of POSITION_LEFT or POSITION_RIGHT) or from the left edge of the parent display
         * to the left edge of this display (in case of POSITION_TOP or POSITION_BOTTOM). The unit
         * used is density-independent pixels (dp).
     * @param info The display info
     * @return The width of the display in dp
     */
        private double mOffset;
    private double getWidth(DisplayInfo info) {
        return info.logicalWidth * (double) DisplayMetrics.DENSITY_DEFAULT
                / info.logicalDensityDpi;
    }

    /**
         * Print the object's state and debug information into the given stream.
         * @param ipw The stream to dump information to.
     * @param info The display info
     * @return The height of the display in dp
     */
        void dump(IndentingPrintWriter ipw) {
            ipw.println("Display {id=" + mDisplayId + ", position=" + mPosition
                    + ", offset=" + mOffset + "}");
            ipw.increaseIndent();
            for (TopologyTreeNode child : mChildren) {
                child.dump(ipw);
    private double getHeight(DisplayInfo info) {
        return info.logicalHeight * (double) DisplayMetrics.DENSITY_DEFAULT
                / info.logicalDensityDpi;
    }
            ipw.decreaseIndent();

    private boolean isDisplayAllowedInTopology(DisplayInfo info) {
        return mIsExtendedDisplayEnabled.getAsBoolean()
                && info.displayGroupId == Display.DEFAULT_DISPLAY_GROUP;
    }

        private enum Position {
            POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM
    static class Injector {
        DisplayTopology getTopology() {
            return new DisplayTopology();
        }
    }
}
+9 −13
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.server.display;
import static android.media.AudioDeviceInfo.TYPE_HDMI;
import static android.media.AudioDeviceInfo.TYPE_HDMI_ARC;
import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE;
import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;

import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -32,7 +31,6 @@ import android.media.AudioManager.AudioPlaybackCallback;
import android.media.AudioPlaybackConfiguration;
import android.os.Handler;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Slog;
import android.util.SparseIntArray;
import android.view.Display;
@@ -44,6 +42,7 @@ import com.android.internal.util.FrameworkStatsLog;
import com.android.server.display.utils.DebugUtils;

import java.util.List;
import java.util.function.BooleanSupplier;


/**
@@ -203,8 +202,9 @@ public final class ExternalDisplayStatsService {
        }
    };

    ExternalDisplayStatsService(Context context, Handler handler) {
        this(new Injector(context, handler));
    ExternalDisplayStatsService(Context context, Handler handler,
            BooleanSupplier isExtendedDisplayEnabled) {
        this(new Injector(context, handler, isExtendedDisplayEnabled));
    }

    @VisibleForTesting
@@ -599,25 +599,21 @@ public final class ExternalDisplayStatsService {
        private final Context mContext;
        @NonNull
        private final Handler mHandler;
        private final BooleanSupplier mIsExtendedDisplayEnabled;
        @Nullable
        private AudioManager mAudioManager;
        @Nullable
        private PowerManager mPowerManager;

        Injector(@NonNull Context context, @NonNull Handler handler) {
        Injector(@NonNull Context context, @NonNull Handler handler,
                BooleanSupplier isExtendedDisplayEnabled) {
            mContext = context;
            mHandler = handler;
            mIsExtendedDisplayEnabled = isExtendedDisplayEnabled;
        }

        boolean isExtendedDisplayEnabled() {
            try {
                return 0 != Settings.Global.getInt(
                        mContext.getContentResolver(),
                        DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0);
            } catch (Throwable e) {
                // Some services might not be initialised yet to be able to call getInt
                return false;
            }
            return mIsExtendedDisplayEnabled.getAsBoolean();
        }

        void registerInteractivityReceiver(BroadcastReceiver interactivityReceiver,
+83 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.server.display

import android.util.DisplayMetrics
import android.view.Display
import android.view.DisplayInfo
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyDouble
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.util.function.BooleanSupplier

class DisplayTopologyCoordinatorTest {
    private lateinit var coordinator: DisplayTopologyCoordinator
    private val displayInfo = DisplayInfo()

    private val mockTopology = mock<DisplayTopology>()
    private val mockIsExtendedDisplayEnabled = mock<BooleanSupplier>()

    @Before
    fun setUp() {
        displayInfo.displayId = 2
        displayInfo.logicalWidth = 300
        displayInfo.logicalHeight = 200
        displayInfo.logicalDensityDpi = 100

        val injector = object : DisplayTopologyCoordinator.Injector() {
            override fun getTopology() = mockTopology
        }
        coordinator = DisplayTopologyCoordinator(injector, mockIsExtendedDisplayEnabled)
    }

    @Test
    fun addDisplay() {
        whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(true)

        coordinator.onDisplayAdded(displayInfo)

        val widthDp = displayInfo.logicalWidth * (DisplayMetrics.DENSITY_DEFAULT.toDouble()
                / displayInfo.logicalDensityDpi)
        val heightDp = displayInfo.logicalHeight * (DisplayMetrics.DENSITY_DEFAULT.toDouble()
                / displayInfo.logicalDensityDpi)
        verify(mockTopology).addDisplay(displayInfo.displayId, widthDp, heightDp)
    }

    @Test
    fun addDisplay_extendedDisplaysDisabled() {
        whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(false)

        coordinator.onDisplayAdded(displayInfo)

        verify(mockTopology, never()).addDisplay(anyInt(), anyDouble(), anyDouble())
    }

    @Test
    fun addDisplay_notInDefaultDisplayGroup() {
        whenever(mockIsExtendedDisplayEnabled.asBoolean).thenReturn(true)
        displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP + 1

        coordinator.onDisplayAdded(displayInfo)

        verify(mockTopology, never()).addDisplay(anyInt(), anyDouble(), anyDouble())
    }
}
 No newline at end of file
Loading