• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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