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

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

Update topology when display removed

When a display is removed, the topology is re-created by re-adding the remaining displays to it.

Bug: 370015984
Flag: com.android.server.display.feature.flags.display_topology
Test: DisplayManagerServiceTest, DisplayTopologyCoordinatorTest, DisplayTopologyTest
Change-Id: Ia2760e8187699bbff554c70cf5c811ea35e09e4a
parent 8ab1300f
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -2302,6 +2302,9 @@ public final class DisplayManagerService extends SystemService {
        updateLogicalDisplayState(display);

        mExternalDisplayPolicy.handleLogicalDisplayAddedLocked(display);
        if (mDisplayTopologyCoordinator != null) {
            mDisplayTopologyCoordinator.onDisplayAdded(display.getDisplayInfoLocked());
        }
    }

    private void handleLogicalDisplayChangedLocked(@NonNull LogicalDisplay display) {
@@ -2389,6 +2392,9 @@ public final class DisplayManagerService extends SystemService {
        } else {
            releaseDisplayAndEmitEvent(display, DisplayManagerGlobal.EVENT_DISPLAY_REMOVED);
        }
        if (mDisplayTopologyCoordinator != null) {
            mDisplayTopologyCoordinator.onDisplayRemoved(display.getDisplayIdLocked());
        }

        Slog.i(TAG, "Logical display removed: " + display.getDisplayIdLocked());
    }
+81 −19
Original line number Diff line number Diff line
@@ -20,12 +20,15 @@ import android.annotation.Nullable;
import android.util.IndentingPrintWriter;
import android.util.Pair;
import android.util.Slog;
import android.view.Display;

import com.android.internal.annotations.VisibleForTesting;

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

/**
 * Represents the relative placement of extended displays.
@@ -45,35 +48,50 @@ class DisplayTopology {
     * This is not necessarily the same as the default display.
     */
    @VisibleForTesting
    int mPrimaryDisplayId;
    int mPrimaryDisplayId = Display.INVALID_DISPLAY;

    /**
     * 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 displayId The logical display ID
     * @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);
        addDisplay(displayId, width, height, /* shouldLog= */ true);
    }

    /**
     * Remove a display from the topology.
     * The default topology is created from the remaining displays, as if they were reconnected
     * one by one.
     * @param displayId The logical display ID
     */
    void removeDisplay(int displayId) {
        if (!isDisplayPresent(displayId, mRoot)) {
            return;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(mRoot);
        mRoot = null;
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node.mDisplayId != displayId) {
                addDisplay(node.mDisplayId, node.mWidth, node.mHeight, /* shouldLog= */ false);
            }
            queue.addAll(node.mChildren);
        }
        if (mPrimaryDisplayId == displayId) {
            if (mRoot != null) {
                mPrimaryDisplayId = 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);
                mPrimaryDisplayId = Display.INVALID_DISPLAY;
            }
            Slog.i(TAG,  "Primary display with ID " + displayId
                    + " removed, new primary display: " + mPrimaryDisplayId);
        } else {
            Slog.i(TAG, "Display with ID " + displayId + " removed");
        }
    }

