Merge "Create LetterboxAppearanceCalculator for SysUI" into tm-qpr-dev

This commit is contained in:
TreeHugger Robot
2022-07-25 09:04:59 +00:00
committed by Android (Google) Code Review
6 changed files with 483 additions and 4 deletions

View File

@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
import com.android.systemui.statusbar.window.StatusBarWindowController
import java.lang.IllegalStateException
import javax.inject.Inject
@@ -34,7 +35,8 @@ import javax.inject.Inject
*/
@CentralSurfacesScope
class StatusBarInitializer @Inject constructor(
private val windowController: StatusBarWindowController
private val windowController: StatusBarWindowController,
private val creationListeners: Set<@JvmSuppressWildcards OnStatusBarViewInitializedListener>,
) {
var statusBarViewUpdatedListener: OnStatusBarViewUpdatedListener? = null
@@ -56,6 +58,9 @@ class StatusBarInitializer @Inject constructor(
statusBarFragmentComponent.phoneStatusBarViewController,
statusBarFragmentComponent.phoneStatusBarTransitions
)
creationListeners.forEach { listener ->
listener.onStatusBarViewInitialized(statusBarFragmentComponent)
}
}
override fun onFragmentViewDestroyed(tag: String?, fragment: Fragment?) {
@@ -69,6 +74,17 @@ class StatusBarInitializer @Inject constructor(
.commit()
}
interface OnStatusBarViewInitializedListener {
/**
* The status bar view has been initialized.
*
* @param component Dagger component that is created when the status bar view is created.
* Can be used to retrieve dependencies from that scope, including the status bar root view.
*/
fun onStatusBarViewInitialized(component: StatusBarFragmentComponent)
}
interface OnStatusBarViewUpdatedListener {
fun onStatusBarViewUpdated(
statusBarView: PhoneStatusBarView,

View File

@@ -0,0 +1,227 @@
/*
* Copyright (C) 2022 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.systemui.statusbar.phone
import android.annotation.ColorInt
import android.graphics.Color
import android.graphics.Rect
import android.view.InsetsFlags
import android.view.ViewDebug
import android.view.WindowInsetsController
import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
import android.view.WindowInsetsController.Appearance
import com.android.internal.statusbar.LetterboxDetails
import com.android.internal.util.ContrastColorUtil
import com.android.internal.view.AppearanceRegion
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
import java.io.PrintWriter
import java.util.Arrays
import javax.inject.Inject
class LetterboxAppearance(
@Appearance val appearance: Int,
val appearanceRegions: Array<AppearanceRegion>
)
/**
* Responsible for calculating the [Appearance] and [AppearanceRegion] for the status bar when apps
* are letterboxed.
*/
@CentralSurfacesScope
class LetterboxAppearanceCalculator
@Inject
constructor(
private val lightBarController: LightBarController,
private val dumpManager: DumpManager,
) : OnStatusBarViewInitializedListener, CentralSurfacesComponent.Startable {
private var statusBarBoundsProvider: StatusBarBoundsProvider? = null
override fun start() {
dumpManager.registerDumpable(javaClass.simpleName) { printWriter, _ -> dump(printWriter) }
}
override fun stop() {
dumpManager.unregisterDumpable(javaClass.simpleName)
}
private var lastAppearance: Int? = null
private var lastAppearanceRegions: Array<AppearanceRegion>? = null
private var lastLetterboxes: Array<LetterboxDetails>? = null
private var lastLetterboxAppearance: LetterboxAppearance? = null
fun getLetterboxAppearance(
@Appearance originalAppearance: Int,
originalAppearanceRegions: Array<AppearanceRegion>,
letterboxes: Array<LetterboxDetails>
): LetterboxAppearance {
lastAppearance = originalAppearance
lastAppearanceRegions = originalAppearanceRegions
lastLetterboxes = letterboxes
return getLetterboxAppearanceInternal(
letterboxes, originalAppearance, originalAppearanceRegions)
.also { lastLetterboxAppearance = it }
}
private fun getLetterboxAppearanceInternal(
letterboxes: Array<LetterboxDetails>,
originalAppearance: Int,
originalAppearanceRegions: Array<AppearanceRegion>
): LetterboxAppearance {
if (isScrimNeeded(letterboxes)) {
return originalAppearanceWithScrim(originalAppearance, originalAppearanceRegions)
}
val appearance = appearanceWithoutScrim(originalAppearance)
val appearanceRegions = getAppearanceRegions(originalAppearanceRegions, letterboxes)
return LetterboxAppearance(appearance, appearanceRegions.toTypedArray())
}
private fun isScrimNeeded(letterboxes: Array<LetterboxDetails>): Boolean {
if (isOuterLetterboxMultiColored()) {
return true
}
return letterboxes.any { letterbox ->
letterbox.letterboxInnerBounds.overlapsWith(getStartSideIconBounds()) ||
letterbox.letterboxInnerBounds.overlapsWith(getEndSideIconsBounds())
}
}
private fun getAppearanceRegions(
originalAppearanceRegions: Array<AppearanceRegion>,
letterboxes: Array<LetterboxDetails>
): List<AppearanceRegion> {
return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) +
getAllOuterAppearanceRegions(letterboxes)
}
private fun sanitizeAppearanceRegions(
originalAppearanceRegions: Array<AppearanceRegion>,
letterboxes: Array<LetterboxDetails>
): List<AppearanceRegion> =
originalAppearanceRegions.map { appearanceRegion ->
val matchingLetterbox =
letterboxes.find { it.letterboxFullBounds == appearanceRegion.bounds }
if (matchingLetterbox == null) {
appearanceRegion
} else {
// When WindowManager sends appearance regions for an app, it sends them for the
// full bounds of its window.
// Here we want the bounds to be only for the inner bounds of the letterboxed app.
AppearanceRegion(
appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds)
}
}
private fun originalAppearanceWithScrim(
@Appearance originalAppearance: Int,
originalAppearanceRegions: Array<AppearanceRegion>
): LetterboxAppearance {
return LetterboxAppearance(
originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
originalAppearanceRegions)
}
@Appearance
private fun appearanceWithoutScrim(@Appearance originalAppearance: Int): Int =
originalAppearance and APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS.inv()
private fun getAllOuterAppearanceRegions(
letterboxes: Array<LetterboxDetails>
): List<AppearanceRegion> = letterboxes.map(this::getOuterAppearanceRegions).flatten()
private fun getOuterAppearanceRegions(
letterboxDetails: LetterboxDetails
): List<AppearanceRegion> {
@Appearance val outerAppearance = getOuterAppearance()
return getVisibleOuterBounds(letterboxDetails).map { bounds ->
AppearanceRegion(outerAppearance, bounds)
}
}
private fun getVisibleOuterBounds(letterboxDetails: LetterboxDetails): List<Rect> {
val inner = letterboxDetails.letterboxInnerBounds
val outer = letterboxDetails.letterboxFullBounds
val top = Rect(outer.left, outer.top, outer.right, inner.top)
val left = Rect(outer.left, outer.top, inner.left, outer.bottom)
val right = Rect(inner.right, outer.top, outer.right, outer.bottom)
val bottom = Rect(outer.left, inner.bottom, outer.right, outer.bottom)
return listOf(left, top, right, bottom).filter { !it.isEmpty }
}
@Appearance
private fun getOuterAppearance(): Int {
val backgroundColor = outerLetterboxBackgroundColor()
val darkAppearanceContrast =
ContrastColorUtil.calculateContrast(
lightBarController.darkAppearanceIconColor, backgroundColor)
val lightAppearanceContrast =
ContrastColorUtil.calculateContrast(
lightBarController.lightAppearanceIconColor, backgroundColor)
return if (lightAppearanceContrast > darkAppearanceContrast) {
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
} else {
0 // APPEARANCE_DEFAULT
}
}
@ColorInt
private fun outerLetterboxBackgroundColor(): Int {
// TODO(b/238607453): retrieve this information from WindowManager.
return Color.BLACK
}
private fun isOuterLetterboxMultiColored(): Boolean {
// TODO(b/238607453): retrieve this information from WindowManager.
return false
}
private fun getEndSideIconsBounds(): Rect {
return statusBarBoundsProvider?.visibleEndSideBounds ?: Rect()
}
private fun getStartSideIconBounds(): Rect {
return statusBarBoundsProvider?.visibleStartSideBounds ?: Rect()
}
override fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) {
statusBarBoundsProvider = component.boundsProvider
}
private fun Rect.overlapsWith(other: Rect): Boolean {
if (this.contains(other) || other.contains(this)) {
return false
}
return this.intersect(other)
}
private fun dump(printWriter: PrintWriter) {
printWriter.println(
"""
lastAppearance: ${lastAppearance?.toAppearanceString()}
lastAppearanceRegion: ${Arrays.toString(lastAppearanceRegions)},
lastLetterboxes: ${Arrays.toString(lastLetterboxes)},
lastLetterboxAppearance: $lastLetterboxAppearance
""".trimIndent())
}
}
private fun Int.toAppearanceString(): String =
ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", this)

View File

@@ -23,8 +23,8 @@ import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_LIGHTS_OUT_TRANSPARENT;
import static com.android.systemui.statusbar.phone.BarTransitions.MODE_TRANSPARENT;
import android.annotation.ColorInt;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.view.InsetsFlags;
import android.view.ViewDebug;
@@ -63,7 +63,8 @@ public class LightBarController implements BatteryController.BatteryStateChangeC
private int mStatusBarMode;
private int mNavigationBarMode;
private int mNavigationMode;
private final Color mDarkModeColor;
private final int mDarkIconColor;
private final int mLightIconColor;
/**
* Whether the navigation bar should be light factoring in already how much alpha the scrim has
@@ -94,7 +95,8 @@ public class LightBarController implements BatteryController.BatteryStateChangeC
BatteryController batteryController,
NavigationModeController navModeController,
DumpManager dumpManager) {
mDarkModeColor = Color.valueOf(ctx.getColor(R.color.dark_mode_icon_color_single_tone));
mDarkIconColor = ctx.getColor(R.color.dark_mode_icon_color_single_tone);
mLightIconColor = ctx.getColor(R.color.light_mode_icon_color_single_tone);
mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher;
mBatteryController = batteryController;
mBatteryController.addCallback(this);
@@ -107,6 +109,16 @@ public class LightBarController implements BatteryController.BatteryStateChangeC
}
}
@ColorInt
int getLightAppearanceIconColor() {
return mDarkIconColor;
}
@ColorInt
int getDarkAppearanceIconColor() {
return mLightIconColor;
}
public void setNavigationBar(LightBarTransitionsController navigationBar) {
mNavigationBarController = navigationBar;
updateNavigation();

View File

@@ -16,13 +16,22 @@
package com.android.systemui.statusbar.phone.dagger;
import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator;
import java.util.Set;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoSet;
import dagger.multibindings.Multibinds;
@Module
interface CentralSurfacesStartableModule {
@Multibinds
Set<CentralSurfacesComponent.Startable> multibindStartables();
@Binds
@IntoSet
CentralSurfacesComponent.Startable letterboxAppearanceCalculator(
LetterboxAppearanceCalculator letterboxAppearanceCalculator);
}

View File

@@ -46,10 +46,12 @@ import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.NotificationShelfController;
import com.android.systemui.statusbar.OperatorNameViewController;
import com.android.systemui.statusbar.connectivity.NetworkController;
import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener;
import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView;
import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator;
import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -72,8 +74,10 @@ import java.util.concurrent.Executor;
import javax.inject.Named;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
@Module(subcomponents = StatusBarFragmentComponent.class)
public abstract class StatusBarViewModule {
@@ -248,6 +252,12 @@ public abstract class StatusBarViewModule {
return notificationShadeWindowView.findViewById(R.id.notification_container_parent);
}
@Binds
@IntoSet
abstract OnStatusBarViewInitializedListener statusBarInitializedListener(
LetterboxAppearanceCalculator letterboxAppearanceCalculator
);
/**
* Creates a new {@link CollapsedStatusBarFragment}.
*

View File

@@ -0,0 +1,205 @@
/*
* Copyright (C) 2022 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.systemui.statusbar.phone
import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.view.WindowInsetsController
import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
import androidx.test.filters.SmallTest
import com.android.internal.statusbar.LetterboxDetails
import com.android.internal.view.AppearanceRegion
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.`when` as whenever
import com.android.systemui.dump.DumpManager
import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class LetterboxAppearanceCalculatorTest : SysuiTestCase() {
companion object {
private const val DEFAULT_APPEARANCE = 0
private const val TEST_APPEARANCE = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
private val TEST_APPEARANCE_REGION_BOUNDS = Rect(0, 0, 20, 100)
private val TEST_APPEARANCE_REGION =
AppearanceRegion(TEST_APPEARANCE, TEST_APPEARANCE_REGION_BOUNDS)
private val TEST_APPEARANCE_REGIONS = arrayOf(TEST_APPEARANCE_REGION)
private val TEST_WINDOW_BOUNDS = Rect(0, 0, 500, 500)
}
@get:Rule var expect = Expect.create()
@Mock private lateinit var lightBarController: LightBarController
@Mock private lateinit var statusBarBoundsProvider: StatusBarBoundsProvider
@Mock private lateinit var statusBarFragmentComponent: StatusBarFragmentComponent
@Mock private lateinit var dumpManager: DumpManager
private lateinit var calculator: LetterboxAppearanceCalculator
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(statusBarFragmentComponent.boundsProvider).thenReturn(statusBarBoundsProvider)
calculator = LetterboxAppearanceCalculator(lightBarController, dumpManager)
calculator.onStatusBarViewInitialized(statusBarFragmentComponent)
}
@Test
fun getLetterboxAppearance_overlapStartSide_returnsOriginalWithScrim() {
whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
val letterbox = letterboxWithInnerBounds(Rect(50, 50, 150, 150))
val letterboxAppearance =
calculator.getLetterboxAppearance(
TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
expect
.that(letterboxAppearance.appearance)
.isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
expect.that(letterboxAppearance.appearanceRegions).isEqualTo(TEST_APPEARANCE_REGIONS)
}
@Test
fun getLetterboxAppearance_overlapEndSide_returnsOriginalWithScrim() {
whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
val letterbox = letterboxWithInnerBounds(Rect(150, 50, 250, 150))
val letterboxAppearance =
calculator.getLetterboxAppearance(
TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
expect
.that(letterboxAppearance.appearance)
.isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
expect.that(letterboxAppearance.appearanceRegions).isEqualTo(TEST_APPEARANCE_REGIONS)
}
@Test
fun getLetterboxAppearance_noOverlap_returnsAppearanceWithoutScrim() {
whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
val letterbox = letterboxWithInnerBounds(Rect(101, 0, 199, 100))
val letterboxAppearance =
calculator.getLetterboxAppearance(
TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@Test
fun getLetterboxAppearance_letterboxContainsStartSide_returnsAppearanceWithoutScrim() {
whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
val letterbox = letterboxWithInnerBounds(Rect(0, 0, 101, 101))
val letterboxAppearance =
calculator.getLetterboxAppearance(
TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@Test
fun getLetterboxAppearance_letterboxContainsEndSide_returnsAppearanceWithoutScrim() {
whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
val letterbox = letterboxWithInnerBounds(Rect(199, 0, 301, 101))
val letterboxAppearance =
calculator.getLetterboxAppearance(
TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@Test
fun getLetterboxAppearance_letterboxContainsEntireStatusBar_returnsAppearanceWithoutScrim() {
whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
val letterbox = letterboxWithInnerBounds(Rect(0, 0, 300, 100))
val letterboxAppearance =
calculator.getLetterboxAppearance(
TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
assertThat(letterboxAppearance.appearance).isEqualTo(TEST_APPEARANCE)
}
@Test
fun getLetterboxAppearance_returnsAdaptedAppearanceRegions_basedOnLetterboxInnerBounds() {
whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 0, 0))
whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(0, 0, 0, 0))
val letterbox = letterboxWithInnerBounds(Rect(150, 0, 300, 800))
val letterboxRegion = TEST_APPEARANCE_REGION.copy(bounds = letterbox.letterboxFullBounds)
val letterboxAppearance =
calculator.getLetterboxAppearance(
TEST_APPEARANCE, arrayOf(letterboxRegion), arrayOf(letterbox))
val letterboxAdaptedRegion = letterboxRegion.copy(bounds = letterbox.letterboxInnerBounds)
assertThat(letterboxAppearance.appearanceRegions.toList()).contains(letterboxAdaptedRegion)
assertThat(letterboxAppearance.appearanceRegions.toList()).doesNotContain(letterboxRegion)
}
@Test
fun getLetterboxAppearance_returnsDefaultAppearanceRegions_basedOnLetterboxOuterBounds() {
whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 0, 0))
whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(0, 0, 0, 0))
val letterbox =
letterboxWithBounds(
innerBounds = Rect(left = 25, top = 0, right = 75, bottom = 100),
fullBounds = Rect(left = 0, top = 0, right = 100, bottom = 100))
val letterboxRegion = TEST_APPEARANCE_REGION.copy(bounds = letterbox.letterboxFullBounds)
val letterboxAppearance =
calculator.getLetterboxAppearance(
TEST_APPEARANCE, arrayOf(letterboxRegion), arrayOf(letterbox))
val outerRegions =
listOf(
AppearanceRegion(
DEFAULT_APPEARANCE, Rect(left = 0, top = 0, right = 25, bottom = 100)),
AppearanceRegion(
DEFAULT_APPEARANCE, Rect(left = 75, top = 0, right = 100, bottom = 100)),
)
assertThat(letterboxAppearance.appearanceRegions.toList())
.containsAtLeastElementsIn(outerRegions)
}
private fun letterboxWithBounds(innerBounds: Rect, fullBounds: Rect) =
LetterboxDetails(innerBounds, fullBounds, TEST_APPEARANCE)
private fun letterboxWithInnerBounds(innerBounds: Rect) =
letterboxWithBounds(innerBounds, fullBounds = TEST_WINDOW_BOUNDS)
}
private fun AppearanceRegion.copy(appearance: Int = this.appearance, bounds: Rect = this.bounds) =
AppearanceRegion(appearance, bounds)
private fun Rect(left: Int, top: Int, right: Int, bottom: Int) = Rect(left, top, right, bottom)