1 /*
2  * 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 
73     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 
86     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 
99     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 
109     private fun getAppearanceRegions(
110         originalAppearanceRegions: Array<AppearanceRegion>,
111         letterboxes: Array<LetterboxDetails>
112     ): List<AppearanceRegion> {
113         return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) +
114             getAllOuterAppearanceRegions(letterboxes)
115     }
116 
117     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 
135     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
145     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 
161     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
172     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
188     private fun outerLetterboxBackgroundColor(): Int {
189         return letterboxBackgroundProvider.letterboxBackgroundColor
190     }
191 
192     private fun isOuterLetterboxMultiColored(): Boolean {
193         return letterboxBackgroundProvider.isLetterboxBackgroundMultiColored
194     }
195 
196     private fun getEndSideIconsBounds(): Rect {
197         return statusBarBoundsProvider?.visibleEndSideBounds ?: Rect()
198     }
199 
200     private fun getStartSideIconBounds(): Rect {
201         return statusBarBoundsProvider?.visibleStartSideBounds ?: Rect()
202     }
203 
204     override fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) {
205         statusBarBoundsProvider = component.boundsProvider
206     }
207 
208     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 
215     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 
226 private fun Int.toAppearanceString(): String =
227     ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", this)
228