@@ -97,6 +115,35 @@ class DisplayTopology {
        }
    }

    private void addDisplay(int displayId, double width, double height, boolean shouldLog) {
        if (mRoot == null) {
            mRoot = new TreeNode(displayId, width, height, /* position= */ null, /* offset= */ 0);
            mPrimaryDisplayId = displayId;
            if (shouldLog) {
                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);
            if (shouldLog) {
                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);
            if (shouldLog) {
                Slog.i(TAG, "Display added: " + newDisplay + ", parent ID: "
                        + rightMostDisplay.mDisplayId);
            }
        }
    }

    /**
     * @param display The display from which the search should start.
     * @param xPos The x position of the right edge of that display.
@@ -126,6 +173,21 @@ class DisplayTopology {
        return result;
    }

    private boolean isDisplayPresent(int displayId, TreeNode node) {
        if (node == null) {
            return false;
        }
        if (node.mDisplayId == displayId) {
            return true;
        }
        for (TreeNode child : node.mChildren) {
            if (isDisplayPresent(displayId, child)) {
                return true;
            }
        }
        return false;
    }

    @VisibleForTesting
    static class TreeNode {

+10 −0
Original line number Diff line number Diff line
@@ -65,6 +65,16 @@ class DisplayTopologyCoordinator {
        }
    }

    /**
     * Remove a display from the topology.
     * @param displayId The logical display ID
     */
    void onDisplayRemoved(int displayId) {
        synchronized (mLock) {
            mTopology.removeDisplay(displayId);
        }
    }

    /**
     * Print the object's state and debug information into the given stream.
     * @param pw The stream to dump information to.
+130 −3
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.server.display

import android.view.Display
import com.google.common.truth.Truth.assertThat
import org.junit.Test

@@ -23,7 +24,7 @@ class DisplayTopologyTest {
    private val topology = DisplayTopology()

    @Test
    fun oneDisplay() {
    fun addOneDisplay() {
        val displayId = 1
        val width = 800.0
        val height = 600.0
@@ -40,7 +41,7 @@ class DisplayTopologyTest {
    }

    @Test
    fun twoDisplays() {
    fun addTwoDisplays() {
        val displayId1 = 1
        val width1 = 800.0
        val height1 = 600.0
@@ -71,7 +72,7 @@ class DisplayTopologyTest {
    }

    @Test
    fun manyDisplays() {
    fun addManyDisplays() {
        val displayId1 = 1
        val width1 = 800.0
        val height1 = 600.0
@@ -118,4 +119,130 @@ class DisplayTopologyTest {
            assertThat(display.mOffset).isEqualTo(0)
        }
    }

    @Test
    fun removeDisplays() {
        val displayId1 = 1
        val width1 = 800.0
        val height1 = 600.0

        val displayId2 = 2
        val width2 = 1000.0
        val height2 = 1500.0

        topology.addDisplay(displayId1, width1, height1)
        topology.addDisplay(displayId2, width2, height2)

        val noOfDisplays = 30
        for (i in 3..noOfDisplays) {
            topology.addDisplay(/* displayId= */ i, width1, height1)
        }

        var removedDisplays = arrayOf(20)
        topology.removeDisplay(20)

        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)

        var display1 = topology.mRoot!!
        assertThat(display1.mDisplayId).isEqualTo(displayId1)
        assertThat(display1.mWidth).isEqualTo(width1)
        assertThat(display1.mHeight).isEqualTo(height1)
        assertThat(display1.mChildren).hasSize(1)

        var display2 = display1.mChildren[0]
        assertThat(display2.mDisplayId).isEqualTo(displayId2)
        assertThat(display2.mWidth).isEqualTo(width2)
        assertThat(display2.mHeight).isEqualTo(height2)
        assertThat(display2.mChildren).hasSize(1)
        assertThat(display2.mPosition).isEqualTo(
            DisplayTopology.TreeNode.Position.POSITION_TOP)
        assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2)

        var display = display2
        for (i in 3..noOfDisplays) {
            if (i in removedDisplays) {
                continue
            }
            display = display.mChildren[0]
            assertThat(display.mDisplayId).isEqualTo(i)
            assertThat(display.mWidth).isEqualTo(width1)
            assertThat(display.mHeight).isEqualTo(height1)
            // The last display should have no children
            assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0)
            assertThat(display.mPosition).isEqualTo(
                DisplayTopology.TreeNode.Position.POSITION_RIGHT)
            assertThat(display.mOffset).isEqualTo(0)
        }

        topology.removeDisplay(22)
        removedDisplays += 22
        topology.removeDisplay(23)
        removedDisplays += 23
        topology.removeDisplay(25)
        removedDisplays += 25

        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1)

        display1 = topology.mRoot!!
        assertThat(display1.mDisplayId).isEqualTo(displayId1)
        assertThat(display1.mWidth).isEqualTo(width1)
        assertThat(display1.mHeight).isEqualTo(height1)
        assertThat(display1.mChildren).hasSize(1)

        display2 = display1.mChildren[0]
        assertThat(display2.mDisplayId).isEqualTo(displayId2)
        assertThat(display2.mWidth).isEqualTo(width2)
        assertThat(display2.mHeight).isEqualTo(height2)
        assertThat(display2.mChildren).hasSize(1)
        assertThat(display2.mPosition).isEqualTo(
            DisplayTopology.TreeNode.Position.POSITION_TOP)
        assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2)

        display = display2
        for (i in 3..noOfDisplays) {
            if (i in removedDisplays) {
                continue
            }
            display = display.mChildren[0]
            assertThat(display.mDisplayId).isEqualTo(i)
            assertThat(display.mWidth).isEqualTo(width1)
            assertThat(display.mHeight).isEqualTo(height1)
            // The last display should have no children
            assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0)
            assertThat(display.mPosition).isEqualTo(
                DisplayTopology.TreeNode.Position.POSITION_RIGHT)
            assertThat(display.mOffset).isEqualTo(0)
        }
    }

    @Test
    fun removeAllDisplays() {
        val displayId = 1
        val width = 800.0
        val height = 600.0

        topology.addDisplay(displayId, width, height)
        topology.removeDisplay(displayId)

        assertThat(topology.mPrimaryDisplayId).isEqualTo(Display.INVALID_DISPLAY)
        assertThat(topology.mRoot).isNull()
    }

    @Test
    fun removeDisplayThatDoesNotExist() {
        val displayId = 1
        val width = 800.0
        val height = 600.0

        topology.addDisplay(displayId, width, height)
        topology.removeDisplay(3)

        assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId)

        val display = topology.mRoot!!
        assertThat(display.mDisplayId).isEqualTo(displayId)
        assertThat(display.mWidth).isEqualTo(width)
        assertThat(display.mHeight).isEqualTo(height)
        assertThat(display.mChildren).isEmpty()
    }
}
 No newline at end of file