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