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

Commit dcb7cdae authored by Piotr Wilczyński's avatar Piotr Wilczyński Committed by Android (Google) Code Review
Browse files

Merge "Default display topology initialization" into main

parents c0baa675 d59f5e46
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;
@@ -655,12 +656,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;
        }
@@ -2262,6 +2265,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