• 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.content.Context
20 import android.content.res.Resources
21 import android.graphics.Rect
22 import android.util.Log
23 import android.util.Pair
24 import android.view.DisplayCutout
25 import android.view.View.LAYOUT_DIRECTION_RTL
26 import android.view.WindowManager
27 import android.view.WindowMetrics
28 import androidx.annotation.VisibleForTesting
29 import com.android.systemui.Dumpable
30 import com.android.systemui.R
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.dump.DumpManager
33 import com.android.systemui.statusbar.policy.CallbackController
34 import com.android.systemui.statusbar.policy.ConfigurationController
35 import com.android.systemui.util.leak.RotationUtils
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 java.io.FileDescriptor
42 import java.io.PrintWriter
43 import java.lang.Math.max
44 import javax.inject.Inject
45 
46 /**
47  * Encapsulates logic that can solve for the left/right insets required for the status bar contents.
48  * Takes into account:
49  *  1. rounded_corner_content_padding
50  *  2. status_bar_padding_start, status_bar_padding_end
51  *  2. display cutout insets from left or right
52  *  3. waterfall insets
53  *
54  *
55  *  Importantly, these functions can determine status bar content left/right insets for any rotation
56  *  before having done a layout pass in that rotation.
57  *
58  *  NOTE: This class is not threadsafe
59  */
60 @SysUISingleton
61 class StatusBarContentInsetsProvider @Inject constructor(
62     val context: Context,
63     val configurationController: ConfigurationController,
64     val windowManager: WindowManager,
65     val dumpManager: DumpManager
66 ) : CallbackController<StatusBarContentInsetsChangedListener>,
67         ConfigurationController.ConfigurationListener,
68         Dumpable {
69     // Indexed by @Rotation
70     private val insetsByCorner = arrayOfNulls<Rect>(4)
71     private val listeners = mutableSetOf<StatusBarContentInsetsChangedListener>()
72 
73     init {
74         configurationController.addCallback(this)
75         dumpManager.registerDumpable(TAG, this)
76     }
77 
78     override fun addCallback(listener: StatusBarContentInsetsChangedListener) {
79         listeners.add(listener)
80     }
81 
82     override fun removeCallback(listener: StatusBarContentInsetsChangedListener) {
83         listeners.remove(listener)
84     }
85 
86     override fun onDensityOrFontScaleChanged() {
87         clearCachedInsets()
88     }
89 
90     override fun onOverlayChanged() {
91         clearCachedInsets()
92     }
93 
94     private fun clearCachedInsets() {
95         insetsByCorner[0] = null
96         insetsByCorner[1] = null
97         insetsByCorner[2] = null
98         insetsByCorner[3] = null
99 
100         notifyInsetsChanged()
101     }
102 
103     private fun notifyInsetsChanged() {
104         listeners.forEach {
105             it.onStatusBarContentInsetsChanged()
106         }
107     }
108 
109     /**
110      * Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy
111      * dot in the coordinates relative to the given rotation.
112      */
113     fun getBoundingRectForPrivacyChipForRotation(@Rotation rotation: Int): Rect {
114         var insets = insetsByCorner[rotation]
115         val rotatedResources = RotationUtils.getResourcesForRotation(rotation, context)
116         if (insets == null) {
117             insets = getAndSetInsetsForRotation(rotation, rotatedResources)
118         }
119 
120         val dotWidth = rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
121         val chipWidth = rotatedResources.getDimensionPixelSize(
122                 R.dimen.ongoing_appops_chip_max_width)
123 
124         val isRtl = context.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL
125         return getPrivacyChipBoundingRectForInsets(insets, dotWidth, chipWidth, isRtl)
126     }
127 
128     /**
129      * Calculates the necessary left and right locations for the status bar contents invariant of
130      * the current device rotation, in the target rotation's coordinates
131      */
132     fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Rect {
133         var insets = insetsByCorner[rotation]
134         if (insets == null) {
135             val rotatedResources = RotationUtils.getResourcesForRotation(rotation, context)
136             insets = getAndSetInsetsForRotation(rotation, rotatedResources)
137         }
138 
139         return insets
140     }
141 
142     private fun getAndSetInsetsForRotation(
143         @Rotation rot: Int,
144         rotatedResources: Resources
145     ): Rect {
146         val insets = getCalculatedInsetsForRotation(rot, rotatedResources)
147         insetsByCorner[rot] = insets
148 
149         return insets
150     }
151 
152     private fun getCalculatedInsetsForRotation(
153         @Rotation targetRotation: Int,
154         rotatedResources: Resources
155     ): Rect {
156         val dc = context.display.cutout
157         val currentRotation = RotationUtils.getExactRotation(context)
158 
159         val isRtl = rotatedResources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL
160         val roundedCornerPadding = rotatedResources
161                 .getDimensionPixelSize(R.dimen.rounded_corner_content_padding)
162         val minDotWidth = rotatedResources
163                 .getDimensionPixelSize(R.dimen.ongoing_appops_dot_min_padding)
164 
165         val minLeft: Int
166         val minRight: Int
167         if (isRtl) {
168             minLeft = max(minDotWidth, roundedCornerPadding)
169             minRight = roundedCornerPadding
170         } else {
171             minLeft = roundedCornerPadding
172             minRight = max(minDotWidth, roundedCornerPadding)
173         }
174 
175         return calculateInsetsForRotationWithRotatedResources(
176                 currentRotation,
177                 targetRotation,
178                 dc,
179                 windowManager.maximumWindowMetrics,
180                 rotatedResources.getDimensionPixelSize(R.dimen.status_bar_height),
181                 minLeft,
182                 minRight)
183     }
184 
185     override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
186         insetsByCorner.forEachIndexed { index, rect ->
187             pw.println("${RotationUtils.toString(index)} -> $rect")
188         }
189     }
190 }
191 
192 interface StatusBarContentInsetsChangedListener {
onStatusBarContentInsetsChangednull193     fun onStatusBarContentInsetsChanged()
194 }
195 
196 private const val TAG = "StatusBarInsetsProvider"
197 
198 private fun getRotationZeroDisplayBounds(wm: WindowMetrics, @Rotation exactRotation: Int): Rect {
199     val bounds = wm.bounds
200 
201     if (exactRotation == ROTATION_NONE || exactRotation == ROTATION_UPSIDE_DOWN) {
202         return bounds
203     }
204 
205     // bounds are horizontal, swap height and width
206     return Rect(0, 0, bounds.bottom, bounds.right)
207 }
208 
209 @VisibleForTesting
getPrivacyChipBoundingRectForInsetsnull210 fun getPrivacyChipBoundingRectForInsets(
211     contentRect: Rect,
212     dotWidth: Int,
213     chipWidth: Int,
214     isRtl: Boolean
215 ): Rect {
216     return if (isRtl) {
217         Rect(contentRect.left - dotWidth,
218                 contentRect.top,
219                 contentRect.left + chipWidth,
220                 contentRect.bottom)
221     } else {
222         Rect(contentRect.right - chipWidth,
223                 contentRect.top,
224                 contentRect.right + dotWidth,
225                 contentRect.bottom)
226     }
227 }
228 
229 /**
230  * Calculates the exact left and right positions for the status bar contents for the given
231  * rotation
232  *
233  * @param currentRotation current device rotation
234  * @param targetRotation rotation for which to calculate the status bar content rect
235  * @param displayCutout [DisplayCutout] for the curren display. possibly null
236  * @param windowMetrics [WindowMetrics] for the current window
237  * @param statusBarHeight height of the status bar for the target rotation
238  * @param roundedCornerPadding from rounded_corner_content_padding
239  *
240  * @see [RotationUtils#getResourcesForRotation]
241  */
calculateInsetsForRotationWithRotatedResourcesnull242 fun calculateInsetsForRotationWithRotatedResources(
243     @Rotation currentRotation: Int,
244     @Rotation targetRotation: Int,
245     displayCutout: DisplayCutout?,
246     windowMetrics: WindowMetrics,
247     statusBarHeight: Int,
248     minLeft: Int,
249     minRight: Int
250 ): Rect {
251     /*
252     TODO: Check if this is ever used for devices with no rounded corners
253     val left = if (isRtl) paddingEnd else paddingStart
254     val right = if (isRtl) paddingStart else paddingEnd
255      */
256 
257     val rotZeroBounds = getRotationZeroDisplayBounds(windowMetrics, currentRotation)
258     val currentBounds = windowMetrics.bounds
259 
260     val sbLeftRight = getStatusBarLeftRight(
261             displayCutout,
262             statusBarHeight,
263             rotZeroBounds.right,
264             rotZeroBounds.bottom,
265             currentBounds.width(),
266             currentBounds.height(),
267             minLeft,
268             minRight,
269             targetRotation,
270             currentRotation)
271 
272     return sbLeftRight
273 }
274 
275 /**
276  * Calculate the insets needed from the left and right edges for the given rotation.
277  *
278  * @param dc Device display cutout
279  * @param sbHeight appropriate status bar height for this rotation
280  * @param width display width calculated for ROTATION_NONE
281  * @param height display height calculated for ROTATION_NONE
282  * @param cWidth display width in our current rotation
283  * @param cHeight display height in our current rotation
284  * @param minLeft the minimum padding to enforce on the left
285  * @param minRight the minimum padding to enforce on the right
286  * @param targetRotation the rotation for which to calculate margins
287  * @param currentRotation the rotation from which the display cutout was generated
288  *
289  * @return a Rect which exactly calculates the Status Bar's content rect relative to the target
290  * rotation
291  */
getStatusBarLeftRightnull292 private fun getStatusBarLeftRight(
293     dc: DisplayCutout?,
294     sbHeight: Int,
295     width: Int,
296     height: Int,
297     cWidth: Int,
298     cHeight: Int,
299     minLeft: Int,
300     minRight: Int,
301     @Rotation targetRotation: Int,
302     @Rotation currentRotation: Int
303 ): Rect {
304 
305     val logicalDisplayWidth = if (targetRotation.isHorizontal()) height else width
306 
307     val cutoutRects = dc?.boundingRects
308     if (cutoutRects == null || cutoutRects.isEmpty()) {
309         return Rect(minLeft,
310                 0,
311                 logicalDisplayWidth - minRight,
312                 sbHeight)
313     }
314 
315     val relativeRotation = if (currentRotation - targetRotation < 0) {
316         currentRotation - targetRotation + 4
317     } else {
318         currentRotation - targetRotation
319     }
320 
321     // Size of the status bar window for the given rotation relative to our exact rotation
322     val sbRect = sbRect(relativeRotation, sbHeight, Pair(cWidth, cHeight))
323 
324     var leftMargin = minLeft
325     var rightMargin = minRight
326     for (cutoutRect in cutoutRects) {
327         // There is at most one non-functional area per short edge of the device. So if the status
328         // bar doesn't share a short edge with the cutout, we can ignore its insets because there
329         // will be no letter-boxing to worry about
330         if (!shareShortEdge(sbRect, cutoutRect, cWidth, cHeight)) {
331             continue
332         }
333 
334         if (cutoutRect.touchesLeftEdge(relativeRotation, cWidth, cHeight)) {
335 
336             val l = max(minLeft, cutoutRect.logicalWidth(relativeRotation))
337             leftMargin = max(l, leftMargin)
338         } else if (cutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) {
339             val logicalWidth = cutoutRect.logicalWidth(relativeRotation)
340             rightMargin = max(minRight, logicalWidth)
341         }
342     }
343 
344     return Rect(leftMargin, 0, logicalDisplayWidth - rightMargin, sbHeight)
345 }
346 
sbRectnull347 private fun sbRect(
348     @Rotation relativeRotation: Int,
349     sbHeight: Int,
350     displaySize: Pair<Int, Int>
351 ): Rect {
352     val w = displaySize.first
353     val h = displaySize.second
354     return when (relativeRotation) {
355         ROTATION_NONE -> Rect(0, 0, w, sbHeight)
356         ROTATION_LANDSCAPE -> Rect(0, 0, sbHeight, h)
357         ROTATION_UPSIDE_DOWN -> Rect(0, h - sbHeight, w, h)
358         else -> Rect(w - sbHeight, 0, w, h)
359     }
360 }
361 
shareShortEdgenull362 private fun shareShortEdge(
363     sbRect: Rect,
364     cutoutRect: Rect,
365     currentWidth: Int,
366     currentHeight: Int
367 ): Boolean {
368     if (currentWidth < currentHeight) {
369         // Check top/bottom edges by extending the width of the display cutout rect and checking
370         // for intersections
371         return sbRect.intersects(0, cutoutRect.top, currentWidth, cutoutRect.bottom)
372     } else if (currentWidth > currentHeight) {
373         // Short edge is the height, extend that one this time
374         return sbRect.intersects(cutoutRect.left, 0, cutoutRect.right, currentHeight)
375     }
376 
377     return false
378 }
379 
touchesRightEdgenull380 private fun Rect.touchesRightEdge(@Rotation rot: Int, width: Int, height: Int): Boolean {
381     return when (rot) {
382         ROTATION_NONE -> right >= width
383         ROTATION_LANDSCAPE -> top <= 0
384         ROTATION_UPSIDE_DOWN -> left <= 0
385         else /* SEASCAPE */ -> bottom >= height
386     }
387 }
388 
Rectnull389 private fun Rect.touchesLeftEdge(@Rotation rot: Int, width: Int, height: Int): Boolean {
390     return when (rot) {
391         ROTATION_NONE -> left <= 0
392         ROTATION_LANDSCAPE -> bottom >= height
393         ROTATION_UPSIDE_DOWN -> right >= width
394         else /* SEASCAPE */ -> top <= 0
395     }
396 }
397 
Rectnull398 private fun Rect.logicalTop(@Rotation rot: Int): Int {
399     return when (rot) {
400         ROTATION_NONE -> top
401         ROTATION_LANDSCAPE -> left
402         ROTATION_UPSIDE_DOWN -> bottom
403         else /* SEASCAPE */ -> right
404     }
405 }
406 
Rectnull407 private fun Rect.logicalRight(@Rotation rot: Int): Int {
408     return when (rot) {
409         ROTATION_NONE -> right
410         ROTATION_LANDSCAPE -> top
411         ROTATION_UPSIDE_DOWN -> left
412         else /* SEASCAPE */ -> bottom
413     }
414 }
415 
Rectnull416 private fun Rect.logicalLeft(@Rotation rot: Int): Int {
417     return when (rot) {
418         ROTATION_NONE -> left
419         ROTATION_LANDSCAPE -> bottom
420         ROTATION_UPSIDE_DOWN -> right
421         else /* SEASCAPE */ -> top
422     }
423 }
424 
Rectnull425 private fun Rect.logicalWidth(@Rotation rot: Int): Int {
426     return when (rot) {
427         ROTATION_NONE, ROTATION_UPSIDE_DOWN -> width()
428         else /* LANDSCAPE, SEASCAPE */ -> height()
429     }
430 }
431 
Intnull432 private fun Int.isHorizontal(): Boolean {
433     return this == ROTATION_LANDSCAPE || this == ROTATION_SEASCAPE
434 }
435