1 /*
<lambda>null2 * Copyright (C) 2021 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.content.Context
20 import android.content.res.Resources
21 import android.graphics.Point
22 import android.graphics.Rect
23 import android.util.LruCache
24 import android.util.Pair
25 import android.view.DisplayCutout
26 import androidx.annotation.VisibleForTesting
27 import com.android.internal.policy.SystemBarUtils
28 import com.android.systemui.Dumpable
29 import com.android.systemui.R
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dump.DumpManager
32 import com.android.systemui.statusbar.policy.CallbackController
33 import com.android.systemui.statusbar.policy.ConfigurationController
34 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
35 import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE
36 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
37 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
38 import com.android.systemui.util.leak.RotationUtils.Rotation
39 import com.android.systemui.util.leak.RotationUtils.getExactRotation
40 import com.android.systemui.util.leak.RotationUtils.getResourcesForRotation
41 import com.android.systemui.util.traceSection
42
43 import java.io.PrintWriter
44 import java.lang.Math.max
45 import javax.inject.Inject
46
47 /**
48 * Encapsulates logic that can solve for the left/right insets required for the status bar contents.
49 * Takes into account:
50 * 1. rounded_corner_content_padding
51 * 2. status_bar_padding_start, status_bar_padding_end
52 * 2. display cutout insets from left or right
53 * 3. waterfall insets
54 *
55 *
56 * Importantly, these functions can determine status bar content left/right insets for any rotation
57 * before having done a layout pass in that rotation.
58 *
59 * NOTE: This class is not threadsafe
60 */
61 @SysUISingleton
62 class StatusBarContentInsetsProvider @Inject constructor(
63 val context: Context,
64 val configurationController: ConfigurationController,
65 val dumpManager: DumpManager
66 ) : CallbackController<StatusBarContentInsetsChangedListener>,
67 ConfigurationController.ConfigurationListener,
68 Dumpable {
69
70 // Limit cache size as potentially we may connect large number of displays
71 // (e.g. network displays)
72 private val insetsCache = LruCache<CacheKey, Rect>(MAX_CACHE_SIZE)
73 private val listeners = mutableSetOf<StatusBarContentInsetsChangedListener>()
74 private val isPrivacyDotEnabled: Boolean by lazy(LazyThreadSafetyMode.PUBLICATION) {
75 context.resources.getBoolean(R.bool.config_enablePrivacyDot)
76 }
77
78 init {
79 configurationController.addCallback(this)
80 dumpManager.registerDumpable(TAG, this)
81 }
82
83 override fun addCallback(listener: StatusBarContentInsetsChangedListener) {
84 listeners.add(listener)
85 }
86
87 override fun removeCallback(listener: StatusBarContentInsetsChangedListener) {
88 listeners.remove(listener)
89 }
90
91 override fun onDensityOrFontScaleChanged() {
92 clearCachedInsets()
93 }
94
95 override fun onThemeChanged() {
96 clearCachedInsets()
97 }
98
99 override fun onMaxBoundsChanged() {
100 notifyInsetsChanged()
101 }
102
103 private fun clearCachedInsets() {
104 insetsCache.evictAll()
105 notifyInsetsChanged()
106 }
107
108 private fun notifyInsetsChanged() {
109 listeners.forEach {
110 it.onStatusBarContentInsetsChanged()
111 }
112 }
113
114 /**
115 * Some views may need to care about whether or not the current top display cutout is located
116 * in the corner rather than somewhere in the center. In the case of a corner cutout, the
117 * status bar area is contiguous.
118 */
119 fun currentRotationHasCornerCutout(): Boolean {
120 val cutout = context.display.cutout ?: return false
121 val topBounds = cutout.boundingRectTop
122
123 val point = Point()
124 context.display.getRealSize(point)
125
126 return topBounds.left <= 0 || topBounds.right >= point.x
127 }
128
129 /**
130 * Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy
131 * dot in the coordinates relative to the given rotation.
132 *
133 * @param rotation the rotation for which the bounds are required. This is an absolute value
134 * (i.e., ROTATION_NONE will always return the same bounds regardless of the context
135 * from which this method is called)
136 */
137 fun getBoundingRectForPrivacyChipForRotation(@Rotation rotation: Int,
138 displayCutout: DisplayCutout?): Rect {
139 val key = getCacheKey(rotation, displayCutout)
140 var insets = insetsCache[key]
141 if (insets == null) {
142 insets = getStatusBarContentAreaForRotation(rotation)
143 }
144
145 val rotatedResources = getResourcesForRotation(rotation, context)
146
147 val dotWidth = rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
148 val chipWidth = rotatedResources.getDimensionPixelSize(
149 R.dimen.ongoing_appops_chip_max_width)
150
151 val isRtl = configurationController.isLayoutRtl
152 return getPrivacyChipBoundingRectForInsets(insets, dotWidth, chipWidth, isRtl)
153 }
154
155 /**
156 * Calculate the distance from the left and right edges of the screen to the status bar
157 * content area. This differs from the content area rects in that these values can be used
158 * directly as padding.
159 *
160 * @param rotation the target rotation for which to calculate insets
161 */
162 fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Pair<Int, Int> =
163 traceSection(tag = "StatusBarContentInsetsProvider.getStatusBarContentInsetsForRotation") {
164 val displayCutout = context.display.cutout
165 val key = getCacheKey(rotation, displayCutout)
166
167 val screenBounds = context.resources.configuration.windowConfiguration.maxBounds
168 val point = Point(screenBounds.width(), screenBounds.height())
169
170 // Target rotation can be a different orientation than the current device rotation
171 point.orientToRotZero(getExactRotation(context))
172 val width = point.logicalWidth(rotation)
173
174 val area = insetsCache[key] ?: getAndSetCalculatedAreaForRotation(
175 rotation, displayCutout, getResourcesForRotation(rotation, context), key)
176
177 Pair(area.left, width - area.right)
178 }
179
180 /**
181 * Calculate the left and right insets for the status bar content in the device's current
182 * rotation
183 * @see getStatusBarContentAreaForRotation
184 */
185 fun getStatusBarContentInsetsForCurrentRotation(): Pair<Int, Int> {
186 return getStatusBarContentInsetsForRotation(getExactRotation(context))
187 }
188
189 /**
190 * Calculates the area of the status bar contents invariant of the current device rotation,
191 * in the target rotation's coordinates
192 *
193 * @param rotation the rotation for which the bounds are required. This is an absolute value
194 * (i.e., ROTATION_NONE will always return the same bounds regardless of the context
195 * from which this method is called)
196 */
197 @JvmOverloads
198 fun getStatusBarContentAreaForRotation(
199 @Rotation rotation: Int
200 ): Rect {
201 val displayCutout = context.display.cutout
202 val key = getCacheKey(rotation, displayCutout)
203 return insetsCache[key] ?: getAndSetCalculatedAreaForRotation(
204 rotation, displayCutout, getResourcesForRotation(rotation, context), key)
205 }
206
207 /**
208 * Get the status bar content area for the given rotation, in absolute bounds
209 */
210 fun getStatusBarContentAreaForCurrentRotation(): Rect {
211 val rotation = getExactRotation(context)
212 return getStatusBarContentAreaForRotation(rotation)
213 }
214
215 private fun getAndSetCalculatedAreaForRotation(
216 @Rotation targetRotation: Int,
217 displayCutout: DisplayCutout?,
218 rotatedResources: Resources,
219 key: CacheKey
220 ): Rect {
221 return getCalculatedAreaForRotation(displayCutout, targetRotation, rotatedResources)
222 .also {
223 insetsCache.put(key, it)
224 }
225 }
226
227 private fun getCalculatedAreaForRotation(
228 displayCutout: DisplayCutout?,
229 @Rotation targetRotation: Int,
230 rotatedResources: Resources
231 ): Rect {
232 val currentRotation = getExactRotation(context)
233
234 val roundedCornerPadding = rotatedResources
235 .getDimensionPixelSize(R.dimen.rounded_corner_content_padding)
236 val minDotPadding = if (isPrivacyDotEnabled)
237 rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_min_padding)
238 else 0
239 val dotWidth = if (isPrivacyDotEnabled)
240 rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
241 else 0
242
243 val minLeft: Int
244 val minRight: Int
245 if (configurationController.isLayoutRtl) {
246 minLeft = max(minDotPadding, roundedCornerPadding)
247 minRight = roundedCornerPadding
248 } else {
249 minLeft = roundedCornerPadding
250 minRight = max(minDotPadding, roundedCornerPadding)
251 }
252
253 return calculateInsetsForRotationWithRotatedResources(
254 currentRotation,
255 targetRotation,
256 displayCutout,
257 context.resources.configuration.windowConfiguration.maxBounds,
258 SystemBarUtils.getStatusBarHeightForRotation(context, targetRotation),
259 minLeft,
260 minRight,
261 configurationController.isLayoutRtl,
262 dotWidth)
263 }
264
265 fun getStatusBarPaddingTop(@Rotation rotation: Int? = null): Int {
266 val res = rotation?.let { it -> getResourcesForRotation(it, context) } ?: context.resources
267 return res.getDimensionPixelSize(R.dimen.status_bar_padding_top)
268 }
269
270 override fun dump(pw: PrintWriter, args: Array<out String>) {
271 insetsCache.snapshot().forEach { (key, rect) ->
272 pw.println("$key -> $rect")
273 }
274 pw.println(insetsCache)
275 }
276
277 private fun getCacheKey(
278 @Rotation rotation: Int,
279 displayCutout: DisplayCutout?): CacheKey =
280 CacheKey(
281 rotation = rotation,
282 displaySize = Rect(context.resources.configuration.windowConfiguration.maxBounds),
283 displayCutout = displayCutout
284 )
285
286 private data class CacheKey(
287 @Rotation val rotation: Int,
288 val displaySize: Rect,
289 val displayCutout: DisplayCutout?
290 )
291 }
292
293 interface StatusBarContentInsetsChangedListener {
onStatusBarContentInsetsChangednull294 fun onStatusBarContentInsetsChanged()
295 }
296
297 private const val TAG = "StatusBarInsetsProvider"
298 private const val MAX_CACHE_SIZE = 16
299
300 private fun getRotationZeroDisplayBounds(bounds: Rect, @Rotation exactRotation: Int): Rect {
301 if (exactRotation == ROTATION_NONE || exactRotation == ROTATION_UPSIDE_DOWN) {
302 return bounds
303 }
304
305 // bounds are horizontal, swap height and width
306 return Rect(0, 0, bounds.bottom, bounds.right)
307 }
308
309 @VisibleForTesting
getPrivacyChipBoundingRectForInsetsnull310 fun getPrivacyChipBoundingRectForInsets(
311 contentRect: Rect,
312 dotWidth: Int,
313 chipWidth: Int,
314 isRtl: Boolean
315 ): Rect {
316 return if (isRtl) {
317 Rect(contentRect.left - dotWidth,
318 contentRect.top,
319 contentRect.left + chipWidth,
320 contentRect.bottom)
321 } else {
322 Rect(contentRect.right - chipWidth,
323 contentRect.top,
324 contentRect.right + dotWidth,
325 contentRect.bottom)
326 }
327 }
328
329 /**
330 * Calculates the exact left and right positions for the status bar contents for the given
331 * rotation
332 *
333 * @param currentRotation current device rotation
334 * @param targetRotation rotation for which to calculate the status bar content rect
335 * @param displayCutout [DisplayCutout] for the current display. possibly null
336 * @param maxBounds the display bounds in our current rotation
337 * @param statusBarHeight height of the status bar for the target rotation
338 * @param minLeft the minimum padding to enforce on the left
339 * @param minRight the minimum padding to enforce on the right
340 * @param isRtl current layout direction is Right-To-Left or not
341 * @param dotWidth privacy dot image width (0 if privacy dot is disabled)
342 *
343 * @see [RotationUtils#getResourcesForRotation]
344 */
calculateInsetsForRotationWithRotatedResourcesnull345 fun calculateInsetsForRotationWithRotatedResources(
346 @Rotation currentRotation: Int,
347 @Rotation targetRotation: Int,
348 displayCutout: DisplayCutout?,
349 maxBounds: Rect,
350 statusBarHeight: Int,
351 minLeft: Int,
352 minRight: Int,
353 isRtl: Boolean,
354 dotWidth: Int
355 ): Rect {
356 /*
357 TODO: Check if this is ever used for devices with no rounded corners
358 val left = if (isRtl) paddingEnd else paddingStart
359 val right = if (isRtl) paddingStart else paddingEnd
360 */
361
362 val rotZeroBounds = getRotationZeroDisplayBounds(maxBounds, currentRotation)
363
364 val sbLeftRight = getStatusBarLeftRight(
365 displayCutout,
366 statusBarHeight,
367 rotZeroBounds.right,
368 rotZeroBounds.bottom,
369 maxBounds.width(),
370 maxBounds.height(),
371 minLeft,
372 minRight,
373 isRtl,
374 dotWidth,
375 targetRotation,
376 currentRotation)
377
378 return sbLeftRight
379 }
380
381 /**
382 * Calculate the insets needed from the left and right edges for the given rotation.
383 *
384 * @param displayCutout Device display cutout
385 * @param sbHeight appropriate status bar height for this rotation
386 * @param width display width calculated for ROTATION_NONE
387 * @param height display height calculated for ROTATION_NONE
388 * @param cWidth display width in our current rotation
389 * @param cHeight display height in our current rotation
390 * @param minLeft the minimum padding to enforce on the left
391 * @param minRight the minimum padding to enforce on the right
392 * @param isRtl current layout direction is Right-To-Left or not
393 * @param dotWidth privacy dot image width (0 if privacy dot is disabled)
394 * @param targetRotation the rotation for which to calculate margins
395 * @param currentRotation the rotation from which the display cutout was generated
396 *
397 * @return a Rect which exactly calculates the Status Bar's content rect relative to the target
398 * rotation
399 */
getStatusBarLeftRightnull400 private fun getStatusBarLeftRight(
401 displayCutout: DisplayCutout?,
402 sbHeight: Int,
403 width: Int,
404 height: Int,
405 cWidth: Int,
406 cHeight: Int,
407 minLeft: Int,
408 minRight: Int,
409 isRtl: Boolean,
410 dotWidth: Int,
411 @Rotation targetRotation: Int,
412 @Rotation currentRotation: Int
413 ): Rect {
414
415 val logicalDisplayWidth = if (targetRotation.isHorizontal()) height else width
416
417 val cutoutRects = displayCutout?.boundingRects
418 if (cutoutRects == null || cutoutRects.isEmpty()) {
419 return Rect(minLeft,
420 0,
421 logicalDisplayWidth - minRight,
422 sbHeight)
423 }
424
425 val relativeRotation = if (currentRotation - targetRotation < 0) {
426 currentRotation - targetRotation + 4
427 } else {
428 currentRotation - targetRotation
429 }
430
431 // Size of the status bar window for the given rotation relative to our exact rotation
432 val sbRect = sbRect(relativeRotation, sbHeight, Pair(cWidth, cHeight))
433
434 var leftMargin = minLeft
435 var rightMargin = minRight
436 for (cutoutRect in cutoutRects) {
437 // There is at most one non-functional area per short edge of the device. So if the status
438 // bar doesn't share a short edge with the cutout, we can ignore its insets because there
439 // will be no letter-boxing to worry about
440 if (!shareShortEdge(sbRect, cutoutRect, cWidth, cHeight)) {
441 continue
442 }
443
444 if (cutoutRect.touchesLeftEdge(relativeRotation, cWidth, cHeight)) {
445 var logicalWidth = cutoutRect.logicalWidth(relativeRotation)
446 if (isRtl) logicalWidth += dotWidth
447 leftMargin = max(logicalWidth, leftMargin)
448 } else if (cutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) {
449 var logicalWidth = cutoutRect.logicalWidth(relativeRotation)
450 if (!isRtl) logicalWidth += dotWidth
451 rightMargin = max(rightMargin, logicalWidth)
452 }
453 // TODO(b/203626889): Fix the scenario when config_mainBuiltInDisplayCutoutRectApproximation
454 // is very close to but not directly touch edges.
455 }
456
457 return Rect(leftMargin, 0, logicalDisplayWidth - rightMargin, sbHeight)
458 }
459
sbRectnull460 private fun sbRect(
461 @Rotation relativeRotation: Int,
462 sbHeight: Int,
463 displaySize: Pair<Int, Int>
464 ): Rect {
465 val w = displaySize.first
466 val h = displaySize.second
467 return when (relativeRotation) {
468 ROTATION_NONE -> Rect(0, 0, w, sbHeight)
469 ROTATION_LANDSCAPE -> Rect(0, 0, sbHeight, h)
470 ROTATION_UPSIDE_DOWN -> Rect(0, h - sbHeight, w, h)
471 else -> Rect(w - sbHeight, 0, w, h)
472 }
473 }
474
shareShortEdgenull475 private fun shareShortEdge(
476 sbRect: Rect,
477 cutoutRect: Rect,
478 currentWidth: Int,
479 currentHeight: Int
480 ): Boolean {
481 if (currentWidth < currentHeight) {
482 // Check top/bottom edges by extending the width of the display cutout rect and checking
483 // for intersections
484 return sbRect.intersects(0, cutoutRect.top, currentWidth, cutoutRect.bottom)
485 } else if (currentWidth > currentHeight) {
486 // Short edge is the height, extend that one this time
487 return sbRect.intersects(cutoutRect.left, 0, cutoutRect.right, currentHeight)
488 }
489
490 return false
491 }
492
touchesRightEdgenull493 private fun Rect.touchesRightEdge(@Rotation rot: Int, width: Int, height: Int): Boolean {
494 return when (rot) {
495 ROTATION_NONE -> right >= width
496 ROTATION_LANDSCAPE -> top <= 0
497 ROTATION_UPSIDE_DOWN -> left <= 0
498 else /* SEASCAPE */ -> bottom >= height
499 }
500 }
501
Rectnull502 private fun Rect.touchesLeftEdge(@Rotation rot: Int, width: Int, height: Int): Boolean {
503 return when (rot) {
504 ROTATION_NONE -> left <= 0
505 ROTATION_LANDSCAPE -> bottom >= height
506 ROTATION_UPSIDE_DOWN -> right >= width
507 else /* SEASCAPE */ -> top <= 0
508 }
509 }
510
Rectnull511 private fun Rect.logicalTop(@Rotation rot: Int): Int {
512 return when (rot) {
513 ROTATION_NONE -> top
514 ROTATION_LANDSCAPE -> left
515 ROTATION_UPSIDE_DOWN -> bottom
516 else /* SEASCAPE */ -> right
517 }
518 }
519
Rectnull520 private fun Rect.logicalRight(@Rotation rot: Int): Int {
521 return when (rot) {
522 ROTATION_NONE -> right
523 ROTATION_LANDSCAPE -> top
524 ROTATION_UPSIDE_DOWN -> left
525 else /* SEASCAPE */ -> bottom
526 }
527 }
528
Rectnull529 private fun Rect.logicalLeft(@Rotation rot: Int): Int {
530 return when (rot) {
531 ROTATION_NONE -> left
532 ROTATION_LANDSCAPE -> bottom
533 ROTATION_UPSIDE_DOWN -> right
534 else /* SEASCAPE */ -> top
535 }
536 }
537
Rectnull538 private fun Rect.logicalWidth(@Rotation rot: Int): Int {
539 return when (rot) {
540 ROTATION_NONE, ROTATION_UPSIDE_DOWN -> width()
541 else /* LANDSCAPE, SEASCAPE */ -> height()
542 }
543 }
544
Intnull545 private fun Int.isHorizontal(): Boolean {
546 return this == ROTATION_LANDSCAPE || this == ROTATION_SEASCAPE
547 }
548
Pointnull549 private fun Point.orientToRotZero(@Rotation rot: Int) {
550 when (rot) {
551 ROTATION_NONE, ROTATION_UPSIDE_DOWN -> return
552 else -> {
553 // swap width and height to zero-orient bounds
554 val yTmp = y
555 y = x
556 x = yTmp
557 }
558 }
559 }
560
Pointnull561 private fun Point.logicalWidth(@Rotation rot: Int): Int {
562 return when (rot) {
563 ROTATION_NONE, ROTATION_UPSIDE_DOWN -> x
564 else -> y
565 }
566 }
567