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