Loading libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt +71 −53 Original line number Diff line number Diff line Loading @@ -150,7 +150,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { return Placement( pipBounds, anchorBounds, getStashType(pipBounds, movementBounds), getStashType(pipBounds, unstashedDestBounds), unstashedDestBounds, result.unstashTime ) Loading Loading @@ -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) } Loading @@ -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 Loading @@ -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 } } Loading Loading @@ -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 } /** Loading Loading @@ -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) Loading @@ -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 { Loading @@ -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) } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt +23 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithm.kt +71 −53 Original line number Diff line number Diff line Loading @@ -150,7 +150,7 @@ class TvPipKeepClearAlgorithm(private val clock: () -> Long) { return Placement( pipBounds, anchorBounds, getStashType(pipBounds, movementBounds), getStashType(pipBounds, unstashedDestBounds), unstashedDestBounds, result.unstashTime ) Loading Loading @@ -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) } Loading @@ -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 Loading @@ -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 } } Loading Loading @@ -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 } /** Loading Loading @@ -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) Loading @@ -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 { Loading @@ -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) }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipKeepClearAlgorithmTest.kt +23 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) Loading