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

Commit daeb8ec6 authored by Matthew DeVore's avatar Matthew DeVore
Browse files

Topology conversion to tree

This will allow the Settings App to change the absolute coordinates of
each display in the topology to a tree, which is the data structure used
by the DisplayManager API.

Flag: com.android.server.display.feature.flags.display_topology
Test: atest DisplayTopologyTest.kt
Bug: b/352648432
Change-Id: Ia4b71d6dc4d6bc4f709e98f15949beaeeaec15d6
parent 113037ed
Loading
Loading
Loading
Loading
+147 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import static android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Parcel;
import android.os.Parcelable;
@@ -150,6 +151,138 @@ public final class DisplayTopology implements Parcelable {
        }
    }

    /**
     * Rearranges the topology toward the positions given for each display. The width and height of
     * each display, as well as the primary display, are not changed by this call.
     * <p>
     * Upon returning, the topology will be valid and normalized with each display as close to the
     * requested positions as possible.
     *
     * @param newPos the desired positions (upper-left corner) of each display. The keys in the map
     *               are the display IDs.
     * @throws IllegalArgumentException if the keys in {@code positions} are not the exact display
     *                                  IDs in this topology, no more, no less
     */
    public void rearrange(Map<Integer, PointF> newPos) {
        var availableParents = new ArrayList<TreeNode>();

        availableParents.addLast(mRoot);

        var needsParent = allNodesIdMap();

        // In the case of missing items, if this check doesn't detect it, a NPE will be thrown
        // later.
        if (needsParent.size() != newPos.size()) {
            throw new IllegalArgumentException("newPos has wrong number of entries: " + newPos);
        }

        mRoot.mChildren.clear();
        for (TreeNode n : needsParent.values()) {
            n.mChildren.clear();
        }

        needsParent.remove(mRoot.mDisplayId);
        // Start with a root island and add children to it one-by-one until the island consists of
        // all the displays. The root island begins with only the root node, which has no
        // parent. Then we greedily choose an optimal pairing of two nodes, consisting of a node
        // from the island and a node not yet in the island. This is repeating until all nodes are
        // in the island.
        //
        // The optimal pair is the pair which has the smallest deviation. The deviation consists of
        // an x-axis component and a y-axis component, called xDeviation and yDeviation.
        //
        // The deviations are like distances but a little different. They are calculated in two
        // steps. The first step calculates both axes in a similar way. The next step compares the
        // two values and chooses which axis to attach along. Depending on which axis is chosen,
        // the deviation for one axis is updated. See below for details.
        while (!needsParent.isEmpty()) {
            double bestDist = Double.POSITIVE_INFINITY;
            TreeNode bestChild = null, bestParent = null;

            for (var child : needsParent.values()) {
                PointF childPos = newPos.get(child.mDisplayId);
                float childRight = childPos.x + child.getWidth();
                float childBottom = childPos.y + child.getHeight();
                for (var parent : availableParents) {
                    PointF parentPos = newPos.get(parent.mDisplayId);
                    float parentRight = parentPos.x + parent.getWidth();
                    float parentBottom = parentPos.y + parent.getHeight();

                    // This is the smaller of the two ranges minus the amount of overlap shared
                    // between them. The "amount of overlap" is negative if there is no overlap, but
                    // this does not make a parenting ineligible, because we allow for attaching at
                    // the corner and for floating point error. The overlap is more negative the
                    // farther apart the closest corner pair is.
                    //
                    // For each axis, this calculates (SmallerRange - Overlap). If one range lies
                    // completely in the other (or they are equal), the axis' deviation will be
                    // zero.
                    //
                    // The "SmallerRange," which refers to smaller of the widths of the two rects,
                    // or smaller of the heights of the two rects, is added to the deviation so that
                    // a maximum overlap results in a deviation of zero.
                    float xSmallerRange = Math.min(child.getWidth(), parent.getWidth());
                    float ySmallerRange = Math.min(child.getHeight(), parent.getHeight());
                    float xOverlap
                            = Math.min(parentRight, childRight)
                            - Math.max(parentPos.x, childPos.x);
                    float yOverlap
                            = Math.min(parentBottom, childBottom)
                            - Math.max(parentPos.y, childPos.y);
                    float xDeviation = xSmallerRange - xOverlap;
                    float yDeviation = ySmallerRange - yOverlap;

                    float offset;
                    int pos;
                    if (xDeviation <= yDeviation) {
                        if (childPos.y < parentPos.y) {
                            yDeviation = childBottom - parentPos.y;
                            pos = POSITION_TOP;
                        } else {
                            yDeviation = parentBottom - childPos.y;
                            pos = POSITION_BOTTOM;
                        }
                        offset = childPos.x - parentPos.x;
                    } else {
                        if (childPos.x < parentPos.x) {
                            xDeviation = childRight - parentPos.x;
                            pos = POSITION_LEFT;
                        } else {
                            xDeviation = parentRight - childPos.x;
                            pos = POSITION_RIGHT;
                        }
                        offset = childPos.y - parentPos.y;
                    }

                    double dist = Math.hypot(xDeviation, yDeviation);
                    if (dist >= bestDist) {
                        continue;
                    }

                    bestDist = dist;
                    bestChild = child;
                    bestParent = parent;
                    // Eagerly update the child's parenting info, even though we may not use it, in
                    // which case it will be overwritten later.
                    bestChild.mPosition = pos;
                    bestChild.mOffset = offset;
                }
            }

            assert bestParent != null & bestChild != null;

            bestParent.addChild(bestChild);
            if (null == needsParent.remove(bestChild.mDisplayId)) {
                throw new IllegalStateException("child not in pending set! " + bestChild);
            }
            availableParents.add(bestChild);
        }

        // The conversion may have introduced an intersection of two display rects. If they are
        // bigger than our error tolerance, this function will remove them.
        normalize();
    }

    @Override
    public int describeContents() {
        return 0;
@@ -450,6 +583,20 @@ public final class DisplayTopology implements Parcelable {
        return a == b || (Float.isNaN(a) && Float.isNaN(b)) || Math.abs(a - b) < EPSILON;
    }

    private Map<Integer, TreeNode> allNodesIdMap() {
        var pend = new ArrayDeque<TreeNode>();
        var found = new HashMap<Integer, TreeNode>();

        pend.push(mRoot);
        do {
            TreeNode node = pend.pop();
            found.put(node.mDisplayId, node);
            pend.addAll(node.mChildren);
        } while (!pend.isEmpty());

        return found;
    }

    public static final class TreeNode implements Parcelable {
        public static final int POSITION_LEFT = 0;
        public static final int POSITION_TOP = 1;
+205 −1
Original line number Diff line number Diff line
@@ -16,7 +16,10 @@

package android.hardware.display

import android.graphics.PointF
import android.graphics.RectF
import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM
import android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT
import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP
import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT
import android.view.Display
@@ -469,4 +472,205 @@ class DisplayTopologyTest {
        assertThat(actualDisplay4.offset).isEqualTo(-400f)
        assertThat(actualDisplay4.children).isEmpty()
    }

    @Test
    fun rearrange_twoDisplays() {
        val nodes = rearrangeRects(
            // Arrange in staggered manner, connected vertically.
            RectF(100f, 100f, 250f, 200f),
            RectF(150f, 200f, 300f, 300f),
        )

        assertThat(nodes[0].children).containsExactly(nodes[1])
        assertThat(nodes[1].children).isEmpty()
        assertPositioning(nodes, Pair(POSITION_BOTTOM, 50f))
    }

    @Test
    fun rearrange_reverseOrderOfSeveralDisplays() {
        val nodes = rearrangeRects(
            RectF(0f, 0f, 150f, 100f),
            RectF(-150f, 0f, 0f, 100f),
            RectF(-300f, 0f, -150f, 100f),
            RectF(-450f, 0f, -300f, 100f),
        )

        assertPositioning(
            nodes,
            Pair(POSITION_LEFT, 0f),
            Pair(POSITION_LEFT, 0f),
            Pair(POSITION_LEFT, 0f),
        )

        assertThat(nodes[0].children).containsExactly(nodes[1])
        assertThat(nodes[1].children).containsExactly(nodes[2])
        assertThat(nodes[2].children).containsExactly(nodes[3])
        assertThat(nodes[3].children).isEmpty()
    }

    @Test
    fun rearrange_crossWithRootInCenter() {
        val nodes = rearrangeRects(
            RectF(0f, 0f, 150f, 100f),
            RectF(-150f, 0f, 0f, 100f),
            RectF(0f,-100f, 150f, 0f),
            RectF(150f, 0f, 300f, 100f),
            RectF(0f, 100f, 150f, 200f),
        )

        assertPositioning(
            nodes,
            Pair(POSITION_LEFT, 0f),
            Pair(POSITION_TOP, 0f),
            Pair(POSITION_RIGHT, 0f),
            Pair(POSITION_BOTTOM, 0f),
        )

        assertThat(nodes[0].children)
            .containsExactly(nodes[1], nodes[2], nodes[3], nodes[4])
    }

    @Test
    fun rearrange_elbowArrangementDoesNotUseCornerAdjacency1() {
        val nodes = rearrangeRects(
            //     2
            //     |
            // 0 - 1

            RectF(0f, 0f, 100f, 100f),
            RectF(100f, 0f, 200f, 100f),
            RectF(100f, -100f, 200f, 0f),
        )

        assertThat(nodes[0].children).containsExactly(nodes[1])
        assertThat(nodes[1].children).containsExactly(nodes[2])
        assertThat(nodes[2].children).isEmpty()

        assertPositioning(
            nodes,
            Pair(POSITION_RIGHT, 0f),
            Pair(POSITION_TOP, 0f),
        )
    }

    @Test
    fun rearrange_elbowArrangementDoesNotUseCornerAdjacency2() {
        val nodes = rearrangeRects(
            //     0
            //     |
            //     1
            //     |
            // 3 - 2

            RectF(0f, 0f, 100f, 100f),
            RectF(0f, 100f, 100f, 200f),
            RectF(0f, 200f, 100f, 300f),
            RectF(-100f, 200f, 0f, 300f),
        )

        assertThat(nodes[0].children).containsExactly(nodes[1])
        assertThat(nodes[1].children).containsExactly(nodes[2])
        assertThat(nodes[2].children).containsExactly(nodes[3])
        assertThat(nodes[3].children).isEmpty()

        assertPositioning(
            nodes,
            Pair(POSITION_BOTTOM, 0f),
            Pair(POSITION_BOTTOM, 0f),
            Pair(POSITION_LEFT, 0f),
        )
    }

    @Test
    fun rearrange_useLargerEdge() {
        val nodes = rearrangeRects(
            //444111
            //444111
            //444111
            //  000222
            //  000222
            //  000222
            //    333
            //    333
            //    333
            RectF(20f, 30f, 50f, 60f),
            RectF(30f, 0f, 60f, 30f),
            RectF(50f, 30f, 80f, 60f),
            RectF(40f, 60f, 70f, 90f),
            RectF(0f, 0f, 30f, 30f),
        )

        assertPositioning(
            nodes,
            Pair(POSITION_TOP, 10f),
            Pair(POSITION_RIGHT, 0f),
            Pair(POSITION_BOTTOM, -10f),
            Pair(POSITION_LEFT, 0f),
        )

        assertThat(nodes[0].children).containsExactly(nodes[1], nodes[2])
        assertThat(nodes[1].children).containsExactly(nodes[4])
        assertThat(nodes[2].children).containsExactly(nodes[3])
        (3..4).forEach { assertThat(nodes[it].children).isEmpty() }
    }

    @Test
    fun rearrange_closeGaps() {
        val nodes = rearrangeRects(
            //000
            //000 111
            //000 111
            //    111
            //
            //        222
            //        222
            //        222
            RectF(0f, 0f, 30f, 30f),
            RectF(40f, 10f, 70f, 40f),
            RectF(80.5f, 50f, 110f, 80f),  // left+=0.5 to cause a preference for TOP/BOTTOM attach
        )

        assertPositioning(
            nodes,
            // In the case of corner adjacency, we prefer a left/right attachment.
            Pair(POSITION_RIGHT, 10f),
            Pair(POSITION_BOTTOM, 40.5f),  // TODO: fix implementation to remove this gap
        )

        assertThat(nodes[0].children).containsExactly(nodes[1])
        assertThat(nodes[1].children).containsExactly(nodes[2])
        assertThat(nodes[2].children).isEmpty()
    }

    /**
     * Runs the rearrange algorithm and returns the resulting tree as a list of nodes, with the
     * root at index 0. The number of nodes is inferred from the number of positions passed.
     */
    private fun rearrangeRects(vararg pos : RectF) : List<DisplayTopology.TreeNode> {
        // Generates a linear sequence of nodes in order in the List from root to leaf,
        // left-to-right. IDs are ascending from 0 to count - 1.

        val nodes = pos.indices.map {
            DisplayTopology.TreeNode(it, pos[it].width(), pos[it].height(), POSITION_RIGHT, 0f)
        }

        nodes.forEachIndexed { id, node ->
            if (id > 0) {
                nodes[id - 1].addChild(node)
            }
        }

        DisplayTopology(nodes[0], 0).rearrange(pos.indices.associateWith {
            PointF(pos[it].left, pos[it].top)
        })

        return nodes
    }

    private fun assertPositioning(
            nodes : List<DisplayTopology.TreeNode>, vararg positions : Pair<Int, Float>) {
        assertThat(nodes.drop(1).map { Pair(it.position, it.offset )})
            .containsExactly(*positions)
            .inOrder()
    }
}