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