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

Commit 3d0f6d2a authored by Robert Horvath's avatar Robert Horvath
Browse files

Improve handling of TV PiPs bigger than movement bounds

For very tall/wide expanded PiPs, the positioning algorithm considered
the anchor position as invalid, as it crossed the movement bounds.
It assumed that if no free valid position is found the PiP must be
stashed and there exist keep clear areas overlapping the unstash
position that caused that stashing. But in the case of these big PiPs
there were no overlapping keep clear areas, resulting in a NPE.

This change improves handling of oversize PiPs, allowing the anchor
position to cross movement bounds without stashing, and improves null
safety when searching for the stash position.

Bug: 228948019
Test: atest TvPipKeepClearAlgorithmTest
Change-Id: I6a7c6c316d03ca8902b9cfb1e0c4abcb873eff18
parent 6474fe47
Loading
Loading
Loading
Loading
+71 −53
Original line number Diff line number Diff line
@@ -150,7 +150,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
        return Placement(
            pipBounds,
            anchorBounds,
            getStashType(pipBounds, movementBounds),
            getStashType(pipBounds, unstashedDestBounds),
            unstashedDestBounds,
            result.unstashTime
        )
@@ -185,7 +185,10 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
        restrictedAreas: Set<Rect>,
        unrestrictedAreas: Set<Rect>
    ): Placement {
        if (restrictedAreas.isEmpty() && unrestrictedAreas.isEmpty()) {
        // If PiP is not covered by any keep clear areas, we can leave it at the anchor bounds
        val keepClearAreas = restrictedAreas + unrestrictedAreas
        if (keepClearAreas.none { it.intersects(pipAnchorBounds) }) {
            lastAreasOverlappingUnstashPosition = emptySet()
            return Placement(pipAnchorBounds, pipAnchorBounds)
        }

@@ -204,9 +207,8 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
                ?: findFreeMovePosition(pipAnchorBounds, emptySet(), unrestrictedAreas)
                ?: pipAnchorBounds

        val keepClearAreas = restrictedAreas + unrestrictedAreas
        val areasOverlappingUnstashPosition =
            keepClearAreas.filter { Rect.intersects(it, unstashBounds) }.toSet()
            keepClearAreas.filterTo(mutableSetOf()) { it.intersects(unstashBounds) }
        val areasOverlappingUnstashPositionChanged =
            !lastAreasOverlappingUnstashPosition.containsAll(areasOverlappingUnstashPosition)
        lastAreasOverlappingUnstashPosition = areasOverlappingUnstashPosition
@@ -228,19 +230,22 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
        return Placement(
            stashedBounds,
            pipAnchorBounds,
            getStashType(stashedBounds, transformedMovementBounds),
            getStashType(stashedBounds, unstashBounds),
            unstashBounds,
            unstashTime
        )
    }

    @PipBoundsState.StashType
    private fun getStashType(stashedBounds: Rect, movementBounds: Rect): Int {
    private fun getStashType(stashedBounds: Rect, unstashedDestBounds: Rect?): Int {
        if (unstashedDestBounds == null) {
            return STASH_TYPE_NONE
        }
        return when {
            stashedBounds.left < movementBounds.left -> STASH_TYPE_LEFT
            stashedBounds.right > movementBounds.right -> STASH_TYPE_RIGHT
            stashedBounds.top < movementBounds.top -> STASH_TYPE_TOP
            stashedBounds.bottom > movementBounds.bottom -> STASH_TYPE_BOTTOM
            stashedBounds.left < unstashedDestBounds.left -> STASH_TYPE_LEFT
            stashedBounds.right > unstashedDestBounds.right -> STASH_TYPE_RIGHT
            stashedBounds.top < unstashedDestBounds.top -> STASH_TYPE_TOP
            stashedBounds.bottom > unstashedDestBounds.bottom -> STASH_TYPE_BOTTOM
            else -> STASH_TYPE_NONE
        }
    }
@@ -368,57 +373,69 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
        val areasOverlappingPipX = keepClearAreas.filter { it.intersectsX(bounds) }
        val areasOverlappingPipY = keepClearAreas.filter { it.intersectsY(bounds) }

        if (areasOverlappingPipX.isNotEmpty()) {
            if (screenBounds.bottom - bounds.bottom <= bounds.top - screenBounds.top) {
                val fullStashTop = screenBounds.bottom - stashOffset

                val maxBottom = areasOverlappingPipX.maxByOrNull { it.bottom }!!.bottom
                val partialStashTop = maxBottom + pipAreaPadding

                val newTop = min(fullStashTop, partialStashTop)
                if (newTop > bounds.top) {
                    val downPosition = Rect(bounds)
            downPosition.offsetTo(bounds.left, min(fullStashTop, partialStashTop))
                    downPosition.offsetTo(bounds.left, newTop)
                    stashCandidates += downPosition
                }
            }
            if (screenBounds.bottom - bounds.bottom >= bounds.top - screenBounds.top) {
                val fullStashBottom = screenBounds.top - bounds.height() + stashOffset

                val minTop = areasOverlappingPipX.minByOrNull { it.top }!!.top
                val partialStashBottom = minTop - bounds.height() - pipAreaPadding

                val newTop = max(fullStashBottom, partialStashBottom)
                if (newTop < bounds.top) {
                    val upPosition = Rect(bounds)
            upPosition.offsetTo(bounds.left, max(fullStashBottom, partialStashBottom))
                    upPosition.offsetTo(bounds.left, newTop)
                    stashCandidates += upPosition
                }
            }
        }

        if (areasOverlappingPipY.isNotEmpty()) {
            if (screenBounds.right - bounds.right <= bounds.left - screenBounds.left) {
                val fullStashRight = screenBounds.right - stashOffset

                val maxRight = areasOverlappingPipY.maxByOrNull { it.right }!!.right
                val partialStashRight = maxRight + pipAreaPadding

                val newLeft = min(fullStashRight, partialStashRight)
                if (newLeft > bounds.left) {
                    val rightPosition = Rect(bounds)
            rightPosition.offsetTo(min(fullStashRight, partialStashRight), bounds.top)
                    rightPosition.offsetTo(newLeft, bounds.top)
                    stashCandidates += rightPosition
                }
            }
            if (screenBounds.right - bounds.right >= bounds.left - screenBounds.left) {
                val fullStashLeft = screenBounds.left - bounds.width() + stashOffset

                val minLeft = areasOverlappingPipY.minByOrNull { it.left }!!.left
                val partialStashLeft = minLeft - bounds.width() - pipAreaPadding

                val newLeft = max(fullStashLeft, partialStashLeft)
                if (newLeft < bounds.left) {
                    val leftPosition = Rect(bounds)
            leftPosition.offsetTo(max(fullStashLeft, partialStashLeft), bounds.top)
                    leftPosition.offsetTo(newLeft, bounds.top)
                    stashCandidates += leftPosition
                }

        if (stashCandidates.isEmpty()) {
            return bounds
            }
        }

        return stashCandidates.minByOrNull {
            val dx = abs(it.left - bounds.left)
            val dy = abs(it.top - bounds.top)
            return@minByOrNull dx + dy
        }!!
        } ?: bounds
    }

    /**
@@ -768,7 +785,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
    }

    /**
     * Adds space around [size] to leave space for decorations that will be drawn around the pip
     * Adds space around [size] to leave space for decorations that will be drawn around the PiP
     */
    private fun addDecors(size: Size): Size {
        val bounds = Rect(0, 0, size.width, size.height)
@@ -779,7 +796,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
    }

    /**
     * Removes the space that was reserved for permanent decorations around the pip
     * Removes the space that was reserved for permanent decorations around the PiP
     * @param bounds the bounds (in screen space) to remove the insets from
     */
    private fun removePermanentDecors(bounds: Rect): Rect {
@@ -789,19 +806,20 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) {
    }

    /**
     * Removes the space that was reserved for temporary decorations around the pip
     * Removes the space that was reserved for temporary decorations around the PiP
     * @param bounds the bounds (in base case) to remove the insets from
     */
    private fun removeTemporaryDecorsTransformed(bounds: Rect): Rect {
        if (pipTemporaryDecorInsets == Insets.NONE) return bounds

        var reverseInsets = Insets.subtract(Insets.NONE, pipTemporaryDecorInsets)
        var boundsInScreenSpace = fromTransformedSpace(bounds)
        val reverseInsets = Insets.subtract(Insets.NONE, pipTemporaryDecorInsets)
        val boundsInScreenSpace = fromTransformedSpace(bounds)
        boundsInScreenSpace.inset(reverseInsets)
        return toTransformedSpace(boundsInScreenSpace)
    }

    private fun Rect.offsetCopy(dx: Int, dy: Int) = Rect(this).apply { offset(dx, dy) }
    private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom
    private fun Rect.intersectsX(other: Rect) = right >= other.left && left <= other.right
    private fun Rect.intersectsY(other: Rect) = bottom >= other.top && top <= other.bottom
    private fun Rect.intersects(other: Rect) = intersectsX(other) && intersectsY(other)
}
+23 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import org.junit.runner.RunWith
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_BOTTOM
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT
import com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_TOP
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement
import org.junit.Before
import org.junit.Test
@@ -433,6 +434,28 @@ class TvPipKeepClearAlgorithmTest {
        assertEquals(currentTime + algorithm.stashDuration, placement.unstashTime)
    }

    @Test
    fun test_ExpandedPiPHeightExceedsMovementBounds_AtAnchor() {
        gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
        pipSize = Size(DEFAULT_PIP_SIZE.width, SCREEN_SIZE.height)
        testAnchorPosition()
    }

    @Test
    fun test_ExpandedPiPHeightExceedsMovementBounds_BottomBar_StashedUp() {
        gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
        pipSize = Size(DEFAULT_PIP_SIZE.width, SCREEN_SIZE.height)
        val bottomBar = makeBottomBar(96)
        unrestrictedAreas.add(bottomBar)

        val expectedBounds = getExpectedAnchorBounds()
        expectedBounds.offset(0, -bottomBar.height() - PADDING)
        val placement = getActualPlacement()
        assertEquals(expectedBounds, placement.bounds)
        assertEquals(STASH_TYPE_TOP, placement.stashType)
        assertEquals(getExpectedAnchorBounds(), placement.unstashDestinationBounds)
    }

    @Test
    fun test_PipInsets() {
        val permInsets = Insets.of(-1, -2, -3, -4)