1 /*
2  * 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 = checkNotNull(context.display).cutout ?: return false
121         val topBounds = cutout.boundingRectTop
122 
123         val point = Point()
124         checkNotNull(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 = checkNotNull(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 = checkNotNull(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 {
294     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
310 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  */
345 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  */
400 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 
460 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 
475 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 
493 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 
502 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 
511 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 
520 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 
529 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 
538 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 
545 private fun Int.isHorizontal(): Boolean {
546     return this == ROTATION_LANDSCAPE || this == ROTATION_SEASCAPE
547 }
548 
549 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 
561 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