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.dump.DumpManager
30 import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
31 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
32 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
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 @CentralSurfacesScope
54 class LetterboxAppearanceCalculator
55 @Inject
56 constructor(
57 private val lightBarController: LightBarController,
58 private val dumpManager: DumpManager,
59 private val letterboxBackgroundProvider: LetterboxBackgroundProvider,
60 ) : OnStatusBarViewInitializedListener, CentralSurfacesComponent.Startable {
61
62 private var statusBarBoundsProvider: StatusBarBoundsProvider? = null
63
startnull64 override fun start() {
65 dumpManager.registerCriticalDumpable(javaClass.simpleName) { pw, _ -> dump(pw) }
66 }
67
stopnull68 override fun stop() {
69 dumpManager.unregisterDumpable(javaClass.simpleName)
70 }
71
72 private var lastAppearance: Int? = null
73 private var lastAppearanceRegions: Array<AppearanceRegion>? = null
74 private var lastLetterboxes: Array<LetterboxDetails>? = null
75 private var lastLetterboxAppearance: LetterboxAppearance? = null
76
getLetterboxAppearancenull77 fun getLetterboxAppearance(
78 @Appearance originalAppearance: Int,
79 originalAppearanceRegions: Array<AppearanceRegion>,
80 letterboxes: Array<LetterboxDetails>
81 ): LetterboxAppearance {
82 lastAppearance = originalAppearance
83 lastAppearanceRegions = originalAppearanceRegions
84 lastLetterboxes = letterboxes
85 return getLetterboxAppearanceInternal(
86 letterboxes, originalAppearance, originalAppearanceRegions)
87 .also { lastLetterboxAppearance = it }
88 }
89
getLetterboxAppearanceInternalnull90 private fun getLetterboxAppearanceInternal(
91 letterboxes: Array<LetterboxDetails>,
92 originalAppearance: Int,
93 originalAppearanceRegions: Array<AppearanceRegion>
94 ): LetterboxAppearance {
95 if (isScrimNeeded(letterboxes)) {
96 return originalAppearanceWithScrim(originalAppearance, originalAppearanceRegions)
97 }
98 val appearance = appearanceWithoutScrim(originalAppearance)
99 val appearanceRegions = getAppearanceRegions(originalAppearanceRegions, letterboxes)
100 return LetterboxAppearance(appearance, appearanceRegions.toTypedArray())
101 }
102
isScrimNeedednull103 private fun isScrimNeeded(letterboxes: Array<LetterboxDetails>): Boolean {
104 if (isOuterLetterboxMultiColored()) {
105 return true
106 }
107 return letterboxes.any { letterbox ->
108 letterbox.letterboxInnerBounds.overlapsWith(getStartSideIconBounds()) ||
109 letterbox.letterboxInnerBounds.overlapsWith(getEndSideIconsBounds())
110 }
111 }
112
getAppearanceRegionsnull113 private fun getAppearanceRegions(
114 originalAppearanceRegions: Array<AppearanceRegion>,
115 letterboxes: Array<LetterboxDetails>
116 ): List<AppearanceRegion> {
117 return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) +
118 getAllOuterAppearanceRegions(letterboxes)
119 }
120
sanitizeAppearanceRegionsnull121 private fun sanitizeAppearanceRegions(
122 originalAppearanceRegions: Array<AppearanceRegion>,
123 letterboxes: Array<LetterboxDetails>
124 ): List<AppearanceRegion> =
125 originalAppearanceRegions.map { appearanceRegion ->
126 val matchingLetterbox =
127 letterboxes.find { it.letterboxFullBounds == appearanceRegion.bounds }
128 if (matchingLetterbox == null) {
129 appearanceRegion
130 } else {
131 // When WindowManager sends appearance regions for an app, it sends them for the
132 // full bounds of its window.
133 // Here we want the bounds to be only for the inner bounds of the letterboxed app.
134 AppearanceRegion(
135 appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds)
136 }
137 }
138
originalAppearanceWithScrimnull139 private fun originalAppearanceWithScrim(
140 @Appearance originalAppearance: Int,
141 originalAppearanceRegions: Array<AppearanceRegion>
142 ): LetterboxAppearance {
143 return LetterboxAppearance(
144 originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS,
145 originalAppearanceRegions)
146 }
147
148 @Appearance
appearanceWithoutScrimnull149 private fun appearanceWithoutScrim(@Appearance originalAppearance: Int): Int =
150 originalAppearance and APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS.inv()
151
152 private fun getAllOuterAppearanceRegions(
153 letterboxes: Array<LetterboxDetails>
154 ): List<AppearanceRegion> = letterboxes.map(this::getOuterAppearanceRegions).flatten()
155
156 private fun getOuterAppearanceRegions(
157 letterboxDetails: LetterboxDetails
158 ): List<AppearanceRegion> {
159 @Appearance val outerAppearance = getOuterAppearance()
160 return getVisibleOuterBounds(letterboxDetails).map { bounds ->
161 AppearanceRegion(outerAppearance, bounds)
162 }
163 }
164
getVisibleOuterBoundsnull165 private fun getVisibleOuterBounds(letterboxDetails: LetterboxDetails): List<Rect> {
166 val inner = letterboxDetails.letterboxInnerBounds
167 val outer = letterboxDetails.letterboxFullBounds
168 val top = Rect(outer.left, outer.top, outer.right, inner.top)
169 val left = Rect(outer.left, outer.top, inner.left, outer.bottom)
170 val right = Rect(inner.right, outer.top, outer.right, outer.bottom)
171 val bottom = Rect(outer.left, inner.bottom, outer.right, outer.bottom)
172 return listOf(left, top, right, bottom).filter { !it.isEmpty }
173 }
174
175 @Appearance
getOuterAppearancenull176 private fun getOuterAppearance(): Int {
177 val backgroundColor = outerLetterboxBackgroundColor()
178 val darkAppearanceContrast =
179 ContrastColorUtil.calculateContrast(
180 lightBarController.darkAppearanceIconColor, backgroundColor)
181 val lightAppearanceContrast =
182 ContrastColorUtil.calculateContrast(
183 lightBarController.lightAppearanceIconColor, backgroundColor)
184 return if (lightAppearanceContrast > darkAppearanceContrast) {
185 WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
186 } else {
187 0 // APPEARANCE_DEFAULT
188 }
189 }
190
191 @ColorInt
outerLetterboxBackgroundColornull192 private fun outerLetterboxBackgroundColor(): Int {
193 return letterboxBackgroundProvider.letterboxBackgroundColor
194 }
195
isOuterLetterboxMultiColorednull196 private fun isOuterLetterboxMultiColored(): Boolean {
197 return letterboxBackgroundProvider.isLetterboxBackgroundMultiColored
198 }
199
getEndSideIconsBoundsnull200 private fun getEndSideIconsBounds(): Rect {
201 return statusBarBoundsProvider?.visibleEndSideBounds ?: Rect()
202 }
203
getStartSideIconBoundsnull204 private fun getStartSideIconBounds(): Rect {
205 return statusBarBoundsProvider?.visibleStartSideBounds ?: Rect()
206 }
207
onStatusBarViewInitializednull208 override fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) {
209 statusBarBoundsProvider = component.boundsProvider
210 }
211
Rectnull212 private fun Rect.overlapsWith(other: Rect): Boolean {
213 if (this.contains(other) || other.contains(this)) {
214 return false
215 }
216 return this.intersect(other)
217 }
218
dumpnull219 private fun dump(printWriter: PrintWriter) {
220 printWriter.println(
221 """
222 lastAppearance: ${lastAppearance?.toAppearanceString()}
223 lastAppearanceRegion: ${Arrays.toString(lastAppearanceRegions)},
224 lastLetterboxes: ${Arrays.toString(lastLetterboxes)},
225 lastLetterboxAppearance: $lastLetterboxAppearance
226 """.trimIndent())
227 }
228 }
229
toAppearanceStringnull230 private fun Int.toAppearanceString(): String =
231 ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", this)
232