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