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