1 /*
<lambda>null2 * Copyright (C) 2023 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.data.repository
18
19 import android.graphics.Rect
20 import android.view.InsetsFlags
21 import android.view.ViewDebug
22 import android.view.WindowInsets
23 import android.view.WindowInsetsController
24 import android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS
25 import android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS
26 import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS
27 import android.view.WindowInsetsController.Appearance
28 import com.android.internal.statusbar.LetterboxDetails
29 import com.android.internal.view.AppearanceRegion
30 import com.android.systemui.Dumpable
31 import com.android.systemui.dagger.qualifiers.Application
32 import com.android.systemui.statusbar.CommandQueue
33 import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
34 import com.android.systemui.statusbar.data.model.StatusBarAppearance
35 import com.android.systemui.statusbar.data.model.StatusBarMode
36 import com.android.systemui.statusbar.phone.BoundsPair
37 import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator
38 import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
39 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
40 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
41 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
42 import dagger.assisted.Assisted
43 import dagger.assisted.AssistedFactory
44 import dagger.assisted.AssistedInject
45 import java.io.PrintWriter
46 import kotlinx.coroutines.CoroutineScope
47 import kotlinx.coroutines.flow.MutableStateFlow
48 import kotlinx.coroutines.flow.SharingStarted
49 import kotlinx.coroutines.flow.StateFlow
50 import kotlinx.coroutines.flow.asStateFlow
51 import kotlinx.coroutines.flow.combine
52 import kotlinx.coroutines.flow.map
53 import kotlinx.coroutines.flow.stateIn
54
55 /**
56 * A repository for the current mode of the status bar on the homescreen (translucent, transparent,
57 * opaque, lights out, hidden, etc.).
58 *
59 * Note: These status bar modes are status bar *window* states that are sent to us from
60 * WindowManager, not determined internally.
61 */
62 interface StatusBarModePerDisplayRepository {
63 /**
64 * True if the status bar window is showing transiently and will disappear soon, and false
65 * otherwise. ("Otherwise" in this case means the status bar is persistently hidden OR
66 * persistently shown.)
67 *
68 * This behavior is controlled by WindowManager via
69 * [android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE], *not* calculated
70 * internally. SysUI merely obeys the behavior sent to us.
71 */
72 val isTransientShown: StateFlow<Boolean>
73
74 /**
75 * True the focused window is fullscreen (aka immersive) and false otherwise.
76 *
77 * Typically, the only time the status bar window is hidden is when the focused window is
78 * fullscreen.
79 */
80 val isInFullscreenMode: StateFlow<Boolean>
81
82 /**
83 * The current status bar appearance parameters.
84 *
85 * Null at system startup, but non-null once the first system callback has been received.
86 */
87 val statusBarAppearance: StateFlow<StatusBarAppearance?>
88
89 /** The current mode of the status bar. */
90 val statusBarMode: StateFlow<StatusBarMode>
91
92 /**
93 * Requests for the status bar to be shown transiently.
94 *
95 * TODO(b/277764509): Don't allow [CentralSurfaces] to set the transient mode; have it
96 * determined internally instead.
97 */
98 fun showTransient()
99
100 /**
101 * Requests for the status bar to be no longer showing transiently.
102 *
103 * TODO(b/277764509): Don't allow [CentralSurfaces] to set the transient mode; have it
104 * determined internally instead.
105 */
106 fun clearTransient()
107 }
108
109 class StatusBarModePerDisplayRepositoryImpl
110 @AssistedInject
111 constructor(
112 @Application scope: CoroutineScope,
113 @Assisted("displayId") thisDisplayId: Int,
114 private val commandQueue: CommandQueue,
115 private val letterboxAppearanceCalculator: LetterboxAppearanceCalculator,
116 ongoingCallRepository: OngoingCallRepository,
117 ) : StatusBarModePerDisplayRepository, OnStatusBarViewInitializedListener, Dumpable {
118
119 private val commandQueueCallback =
120 object : CommandQueue.Callbacks {
showTransientnull121 override fun showTransient(
122 displayId: Int,
123 @WindowInsets.Type.InsetsType types: Int,
124 isGestureOnSystemBar: Boolean,
125 ) {
126 if (isTransientRelevant(displayId, types)) {
127 _isTransientShown.value = true
128 }
129 }
130
abortTransientnull131 override fun abortTransient(displayId: Int, @WindowInsets.Type.InsetsType types: Int) {
132 if (isTransientRelevant(displayId, types)) {
133 _isTransientShown.value = false
134 }
135 }
136
isTransientRelevantnull137 private fun isTransientRelevant(
138 displayId: Int,
139 @WindowInsets.Type.InsetsType types: Int,
140 ): Boolean {
141 return displayId == thisDisplayId && (types and WindowInsets.Type.statusBars() != 0)
142 }
143
onSystemBarAttributesChangednull144 override fun onSystemBarAttributesChanged(
145 displayId: Int,
146 @Appearance appearance: Int,
147 appearanceRegions: Array<AppearanceRegion>,
148 navbarColorManagedByIme: Boolean,
149 @WindowInsetsController.Behavior behavior: Int,
150 @WindowInsets.Type.InsetsType requestedVisibleTypes: Int,
151 packageName: String,
152 letterboxDetails: Array<LetterboxDetails>,
153 ) {
154 if (displayId != thisDisplayId) return
155 _originalStatusBarAttributes.value =
156 StatusBarAttributes(
157 appearance,
158 appearanceRegions.toList(),
159 navbarColorManagedByIme,
160 requestedVisibleTypes,
161 letterboxDetails.toList(),
162 )
163 }
164 }
165
startnull166 fun start() {
167 commandQueue.addCallback(commandQueueCallback)
168 }
169
170 private val _isTransientShown = MutableStateFlow(false)
171 override val isTransientShown: StateFlow<Boolean> = _isTransientShown.asStateFlow()
172
173 private val _originalStatusBarAttributes = MutableStateFlow<StatusBarAttributes?>(null)
174
175 private val _statusBarBounds = MutableStateFlow(BoundsPair(Rect(), Rect()))
176
onStatusBarViewInitializednull177 override fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) {
178 val statusBarBoundsProvider = component.boundsProvider
179 val listener =
180 object : StatusBarBoundsProvider.BoundsChangeListener {
181 override fun onStatusBarBoundsChanged(bounds: BoundsPair) {
182 _statusBarBounds.value = bounds
183 }
184 }
185 statusBarBoundsProvider.addChangeListener(listener)
186 }
187
188 override val isInFullscreenMode: StateFlow<Boolean> =
189 _originalStatusBarAttributes
paramsnull190 .map { params ->
191 val requestedVisibleTypes = params?.requestedVisibleTypes ?: return@map false
192 // When the status bar is not requested visible, we assume we're in fullscreen mode.
193 requestedVisibleTypes and WindowInsets.Type.statusBars() == 0
194 }
195 .stateIn(scope, SharingStarted.Eagerly, false)
196
197 /** Modifies the raw [StatusBarAttributes] if letterboxing is needed. */
198 private val modifiedStatusBarAttributes: StateFlow<ModifiedStatusBarAttributes?> =
199 combine(
200 _originalStatusBarAttributes,
201 _statusBarBounds,
statusBarBoundsnull202 ) { originalAttributes, statusBarBounds ->
203 if (originalAttributes == null) {
204 null
205 } else {
206 val (newAppearance, newAppearanceRegions) =
207 modifyAppearanceIfNeeded(
208 originalAttributes.appearance,
209 originalAttributes.appearanceRegions,
210 originalAttributes.letterboxDetails,
211 statusBarBounds,
212 )
213 ModifiedStatusBarAttributes(
214 newAppearance,
215 newAppearanceRegions,
216 originalAttributes.navbarColorManagedByIme,
217 statusBarBounds,
218 )
219 }
220 }
221 .stateIn(scope, SharingStarted.Eagerly, initialValue = null)
222
223 override val statusBarAppearance: StateFlow<StatusBarAppearance?> =
224 combine(
225 modifiedStatusBarAttributes,
226 isTransientShown,
227 isInFullscreenMode,
228 ongoingCallRepository.ongoingCallState,
isTransientShownnull229 ) { modifiedAttributes, isTransientShown, isInFullscreenMode, ongoingCallState ->
230 if (modifiedAttributes == null) {
231 null
232 } else {
233 val statusBarMode =
234 toBarMode(
235 modifiedAttributes.appearance,
236 isTransientShown,
237 isInFullscreenMode,
238 hasOngoingCall = ongoingCallState is OngoingCallModel.InCall,
239 )
240 StatusBarAppearance(
241 statusBarMode,
242 modifiedAttributes.statusBarBounds,
243 modifiedAttributes.appearanceRegions,
244 modifiedAttributes.navbarColorManagedByIme,
245 )
246 }
247 }
248 .stateIn(scope, SharingStarted.Eagerly, initialValue = null)
249
250 override val statusBarMode: StateFlow<StatusBarMode> =
251 statusBarAppearance
<lambda>null252 .map { it?.mode ?: StatusBarMode.TRANSPARENT }
253 .stateIn(scope, SharingStarted.Eagerly, initialValue = StatusBarMode.TRANSPARENT)
254
toBarModenull255 private fun toBarMode(
256 appearance: Int,
257 isTransientShown: Boolean,
258 isInFullscreenMode: Boolean,
259 hasOngoingCall: Boolean,
260 ): StatusBarMode {
261 return when {
262 hasOngoingCall && isInFullscreenMode -> StatusBarMode.SEMI_TRANSPARENT
263 isTransientShown -> StatusBarMode.SEMI_TRANSPARENT
264 else -> appearance.toBarMode()
265 }
266 }
267
268 @Appearance
toBarModenull269 private fun Int.toBarMode(): StatusBarMode {
270 val lightsOutOpaque = APPEARANCE_LOW_PROFILE_BARS or APPEARANCE_OPAQUE_STATUS_BARS
271 return when {
272 this and lightsOutOpaque == lightsOutOpaque -> StatusBarMode.LIGHTS_OUT
273 this and APPEARANCE_LOW_PROFILE_BARS != 0 -> StatusBarMode.LIGHTS_OUT_TRANSPARENT
274 this and APPEARANCE_OPAQUE_STATUS_BARS != 0 -> StatusBarMode.OPAQUE
275 this and APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS != 0 -> StatusBarMode.SEMI_TRANSPARENT
276 else -> StatusBarMode.TRANSPARENT
277 }
278 }
279
showTransientnull280 override fun showTransient() {
281 _isTransientShown.value = true
282 }
283
clearTransientnull284 override fun clearTransient() {
285 _isTransientShown.value = false
286 }
287
modifyAppearanceIfNeedednull288 private fun modifyAppearanceIfNeeded(
289 appearance: Int,
290 appearanceRegions: List<AppearanceRegion>,
291 letterboxDetails: List<LetterboxDetails>,
292 statusBarBounds: BoundsPair,
293 ): Pair<Int, List<AppearanceRegion>> =
294 if (shouldUseLetterboxAppearance(letterboxDetails)) {
295 val letterboxAppearance =
296 letterboxAppearanceCalculator.getLetterboxAppearance(
297 appearance,
298 appearanceRegions,
299 letterboxDetails,
300 statusBarBounds,
301 )
302 Pair(letterboxAppearance.appearance, letterboxAppearance.appearanceRegions)
303 } else {
304 Pair(appearance, appearanceRegions)
305 }
306
shouldUseLetterboxAppearancenull307 private fun shouldUseLetterboxAppearance(letterboxDetails: List<LetterboxDetails>) =
308 letterboxDetails.isNotEmpty()
309
310 override fun dump(pw: PrintWriter, args: Array<out String>) {
311 pw.println("${_originalStatusBarAttributes.value}")
312 pw.println("${modifiedStatusBarAttributes.value}")
313 pw.println("statusBarMode: ${statusBarMode.value}")
314 }
315
316 /**
317 * Internal class keeping track of the raw status bar attributes received from the callback.
318 * Should never be exposed.
319 */
320 private data class StatusBarAttributes(
321 @Appearance val appearance: Int,
322 val appearanceRegions: List<AppearanceRegion>,
323 val navbarColorManagedByIme: Boolean,
324 @WindowInsets.Type.InsetsType val requestedVisibleTypes: Int,
325 val letterboxDetails: List<LetterboxDetails>,
326 ) {
toStringnull327 override fun toString(): String {
328 return """
329 StatusBarAttributes(
330 appearance=${appearance.toAppearanceString()},
331 appearanceRegions=$appearanceRegions,
332 navbarColorManagedByIme=$navbarColorManagedByIme,
333 requestedVisibleTypes=${requestedVisibleTypes.toWindowInsetsString()},
334 letterboxDetails=$letterboxDetails
335 )
336 """
337 .trimIndent()
338 }
339 }
340
341 /**
342 * Internal class keeping track of how [StatusBarAttributes] were transformed into new
343 * attributes based on letterboxing and other factors. Should never be exposed.
344 */
345 private data class ModifiedStatusBarAttributes(
346 @Appearance val appearance: Int,
347 val appearanceRegions: List<AppearanceRegion>,
348 val navbarColorManagedByIme: Boolean,
349 val statusBarBounds: BoundsPair,
350 ) {
toStringnull351 override fun toString(): String {
352 return """
353 ModifiedStatusBarAttributes(
354 appearance=${appearance.toAppearanceString()},
355 appearanceRegions=$appearanceRegions,
356 navbarColorManagedByIme=$navbarColorManagedByIme,
357 statusBarBounds=$statusBarBounds
358 )
359 """
360 .trimIndent()
361 }
362 }
363 }
364
toWindowInsetsStringnull365 private fun @receiver:WindowInsets.Type.InsetsType Int.toWindowInsetsString() =
366 "[${WindowInsets.Type.toString(this).replace(" ", ", ")}]"
367
368 private fun @receiver:Appearance Int.toAppearanceString() =
369 if (this == 0) {
370 "NONE"
371 } else {
372 ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", this)
373 }
374
375 @AssistedFactory
376 interface StatusBarModePerDisplayRepositoryFactory {
createnull377 fun create(@Assisted("displayId") displayId: Int): StatusBarModePerDisplayRepositoryImpl
378 }
379