From 53450fbed3109e0d424fc849a5746f16190a7aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Go=CC=88llner?= Date: Mon, 11 Jul 2022 10:56:21 +0200 Subject: [PATCH] Create LetterboxAppearanceCalculator for SysUI This class is responsible for calculating appearance and appearance regions for the status bar, whenever apps are letterboxed. Change-Id: I3adfa99f4908ae316ae9309e19c0db8a4348f27a Merged-In: I3adfa99f4908ae316ae9309e19c0db8a4348f27a Test: LetterboxAppearanceCalculatorTest.kt Test: Manually Fixes: 238588538 --- .../statusbar/core/StatusBarInitializer.kt | 18 +- .../phone/LetterboxAppearanceCalculator.kt | 227 ++++++++++++++++++ .../statusbar/phone/LightBarController.java | 18 +- .../CentralSurfacesStartableModule.java | 9 + .../phone/dagger/StatusBarViewModule.java | 10 + .../LetterboxAppearanceCalculatorTest.kt | 205 ++++++++++++++++ 6 files changed, 483 insertions(+), 4 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt index a62a152ab7f4c..5351024c2f9f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt new file mode 100644 index 0000000000000..70ec13b14abd0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt @@ -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 +) + +/** + * 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? = null + private var lastLetterboxes: Array? = null + private var lastLetterboxAppearance: LetterboxAppearance? = null + + fun getLetterboxAppearance( + @Appearance originalAppearance: Int, + originalAppearanceRegions: Array, + letterboxes: Array + ): LetterboxAppearance { + lastAppearance = originalAppearance + lastAppearanceRegions = originalAppearanceRegions + lastLetterboxes = letterboxes + return getLetterboxAppearanceInternal( + letterboxes, originalAppearance, originalAppearanceRegions) + .also { lastLetterboxAppearance = it } + } + + private fun getLetterboxAppearanceInternal( + letterboxes: Array, + originalAppearance: Int, + originalAppearanceRegions: Array + ): 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): Boolean { + if (isOuterLetterboxMultiColored()) { + return true + } + return letterboxes.any { letterbox -> + letterbox.letterboxInnerBounds.overlapsWith(getStartSideIconBounds()) || + letterbox.letterboxInnerBounds.overlapsWith(getEndSideIconsBounds()) + } + } + + private fun getAppearanceRegions( + originalAppearanceRegions: Array, + letterboxes: Array + ): List { + return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) + + getAllOuterAppearanceRegions(letterboxes) + } + + private fun sanitizeAppearanceRegions( + originalAppearanceRegions: Array, + letterboxes: Array + ): List = + 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 + ): 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 + ): List = letterboxes.map(this::getOuterAppearanceRegions).flatten() + + private fun getOuterAppearanceRegions( + letterboxDetails: LetterboxDetails + ): List { + @Appearance val outerAppearance = getOuterAppearance() + return getVisibleOuterBounds(letterboxDetails).map { bounds -> + AppearanceRegion(outerAppearance, bounds) + } + } + + private fun getVisibleOuterBounds(letterboxDetails: LetterboxDetails): List { + 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) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java index 2c57328174e86..4d14542328534 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java @@ -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(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java index 21e5ad5778b12..590522fc87511 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java @@ -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 multibindStartables(); + + @Binds + @IntoSet + CentralSurfacesComponent.Startable letterboxAppearanceCalculator( + LetterboxAppearanceCalculator letterboxAppearanceCalculator); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java index b60739164a191..b3bef07cb820b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java @@ -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}. * diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt new file mode 100644 index 0000000000000..65401356b3e3b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt @@ -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)