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.layout
18
19 import android.annotation.Px
20 import android.content.Context
21 import android.content.res.Resources
22 import android.graphics.Insets
23 import android.graphics.Point
24 import android.graphics.Rect
25 import android.util.LruCache
26 import android.util.Pair
27 import android.view.Display.DEFAULT_DISPLAY
28 import android.view.DisplayCutout
29 import android.view.Surface
30 import androidx.annotation.VisibleForTesting
31 import com.android.app.tracing.traceSection
32 import com.android.internal.policy.SystemBarUtils
33 import com.android.systemui.BottomMarginCommand
34 import com.android.systemui.Dumpable
35 import com.android.systemui.StatusBarInsetsCommand
36 import com.android.systemui.SysUICutoutInformation
37 import com.android.systemui.SysUICutoutProvider
38 import com.android.systemui.dump.DumpManager
39 import com.android.systemui.res.R
40 import com.android.systemui.statusbar.commandline.CommandRegistry
41 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
42 import com.android.systemui.statusbar.policy.CallbackController
43 import com.android.systemui.statusbar.policy.ConfigurationController
44 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
45 import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE
46 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
47 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
48 import com.android.systemui.util.leak.RotationUtils.Rotation
49 import com.android.systemui.util.leak.RotationUtils.getExactRotation
50 import com.android.systemui.util.leak.RotationUtils.getResourcesForRotation
51 import dagger.assisted.Assisted
52 import dagger.assisted.AssistedFactory
53 import dagger.assisted.AssistedInject
54 import java.io.PrintWriter
55 import java.lang.Math.max
56 import java.util.concurrent.CopyOnWriteArraySet
57
58 /**
59 * Encapsulates logic that can solve for the left/right insets required for the status bar contents.
60 * Takes into account:
61 * 1. rounded_corner_content_padding
62 * 2. status_bar_padding_start, status_bar_padding_end
63 * 2. display cutout insets from left or right
64 * 3. waterfall insets
65 *
66 * Importantly, these functions can determine status bar content left/right insets for any rotation
67 * before having done a layout pass in that rotation.
68 *
69 * NOTE: This class is not threadsafe
70 */
71 interface StatusBarContentInsetsProvider :
72 CallbackController<StatusBarContentInsetsChangedListener> {
73
74 /**
75 * Called when the [StatusBarContentInsetsProvider] should start doing its work and allocate its
76 * resources.
77 */
78 fun start()
79
80 /**
81 * Called when the [StatusBarContentInsetsProvider] should stop and do any required clean up.
82 */
83 fun stop()
84
85 /**
86 * Some views may need to care about whether or not the current top display cutout is located in
87 * the corner rather than somewhere in the center. In the case of a corner cutout, the status
88 * bar area is contiguous.
89 */
90 fun currentRotationHasCornerCutout(): Boolean
91
92 /**
93 * Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy
94 * dot in the coordinates relative to the given rotation.
95 *
96 * @param rotation the rotation for which the bounds are required. This is an absolute value
97 * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from
98 * which this method is called)
99 */
100 fun getBoundingRectForPrivacyChipForRotation(
101 @Rotation rotation: Int,
102 displayCutout: DisplayCutout?,
103 ): Rect
104
105 /**
106 * Calculate the distance from the left, right and top edges of the screen to the status bar
107 * content area. This differs from the content area rects in that these values can be used
108 * directly as padding.
109 *
110 * @param rotation the target rotation for which to calculate insets
111 */
112 fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets
113
114 /**
115 * Calculate the insets for the status bar content in the device's current rotation
116 *
117 * @see getStatusBarContentAreaForRotation
118 */
119 fun getStatusBarContentInsetsForCurrentRotation(): Insets
120
121 /**
122 * Calculates the area of the status bar contents invariant of the current device rotation, in
123 * the target rotation's coordinates
124 *
125 * @param rotation the rotation for which the bounds are required. This is an absolute value
126 * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from
127 * which this method is called)
128 */
129 fun getStatusBarContentAreaForRotation(@Rotation rotation: Int): Rect
130
131 /** Get the status bar content area for the given rotation, in absolute bounds */
132 fun getStatusBarContentAreaForCurrentRotation(): Rect
133
134 fun getStatusBarPaddingTop(@Rotation rotation: Int? = null): Int
135
136 interface Factory {
137 fun create(
138 context: Context,
139 configurationController: ConfigurationController,
140 sysUICutoutProvider: SysUICutoutProvider,
141 ): StatusBarContentInsetsProvider
142 }
143 }
144
145 class StatusBarContentInsetsProviderImpl
146 @AssistedInject
147 constructor(
148 @Assisted val context: Context,
149 @Assisted val configurationController: ConfigurationController,
150 val dumpManager: DumpManager,
151 val commandRegistry: CommandRegistry,
152 @Assisted val sysUICutoutProvider: SysUICutoutProvider,
153 ) : StatusBarContentInsetsProvider, ConfigurationController.ConfigurationListener, Dumpable {
154
155 @AssistedFactory
156 interface Factory : StatusBarContentInsetsProvider.Factory {
createnull157 override fun create(
158 context: Context,
159 configurationController: ConfigurationController,
160 sysUICutoutProvider: SysUICutoutProvider,
161 ): StatusBarContentInsetsProviderImpl
162 }
163
164 // Limit cache size as potentially we may connect large number of displays
165 // (e.g. network displays)
166 private val insetsCache = LruCache<CacheKey, Rect>(MAX_CACHE_SIZE)
167 private val listeners = CopyOnWriteArraySet<StatusBarContentInsetsChangedListener>()
168 private val isPrivacyDotEnabled: Boolean by
169 lazy(LazyThreadSafetyMode.PUBLICATION) {
170 context.resources.getBoolean(R.bool.config_enablePrivacyDot)
171 }
172
173 private val nameSuffix =
174 if (context.displayId == DEFAULT_DISPLAY) "" else context.displayId.toString()
175 private val dumpableName = TAG + nameSuffix
176 private val commandName = StatusBarInsetsCommand.NAME + nameSuffix
177
178 init {
179 if (!StatusBarConnectedDisplays.isEnabled) {
180 // Call start(), since it is not called when the flag is disabled, to keep the old
181 // behavior as it was.
182 start()
183 }
184 }
185
startnull186 override fun start() {
187 configurationController.addCallback(this)
188 dumpManager.registerNormalDumpable(dumpableName, this)
189 commandRegistry.registerCommand(commandName) {
190 StatusBarInsetsCommand(
191 object : StatusBarInsetsCommand.Callback {
192 override fun onExecute(
193 command: StatusBarInsetsCommand,
194 printWriter: PrintWriter,
195 ) {
196 executeCommand(command, printWriter)
197 }
198 }
199 )
200 }
201 }
202
stopnull203 override fun stop() {
204 StatusBarConnectedDisplays.unsafeAssertInNewMode()
205 configurationController.removeCallback(this)
206 dumpManager.unregisterDumpable(dumpableName)
207 commandRegistry.unregisterCommand(commandName)
208 }
209
addCallbacknull210 override fun addCallback(listener: StatusBarContentInsetsChangedListener) {
211 listeners.add(listener)
212 }
213
removeCallbacknull214 override fun removeCallback(listener: StatusBarContentInsetsChangedListener) {
215 listeners.remove(listener)
216 }
217
onDensityOrFontScaleChangednull218 override fun onDensityOrFontScaleChanged() {
219 clearCachedInsets()
220 }
221
onThemeChangednull222 override fun onThemeChanged() {
223 clearCachedInsets()
224 }
225
onMaxBoundsChangednull226 override fun onMaxBoundsChanged() {
227 notifyInsetsChanged()
228 }
229
clearCachedInsetsnull230 private fun clearCachedInsets() {
231 insetsCache.evictAll()
232 notifyInsetsChanged()
233 }
234
notifyInsetsChangednull235 private fun notifyInsetsChanged() {
236 listeners.forEach { it.onStatusBarContentInsetsChanged() }
237 }
238
currentRotationHasCornerCutoutnull239 override fun currentRotationHasCornerCutout(): Boolean {
240 val cutout = checkNotNull(context.display).cutout ?: return false
241 val topBounds = cutout.boundingRectTop
242
243 val point = Point()
244 checkNotNull(context.display).getRealSize(point)
245
246 return topBounds.left <= 0 || topBounds.right >= point.x
247 }
248
getBoundingRectForPrivacyChipForRotationnull249 override fun getBoundingRectForPrivacyChipForRotation(
250 @Rotation rotation: Int,
251 displayCutout: DisplayCutout?,
252 ): Rect {
253 val key = getCacheKey(rotation, displayCutout)
254 var insets = insetsCache[key]
255 if (insets == null) {
256 insets = getStatusBarContentAreaForRotation(rotation)
257 }
258
259 val rotatedResources = getResourcesForRotation(rotation, context)
260
261 val dotWidth = rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
262 val chipWidth =
263 rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_max_width)
264
265 val isRtl = configurationController.isLayoutRtl
266 return getPrivacyChipBoundingRectForInsets(insets, dotWidth, chipWidth, isRtl)
267 }
268
getStatusBarContentInsetsForRotationnull269 override fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets =
270 traceSection(tag = "StatusBarContentInsetsProvider.getStatusBarContentInsetsForRotation") {
271 val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplayAndRotation()
272 val displayCutout = sysUICutout?.cutout
273 val key = getCacheKey(rotation, displayCutout)
274
275 val screenBounds = context.resources.configuration.windowConfiguration.maxBounds
276 val point = Point(screenBounds.width(), screenBounds.height())
277
278 // Target rotation can be a different orientation than the current device rotation
279 point.orientToRotZero(getExactRotation(context))
280 val width = point.logicalWidth(rotation)
281
282 val area =
283 insetsCache[key]
284 ?: getAndSetCalculatedAreaForRotation(
285 rotation,
286 sysUICutout,
287 getResourcesForRotation(rotation, context),
288 key,
289 )
290
291 Insets.of(area.left, area.top, /* right= */ width - area.right, /* bottom= */ 0)
292 }
293
getStatusBarContentInsetsForCurrentRotationnull294 override fun getStatusBarContentInsetsForCurrentRotation(): Insets {
295 return getStatusBarContentInsetsForRotation(getExactRotation(context))
296 }
297
getStatusBarContentAreaForRotationnull298 override fun getStatusBarContentAreaForRotation(@Rotation rotation: Int): Rect {
299 val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplayAndRotation()
300 val displayCutout = sysUICutout?.cutout
301 val key = getCacheKey(rotation, displayCutout)
302 return insetsCache[key]
303 ?: getAndSetCalculatedAreaForRotation(
304 rotation,
305 sysUICutout,
306 getResourcesForRotation(rotation, context),
307 key,
308 )
309 }
310
getStatusBarContentAreaForCurrentRotationnull311 override fun getStatusBarContentAreaForCurrentRotation(): Rect {
312 val rotation = getExactRotation(context)
313 return getStatusBarContentAreaForRotation(rotation)
314 }
315
getAndSetCalculatedAreaForRotationnull316 private fun getAndSetCalculatedAreaForRotation(
317 @Rotation targetRotation: Int,
318 sysUICutout: SysUICutoutInformation?,
319 rotatedResources: Resources,
320 key: CacheKey,
321 ): Rect {
322 return getCalculatedAreaForRotation(sysUICutout, targetRotation, rotatedResources).also {
323 insetsCache.put(key, it)
324 }
325 }
326
getCalculatedAreaForRotationnull327 private fun getCalculatedAreaForRotation(
328 sysUICutout: SysUICutoutInformation?,
329 @Rotation targetRotation: Int,
330 rotatedResources: Resources,
331 ): Rect {
332 val currentRotation = getExactRotation(context)
333
334 val roundedCornerPadding =
335 rotatedResources.getDimensionPixelSize(R.dimen.rounded_corner_content_padding)
336 val minDotPadding =
337 if (isPrivacyDotEnabled)
338 rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_min_padding)
339 else 0
340 val dotWidth =
341 if (isPrivacyDotEnabled)
342 rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
343 else 0
344
345 val minLeft: Int
346 val minRight: Int
347 if (configurationController.isLayoutRtl) {
348 minLeft = max(minDotPadding, roundedCornerPadding)
349 minRight = roundedCornerPadding
350 } else {
351 minLeft = roundedCornerPadding
352 minRight = max(minDotPadding, roundedCornerPadding)
353 }
354
355 val bottomAlignedMargin = getBottomAlignedMargin(targetRotation, rotatedResources)
356 val statusBarContentHeight =
357 rotatedResources.getDimensionPixelSize(R.dimen.status_bar_icon_size_sp)
358
359 return calculateInsetsForRotationWithRotatedResources(
360 currentRotation,
361 targetRotation,
362 sysUICutout,
363 context.resources.configuration.windowConfiguration.maxBounds,
364 SystemBarUtils.getStatusBarHeightForRotation(context, targetRotation),
365 minLeft,
366 minRight,
367 configurationController.isLayoutRtl,
368 dotWidth,
369 bottomAlignedMargin,
370 statusBarContentHeight,
371 )
372 }
373
executeCommandnull374 private fun executeCommand(command: StatusBarInsetsCommand, printWriter: PrintWriter) {
375 command.bottomMargin?.let { executeBottomMarginCommand(it, printWriter) }
376 }
377
executeBottomMarginCommandnull378 private fun executeBottomMarginCommand(command: BottomMarginCommand, printWriter: PrintWriter) {
379 val rotation = command.rotationValue
380 if (rotation == null) {
381 printWriter.println(
382 "Rotation should be one of ${BottomMarginCommand.ROTATION_DEGREES_OPTIONS}"
383 )
384 return
385 }
386 val marginBottomDp = command.marginBottomDp
387 if (marginBottomDp == null) {
388 printWriter.println("Margin bottom not set.")
389 return
390 }
391 setBottomMarginOverride(rotation, marginBottomDp)
392 }
393
394 private val marginBottomOverrides = mutableMapOf<Int, Int>()
395
setBottomMarginOverridenull396 private fun setBottomMarginOverride(rotation: Int, marginBottomDp: Float) {
397 insetsCache.evictAll()
398 val marginBottomPx = (marginBottomDp * context.resources.displayMetrics.density).toInt()
399 marginBottomOverrides[rotation] = marginBottomPx
400 notifyInsetsChanged()
401 }
402
403 @Px
getBottomAlignedMarginnull404 private fun getBottomAlignedMargin(targetRotation: Int, resources: Resources): Int {
405 val override = marginBottomOverrides[targetRotation]
406 if (override != null) {
407 return override
408 }
409 val dimenRes =
410 when (targetRotation) {
411 Surface.ROTATION_0 -> R.dimen.status_bar_bottom_aligned_margin_rotation_0
412 Surface.ROTATION_90 -> R.dimen.status_bar_bottom_aligned_margin_rotation_90
413 Surface.ROTATION_180 -> R.dimen.status_bar_bottom_aligned_margin_rotation_180
414 Surface.ROTATION_270 -> R.dimen.status_bar_bottom_aligned_margin_rotation_270
415 else -> throw IllegalStateException("Unknown rotation: $targetRotation")
416 }
417 return resources.getDimensionPixelSize(dimenRes)
418 }
419
getStatusBarPaddingTopnull420 override fun getStatusBarPaddingTop(@Rotation rotation: Int?): Int {
421 val res = rotation?.let { it -> getResourcesForRotation(it, context) } ?: context.resources
422 return res.getDimensionPixelSize(R.dimen.status_bar_padding_top)
423 }
424
dumpnull425 override fun dump(pw: PrintWriter, args: Array<out String>) {
426 insetsCache.snapshot().forEach { (key, rect) -> pw.println("$key -> $rect") }
427 pw.println(insetsCache)
428 pw.println("Bottom margin overrides: $marginBottomOverrides")
429 }
430
getCacheKeynull431 private fun getCacheKey(@Rotation rotation: Int, displayCutout: DisplayCutout?): CacheKey =
432 CacheKey(
433 rotation = rotation,
434 displaySize = Rect(context.resources.configuration.windowConfiguration.maxBounds),
435 displayCutout = displayCutout,
436 )
437
438 private data class CacheKey(
439 @Rotation val rotation: Int,
440 val displaySize: Rect,
441 val displayCutout: DisplayCutout?,
442 )
443 }
444
445 interface StatusBarContentInsetsChangedListener {
446 fun onStatusBarContentInsetsChanged()
447 }
448
449 private const val TAG = "StatusBarInsetsProvider"
450 private const val MAX_CACHE_SIZE = 16
451
getRotationZeroDisplayBoundsnull452 private fun getRotationZeroDisplayBounds(bounds: Rect, @Rotation exactRotation: Int): Rect {
453 if (exactRotation == ROTATION_NONE || exactRotation == ROTATION_UPSIDE_DOWN) {
454 return bounds
455 }
456
457 // bounds are horizontal, swap height and width
458 return Rect(0, 0, bounds.bottom, bounds.right)
459 }
460
461 @VisibleForTesting
getPrivacyChipBoundingRectForInsetsnull462 fun getPrivacyChipBoundingRectForInsets(
463 contentRect: Rect,
464 dotWidth: Int,
465 chipWidth: Int,
466 isRtl: Boolean,
467 ): Rect {
468 return if (isRtl) {
469 Rect(
470 contentRect.left - dotWidth,
471 contentRect.top,
472 contentRect.left + chipWidth,
473 contentRect.bottom,
474 )
475 } else {
476 Rect(
477 contentRect.right - chipWidth,
478 contentRect.top,
479 contentRect.right + dotWidth,
480 contentRect.bottom,
481 )
482 }
483 }
484
485 /**
486 * Calculates the exact left and right positions for the status bar contents for the given rotation
487 *
488 * @param currentRotation current device rotation
489 * @param targetRotation rotation for which to calculate the status bar content rect
490 * @param displayCutout [DisplayCutout] for the current display. possibly null
491 * @param maxBounds the display bounds in our current rotation
492 * @param statusBarHeight height of the status bar for the target rotation
493 * @param minLeft the minimum padding to enforce on the left
494 * @param minRight the minimum padding to enforce on the right
495 * @param isRtl current layout direction is Right-To-Left or not
496 * @param dotWidth privacy dot image width (0 if privacy dot is disabled)
497 * @param bottomAlignedMargin the bottom margin that the status bar content should have. -1 if none,
498 * and content should be centered vertically.
499 * @param statusBarContentHeight the height of the status bar contents (icons, text, etc)
500 * @see [RotationUtils#getResourcesForRotation]
501 */
502 @VisibleForTesting
calculateInsetsForRotationWithRotatedResourcesnull503 fun calculateInsetsForRotationWithRotatedResources(
504 @Rotation currentRotation: Int,
505 @Rotation targetRotation: Int,
506 sysUICutout: SysUICutoutInformation?,
507 maxBounds: Rect,
508 statusBarHeight: Int,
509 minLeft: Int,
510 minRight: Int,
511 isRtl: Boolean,
512 dotWidth: Int,
513 bottomAlignedMargin: Int,
514 statusBarContentHeight: Int,
515 ): Rect {
516 /*
517 TODO: Check if this is ever used for devices with no rounded corners
518 val left = if (isRtl) paddingEnd else paddingStart
519 val right = if (isRtl) paddingStart else paddingEnd
520 */
521
522 val rotZeroBounds = getRotationZeroDisplayBounds(maxBounds, currentRotation)
523
524 return getStatusBarContentBounds(
525 sysUICutout,
526 statusBarHeight,
527 rotZeroBounds.right,
528 rotZeroBounds.bottom,
529 maxBounds.width(),
530 maxBounds.height(),
531 minLeft,
532 minRight,
533 isRtl,
534 dotWidth,
535 targetRotation,
536 currentRotation,
537 bottomAlignedMargin,
538 statusBarContentHeight,
539 )
540 }
541
542 /**
543 * Calculate the insets needed from the left and right edges for the given rotation.
544 *
545 * @param displayCutout Device display cutout
546 * @param sbHeight appropriate status bar height for this rotation
547 * @param width display width calculated for ROTATION_NONE
548 * @param height display height calculated for ROTATION_NONE
549 * @param cWidth display width in our current rotation
550 * @param cHeight display height in our current rotation
551 * @param minLeft the minimum padding to enforce on the left
552 * @param minRight the minimum padding to enforce on the right
553 * @param isRtl current layout direction is Right-To-Left or not
554 * @param dotWidth privacy dot image width (0 if privacy dot is disabled)
555 * @param targetRotation the rotation for which to calculate margins
556 * @param currentRotation the rotation from which the display cutout was generated
557 * @return a Rect which exactly calculates the Status Bar's content rect relative to the target
558 * rotation
559 */
getStatusBarContentBoundsnull560 private fun getStatusBarContentBounds(
561 sysUICutout: SysUICutoutInformation?,
562 sbHeight: Int,
563 width: Int,
564 height: Int,
565 cWidth: Int,
566 cHeight: Int,
567 minLeft: Int,
568 minRight: Int,
569 isRtl: Boolean,
570 dotWidth: Int,
571 @Rotation targetRotation: Int,
572 @Rotation currentRotation: Int,
573 bottomAlignedMargin: Int,
574 statusBarContentHeight: Int,
575 ): Rect {
576 val insetTop = getInsetTop(bottomAlignedMargin, statusBarContentHeight, sbHeight)
577
578 val logicalDisplayWidth = if (targetRotation.isHorizontal()) height else width
579
580 // Exclude the bottom rect, as it doesn't intersect with the status bar.
581 val cutoutRects = sysUICutout?.cutout?.boundingRectsLeftRightTop
582 if (cutoutRects.isNullOrEmpty()) {
583 return Rect(minLeft, insetTop, logicalDisplayWidth - minRight, sbHeight)
584 }
585
586 val relativeRotation =
587 if (currentRotation - targetRotation < 0) {
588 currentRotation - targetRotation + 4
589 } else {
590 currentRotation - targetRotation
591 }
592
593 // Size of the status bar window for the given rotation relative to our exact rotation
594 val sbRect = sbRect(relativeRotation, sbHeight, Pair(cWidth, cHeight))
595
596 var leftMargin = minLeft
597 var rightMargin = minRight
598 for (cutoutRect in cutoutRects) {
599 val protectionRect = sysUICutout.cameraProtection?.bounds
600 val actualCutoutRect =
601 if (protectionRect?.intersects(cutoutRect) == true) {
602 rectUnion(cutoutRect, protectionRect)
603 } else {
604 cutoutRect
605 }
606 // There is at most one non-functional area per short edge of the device. So if the status
607 // bar doesn't share a short edge with the cutout, we can ignore its insets because there
608 // will be no letter-boxing to worry about
609 if (!shareShortEdge(sbRect, actualCutoutRect, cWidth, cHeight)) {
610 continue
611 }
612
613 if (actualCutoutRect.touchesLeftEdge(relativeRotation, cWidth, cHeight)) {
614 var logicalWidth = actualCutoutRect.logicalWidth(relativeRotation)
615 if (isRtl) logicalWidth += dotWidth
616 leftMargin = max(logicalWidth, leftMargin)
617 } else if (actualCutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) {
618 var logicalWidth = actualCutoutRect.logicalWidth(relativeRotation)
619 if (!isRtl) logicalWidth += dotWidth
620 rightMargin = max(rightMargin, logicalWidth)
621 }
622 // TODO(b/203626889): Fix the scenario when config_mainBuiltInDisplayCutoutRectApproximation
623 // is very close to but not directly touch edges.
624 }
625
626 return Rect(leftMargin, insetTop, logicalDisplayWidth - rightMargin, sbHeight)
627 }
628
<lambda>null629 private fun rectUnion(first: Rect, second: Rect) = Rect(first).apply { union(second) }
630
Rectnull631 private fun Rect.intersects(other: Rect): Boolean =
632 intersects(other.left, other.top, other.right, other.bottom)
633
634 private val DisplayCutout.boundingRectsLeftRightTop
635 get() = listOf(boundingRectLeft, boundingRectRight, boundingRectTop).filter { !it.isEmpty }
636
637 /*
638 * Returns the inset top of the status bar.
639 *
640 * Only greater than 0, when we want the content to be bottom aligned.
641 *
642 * Common case when we want content to be vertically centered within the status bar.
643 * Example dimensions:
644 * - Status bar height: 50dp
645 * - Content height: 20dp
646 * _______________________________________________
647 * | |
648 * | |
649 * | 09:00 5G [] 74% | 20dp Content CENTER_VERTICAL gravity
650 * | |
651 * |_____________________________________________|
652 *
653 * Case when we want bottom alignment and a bottom margin of 10dp.
654 * We need to make the status bar height artificially smaller using top padding/inset.
655 * - Status bar height: 50dp
656 * - Content height: 20dp
657 * - Bottom margin: 10dp
658 * ______________________________________________
659 * |_____________________________________________| 10dp top inset/padding
660 * | | 40dp new artificial status bar height
661 * | 09:00 5G [] 74% | 20dp Content CENTER_VERTICAL gravity
662 * |_____________________________________________| 10dp bottom margin
663 */
664 @Px
getInsetTopnull665 private fun getInsetTop(
666 bottomAlignedMargin: Int,
667 statusBarContentHeight: Int,
668 statusBarHeight: Int,
669 ): Int {
670 val bottomAlignmentEnabled = bottomAlignedMargin >= 0
671 if (!bottomAlignmentEnabled) {
672 return 0
673 }
674 val newArtificialStatusBarHeight = bottomAlignedMargin * 2 + statusBarContentHeight
675 return statusBarHeight - newArtificialStatusBarHeight
676 }
677
sbRectnull678 private fun sbRect(
679 @Rotation relativeRotation: Int,
680 sbHeight: Int,
681 displaySize: Pair<Int, Int>,
682 ): Rect {
683 val w = displaySize.first
684 val h = displaySize.second
685 return when (relativeRotation) {
686 ROTATION_NONE -> Rect(0, 0, w, sbHeight)
687 ROTATION_LANDSCAPE -> Rect(0, 0, sbHeight, h)
688 ROTATION_UPSIDE_DOWN -> Rect(0, h - sbHeight, w, h)
689 else -> Rect(w - sbHeight, 0, w, h)
690 }
691 }
692
shareShortEdgenull693 private fun shareShortEdge(
694 sbRect: Rect,
695 cutoutRect: Rect,
696 currentWidth: Int,
697 currentHeight: Int,
698 ): Boolean {
699 if (currentWidth < currentHeight) {
700 // Check top/bottom edges by extending the width of the display cutout rect and checking
701 // for intersections
702 return sbRect.intersects(0, cutoutRect.top, currentWidth, cutoutRect.bottom)
703 } else if (currentWidth > currentHeight) {
704 // Short edge is the height, extend that one this time
705 return sbRect.intersects(cutoutRect.left, 0, cutoutRect.right, currentHeight)
706 }
707
708 return false
709 }
710
touchesRightEdgenull711 private fun Rect.touchesRightEdge(@Rotation rot: Int, width: Int, height: Int): Boolean {
712 return when (rot) {
713 ROTATION_NONE -> right >= width
714 ROTATION_LANDSCAPE -> top <= 0
715 ROTATION_UPSIDE_DOWN -> left <= 0
716 else /* SEASCAPE */ -> bottom >= height
717 }
718 }
719
Rectnull720 private fun Rect.touchesLeftEdge(@Rotation rot: Int, width: Int, height: Int): Boolean {
721 return when (rot) {
722 ROTATION_NONE -> left <= 0
723 ROTATION_LANDSCAPE -> bottom >= height
724 ROTATION_UPSIDE_DOWN -> right >= width
725 else /* SEASCAPE */ -> top <= 0
726 }
727 }
728
Rectnull729 private fun Rect.logicalTop(@Rotation rot: Int): Int {
730 return when (rot) {
731 ROTATION_NONE -> top
732 ROTATION_LANDSCAPE -> left
733 ROTATION_UPSIDE_DOWN -> bottom
734 else /* SEASCAPE */ -> right
735 }
736 }
737
Rectnull738 private fun Rect.logicalRight(@Rotation rot: Int): Int {
739 return when (rot) {
740 ROTATION_NONE -> right
741 ROTATION_LANDSCAPE -> top
742 ROTATION_UPSIDE_DOWN -> left
743 else /* SEASCAPE */ -> bottom
744 }
745 }
746
Rectnull747 private fun Rect.logicalLeft(@Rotation rot: Int): Int {
748 return when (rot) {
749 ROTATION_NONE -> left
750 ROTATION_LANDSCAPE -> bottom
751 ROTATION_UPSIDE_DOWN -> right
752 else /* SEASCAPE */ -> top
753 }
754 }
755
Rectnull756 private fun Rect.logicalWidth(@Rotation rot: Int): Int {
757 return when (rot) {
758 ROTATION_NONE,
759 ROTATION_UPSIDE_DOWN -> width()
760 else /* LANDSCAPE, SEASCAPE */ -> height()
761 }
762 }
763
Intnull764 private fun Int.isHorizontal(): Boolean {
765 return this == ROTATION_LANDSCAPE || this == ROTATION_SEASCAPE
766 }
767
Pointnull768 private fun Point.orientToRotZero(@Rotation rot: Int) {
769 when (rot) {
770 ROTATION_NONE,
771 ROTATION_UPSIDE_DOWN -> return
772 else -> {
773 // swap width and height to zero-orient bounds
774 val yTmp = y
775 y = x
776 x = yTmp
777 }
778 }
779 }
780
Pointnull781 private fun Point.logicalWidth(@Rotation rot: Int): Int {
782 return when (rot) {
783 ROTATION_NONE,
784 ROTATION_UPSIDE_DOWN -> x
785 else -> y
786 }
787 }
788