1 /*
<lambda>null2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.statusbar.phone
18
19 import android.annotation.ColorInt
20 import android.graphics.Rect
21 import android.view.InsetsFlags
22 import android.view.ViewDebug
23 import android.view.WindowInsetsController
24 import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
25 import android.view.WindowInsetsController.Appearance
26 import com.android.internal.statusbar.LetterboxDetails
27 import com.android.internal.util.ContrastColorUtil
28 import com.android.internal.view.AppearanceRegion
29 import com.android.systemui.Dumpable
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dump.DumpManager
32 import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
33 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
34 import java.io.PrintWriter
35 import java.util.Arrays
36 import javax.inject.Inject
37
38 class LetterboxAppearance(
39 @Appearance val appearance: Int,
40 val appearanceRegions: Array<AppearanceRegion>
41 ) {
42 override fun toString(): String {
43 val appearanceString =
44 ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
45 return "LetterboxAppearance{$appearanceString, ${appearanceRegions.contentToString()}}"
46 }
47 }
48
49 /**
50 * Responsible for calculating the [Appearance] and [AppearanceRegion] for the status bar when apps
51 * are letterboxed.
52 */
53 @SysUISingleton
54 class LetterboxAppearanceCalculator
55 @Inject
56 constructor(
57 private val lightBarController: LightBarController,
58 dumpManager: DumpManager,
59 private val letterboxBackgroundProvider: LetterboxBackgroundProvider,
60 ) : OnStatusBarViewInitializedListener, Dumpable {
61
62 init {
63 dumpManager.registerCriticalDumpable(this)
64 }
65
66 private var statusBarBoundsProvider: StatusBarBoundsProvider? = null
67
68 private var lastAppearance: Int? = null
69 private var lastAppearanceRegions: Array<AppearanceRegion>? = null
70 private var lastLetterboxes: Array<LetterboxDetails>? = null
71 private var lastLetterboxAppearance: LetterboxAppearance? = null
72
getLetterboxAppearancenull73 fun getLetterboxAppearance(
74 @Appearance originalAppearance: Int,
75 originalAppearanceRegions: Array<AppearanceRegion>,
76 letterboxes: Array<LetterboxDetails>
77 ): LetterboxAppearance {
78 lastAppearance = originalAppearance
79 lastAppearanceRegions = originalAppearanceRegions
80 lastLetterboxes = letterboxes
81 return getLetterboxAppearanceInternal(
82 letterboxes, originalAppearance, originalAppearanceRegions)
83 .also { lastLetterboxAppearance = it }
84 }
85
getLetterboxAppearanceInternalnull86 private fun getLetterboxAppearanceInternal(
87 letterboxes: Array<LetterboxDetails>,
88 originalAppearance: Int,
89 originalAppearanceRegions: Array<AppearanceRegion>
90 ): LetterboxAppearance {
91 if (isScrimNeeded(letterboxes)) {
92 return originalAppearanceWithScrim(originalAppearance, originalAppearanceRegions)
93 }
94 val appearance = appearanceWithoutScrim(originalAppearance)
95 val appearanceRegions = getAppearanceRegions(originalAppearanceRegions, letterboxes)
96 return LetterboxAppearance(appearance, appearanceRegions.toTypedArray())
97 }
98
isScrimNeedednull99 private fun isScrimNeeded(letterboxes: Array<LetterboxDetails>): Boolean {
100 if (isOuterLetterboxMultiColored()) {
101 return true
102 }
103 return letterboxes.any { letterbox ->
104 letterbox.letterboxInnerBounds.overlapsWith(getStartSideIconBounds()) ||
105 letterbox.letterboxInnerBounds.overlapsWith(getEndSideIconsBounds())
106 }
107 }
108
getAppearanceRegionsnull109 private fun getAppearanceRegions(
110 originalAppearanceRegions: Array<AppearanceRegion>,
111 letterboxes: Array<LetterboxDetails>
112 ): List<AppearanceRegion> {
113 return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) +
114 getAllOuterAppearanceRegions(letterboxes)
115 }
116
sanitizeAppearanceRegionsnull117 private fun sanitizeAppearanceRegions(
118 originalAppearanceRegions: Array<AppearanceRegion>,
119 letterboxes: Array<LetterboxDetails>
120 ): List<AppearanceRegion> =
121 originalAppearanceRegions.map { appearanceRegion ->
122 val matchingLetterbox =
123 letterboxes.find { it.letterboxFullBounds == appearanceRegion.bounds }
124 if (matchingLetterbox == null) {
125 appearanceRegion
126 } else {
127 // When WindowManager sends appearance regions for an app, it sends them for the
128 // full bounds of its window.
129 // Here we want the bounds to be only for the inner bounds of the letterboxed app.
130 AppearanceRegion(
131 appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds)
132 }
133 }
134
originalAppearanceWithScrimnull135 private fun originalAppearanceWithScrim(
136 @Appearance originalAppearance: Int,
137 originalAppearanceRegions: Array<AppearanceRegion>
138 ): LetterboxAppearance {
139 return LetterboxAppearance(
140 originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
141 originalAppearanceRegions)
142 }
143
144 @Appearance
appearanceWithoutScrimnull145 private fun appearanceWithoutScrim(@Appearance originalAppearance: Int): Int =
146 originalAppearance and APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS.inv()
147
148 private fun getAllOuterAppearanceRegions(
149 letterboxes: Array<LetterboxDetails>
150 ): List<AppearanceRegion> = letterboxes.map(this::getOuterAppearanceRegions).flatten()
151
152 private fun getOuterAppearanceRegions(
153 letterboxDetails: LetterboxDetails
154 ): List<AppearanceRegion> {
155 @Appearance val outerAppearance = getOuterAppearance()
156 return getVisibleOuterBounds(letterboxDetails).map { bounds ->
157 AppearanceRegion(outerAppearance, bounds)
158 }
159 }
160
getVisibleOuterBoundsnull161 private fun getVisibleOuterBounds(letterboxDetails: LetterboxDetails): List<Rect> {
162 val inner = letterboxDetails.letterboxInnerBounds
163 val outer = letterboxDetails.letterboxFullBounds
164 val top = Rect(outer.left, outer.top, outer.right, inner.top)
165 val left = Rect(outer.left, outer.top, inner.left, outer.bottom)
166 val right = Rect(inner.right, outer.top, outer.right, outer.bottom)
167 val bottom = Rect(outer.left, inner.bottom, outer.right, outer.bottom)
168 return listOf(left, top, right, bottom).filter { !it.isEmpty }
169 }
170
171 @Appearance
getOuterAppearancenull172 private fun getOuterAppearance(): Int {
173 val backgroundColor = outerLetterboxBackgroundColor()
174 val darkAppearanceContrast =
175 ContrastColorUtil.calculateContrast(
176 lightBarController.darkAppearanceIconColor, backgroundColor)
177 val lightAppearanceContrast =
178 ContrastColorUtil.calculateContrast(
179 lightBarController.lightAppearanceIconColor, backgroundColor)
180 return if (lightAppearanceContrast > darkAppearanceContrast) {
181 WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
182 } else {
183 0 // APPEARANCE_DEFAULT
184 }
185 }
186
187 @ColorInt
outerLetterboxBackgroundColornull188 private fun outerLetterboxBackgroundColor(): Int {
189 return letterboxBackgroundProvider.letterboxBackgroundColor
190 }
191
isOuterLetterboxMultiColorednull192 private fun isOuterLetterboxMultiColored(): Boolean {
193 return letterboxBackgroundProvider.isLetterboxBackgroundMultiColored
194 }
195
getEndSideIconsBoundsnull196 private fun getEndSideIconsBounds(): Rect {
197 return statusBarBoundsProvider?.visibleEndSideBounds ?: Rect()
198 }
199
getStartSideIconBoundsnull200 private fun getStartSideIconBounds(): Rect {
201 return statusBarBoundsProvider?.visibleStartSideBounds ?: Rect()
202 }
203
onStatusBarViewInitializednull204 override fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) {
205 statusBarBoundsProvider = component.boundsProvider
206 }
207
Rectnull208 private fun Rect.overlapsWith(other: Rect): Boolean {
209 if (this.contains(other) || other.contains(this)) {
210 return false
211 }
212 return this.intersects(other.left, other.top, other.right, other.bottom)
213 }
214
dumpnull215 override fun dump(pw: PrintWriter, args: Array<out String>) {
216 pw.println(
217 """
218 lastAppearance: ${lastAppearance?.toAppearanceString()}
219 lastAppearanceRegion: ${Arrays.toString(lastAppearanceRegions)},
220 lastLetterboxes: ${Arrays.toString(lastLetterboxes)},
221 lastLetterboxAppearance: $lastLetterboxAppearance
222 """.trimIndent())
223 }
224 }
225
toAppearanceStringnull226 private fun Int.toAppearanceString(): String =
227 ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", this)
228