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.Configuration 21 import android.graphics.Rect 22 import android.view.Display 23 import android.view.DisplayCutout 24 import androidx.test.filters.SmallTest 25 import com.android.systemui.SysuiTestCase 26 import com.android.systemui.dump.DumpManager 27 import com.android.systemui.statusbar.policy.ConfigurationController 28 import com.android.systemui.util.leak.RotationUtils 29 import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE 30 import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE 31 import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE 32 import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN 33 import com.android.systemui.util.leak.RotationUtils.Rotation 34 import com.google.common.truth.Truth.assertThat 35 import junit.framework.Assert.assertTrue 36 import org.junit.Before 37 import org.junit.Test 38 import org.mockito.ArgumentMatchers.any 39 import org.mockito.Mock 40 import org.mockito.Mockito.`when` 41 import org.mockito.Mockito.mock 42 import org.mockito.MockitoAnnotations 43 44 @SmallTest 45 class StatusBarContentInsetsProviderTest : SysuiTestCase() { 46 47 @Mock private lateinit var dc: DisplayCutout 48 @Mock private lateinit var contextMock: Context 49 @Mock private lateinit var display: Display 50 private lateinit var configurationController: ConfigurationController 51 52 private val configuration = Configuration() 53 54 @Before 55 fun setup() { 56 MockitoAnnotations.initMocks(this) 57 `when`(contextMock.display).thenReturn(display) 58 59 context.ensureTestableResources() 60 `when`(contextMock.resources).thenReturn(context.resources) 61 `when`(contextMock.resources.configuration).thenReturn(configuration) 62 `when`(contextMock.createConfigurationContext(any())).thenAnswer { 63 context.createConfigurationContext(it.arguments[0] as Configuration) 64 } 65 66 configurationController = ConfigurationControllerImpl(contextMock) 67 } 68 69 @Test 70 fun testGetBoundingRectForPrivacyChipForRotation_noCutout() { 71 val screenBounds = Rect(0, 0, 1080, 2160) 72 val minLeftPadding = 20 73 val minRightPadding = 20 74 val sbHeightPortrait = 100 75 val sbHeightLandscape = 60 76 val currentRotation = ROTATION_NONE 77 val chipWidth = 30 78 val dotWidth = 10 79 80 var isRtl = false 81 var targetRotation = ROTATION_NONE 82 var bounds = calculateInsetsForRotationWithRotatedResources( 83 currentRotation, 84 targetRotation, 85 null, 86 screenBounds, 87 sbHeightPortrait, 88 minLeftPadding, 89 minRightPadding, 90 isRtl, 91 dotWidth) 92 93 var chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) 94 /* 1080 - 20 (rounded corner) - 30 (chip), 95 * 0 (sb top) 96 * 1080 - 20 (rounded corner) + 10 ( dot), 97 * 100 (sb height portrait) 98 */ 99 var expected = Rect(1030, 0, 1070, 100) 100 assertRects(expected, chipBounds, currentRotation, targetRotation) 101 isRtl = true 102 chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) 103 /* 0 + 20 (rounded corner) - 10 (dot), 104 * 0 (sb top) 105 * 0 + 20 (rounded corner) + 30 (chip), 106 * 100 (sb height portrait) 107 */ 108 expected = Rect(10, 0, 50, 100) 109 assertRects(expected, chipBounds, currentRotation, targetRotation) 110 111 isRtl = false 112 targetRotation = ROTATION_LANDSCAPE 113 bounds = calculateInsetsForRotationWithRotatedResources( 114 currentRotation, 115 targetRotation, 116 dc, 117 screenBounds, 118 sbHeightLandscape, 119 minLeftPadding, 120 minRightPadding, 121 isRtl, 122 dotWidth) 123 124 chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) 125 /* 2160 - 20 (rounded corner) - 30 (chip), 126 * 0 (sb top) 127 * 2160 - 20 (rounded corner) + 10 ( dot), 128 * 60 (sb height landscape) 129 */ 130 expected = Rect(2110, 0, 2150, 60) 131 assertRects(expected, chipBounds, currentRotation, targetRotation) 132 isRtl = true 133 chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) 134 /* 0 + 20 (rounded corner) - 10 (dot), 135 * 0 (sb top) 136 * 0 + 20 (rounded corner) + 30 (chip), 137 * 60 (sb height landscape) 138 */ 139 expected = Rect(10, 0, 50, 60) 140 assertRects(expected, chipBounds, currentRotation, targetRotation) 141 } 142 143 @Test 144 fun testCalculateInsetsForRotationWithRotatedResources_topLeftCutout() { 145 // GIVEN a device in portrait mode with width < height and a display cutout in the top-left 146 val screenBounds = Rect(0, 0, 1080, 2160) 147 val dcBounds = Rect(0, 0, 100, 100) 148 val minLeftPadding = 20 149 val minRightPadding = 20 150 val sbHeightPortrait = 100 151 val sbHeightLandscape = 60 152 val currentRotation = ROTATION_NONE 153 val isRtl = false 154 val dotWidth = 10 155 156 `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) 157 158 // THEN rotations which share a short side should use the greater value between rounded 159 // corner padding and the display cutout's size 160 var targetRotation = ROTATION_NONE 161 var expectedBounds = Rect(dcBounds.right, 162 0, 163 screenBounds.right - minRightPadding, 164 sbHeightPortrait) 165 166 var bounds = calculateInsetsForRotationWithRotatedResources( 167 currentRotation, 168 targetRotation, 169 dc, 170 screenBounds, 171 sbHeightPortrait, 172 minLeftPadding, 173 minRightPadding, 174 isRtl, 175 dotWidth) 176 177 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 178 179 targetRotation = ROTATION_LANDSCAPE 180 expectedBounds = Rect(dcBounds.height(), 181 0, 182 screenBounds.height() - minRightPadding, 183 sbHeightLandscape) 184 185 bounds = calculateInsetsForRotationWithRotatedResources( 186 currentRotation, 187 targetRotation, 188 dc, 189 screenBounds, 190 sbHeightLandscape, 191 minLeftPadding, 192 minRightPadding, 193 isRtl, 194 dotWidth) 195 196 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 197 198 // THEN the side that does NOT share a short side with the display cutout ignores the 199 // display cutout bounds 200 targetRotation = ROTATION_UPSIDE_DOWN 201 expectedBounds = Rect(minLeftPadding, 202 0, 203 screenBounds.width() - minRightPadding, 204 sbHeightPortrait) 205 206 bounds = calculateInsetsForRotationWithRotatedResources( 207 currentRotation, 208 targetRotation, 209 dc, 210 screenBounds, 211 sbHeightPortrait, 212 minLeftPadding, 213 minRightPadding, 214 isRtl, 215 dotWidth) 216 217 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 218 219 // Phone in portrait, seascape (rot_270) bounds 220 targetRotation = ROTATION_SEASCAPE 221 expectedBounds = Rect(minLeftPadding, 222 0, 223 screenBounds.height() - dcBounds.height() - dotWidth, 224 sbHeightLandscape) 225 226 bounds = calculateInsetsForRotationWithRotatedResources( 227 currentRotation, 228 targetRotation, 229 dc, 230 screenBounds, 231 sbHeightLandscape, 232 minLeftPadding, 233 minRightPadding, 234 isRtl, 235 dotWidth) 236 237 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 238 } 239 240 @Test 241 fun testCalculateInsetsForRotationWithRotatedResources_nonCornerCutout() { 242 // GIVEN phone in portrait mode, where width < height and the cutout is not in the corner 243 // the assumption here is that if the cutout does NOT touch the corner then we have room to 244 // layout the status bar in the given space. 245 246 val screenBounds = Rect(0, 0, 1080, 2160) 247 // cutout centered at the top 248 val dcBounds = Rect(490, 0, 590, 100) 249 val minLeftPadding = 20 250 val minRightPadding = 20 251 val sbHeightPortrait = 100 252 val sbHeightLandscape = 60 253 val currentRotation = ROTATION_NONE 254 val isRtl = false 255 val dotWidth = 10 256 257 `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) 258 259 // THEN only the landscape/seascape rotations should avoid the cutout area because of the 260 // potential letterboxing 261 var targetRotation = ROTATION_NONE 262 var expectedBounds = Rect(minLeftPadding, 263 0, 264 screenBounds.right - minRightPadding, 265 sbHeightPortrait) 266 267 var bounds = calculateInsetsForRotationWithRotatedResources( 268 currentRotation, 269 targetRotation, 270 dc, 271 screenBounds, 272 sbHeightPortrait, 273 minLeftPadding, 274 minRightPadding, 275 isRtl, 276 dotWidth) 277 278 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 279 280 targetRotation = ROTATION_LANDSCAPE 281 expectedBounds = Rect(dcBounds.height(), 282 0, 283 screenBounds.height() - minRightPadding, 284 sbHeightLandscape) 285 286 bounds = calculateInsetsForRotationWithRotatedResources( 287 currentRotation, 288 targetRotation, 289 dc, 290 screenBounds, 291 sbHeightLandscape, 292 minLeftPadding, 293 minRightPadding, 294 isRtl, 295 dotWidth) 296 297 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 298 299 targetRotation = ROTATION_UPSIDE_DOWN 300 expectedBounds = Rect(minLeftPadding, 301 0, 302 screenBounds.right - minRightPadding, 303 sbHeightPortrait) 304 305 bounds = calculateInsetsForRotationWithRotatedResources( 306 currentRotation, 307 targetRotation, 308 dc, 309 screenBounds, 310 sbHeightPortrait, 311 minLeftPadding, 312 minRightPadding, 313 isRtl, 314 dotWidth) 315 316 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 317 318 targetRotation = ROTATION_SEASCAPE 319 expectedBounds = Rect(minLeftPadding, 320 0, 321 screenBounds.height() - dcBounds.height() - dotWidth, 322 sbHeightLandscape) 323 324 bounds = calculateInsetsForRotationWithRotatedResources( 325 currentRotation, 326 targetRotation, 327 dc, 328 screenBounds, 329 sbHeightLandscape, 330 minLeftPadding, 331 minRightPadding, 332 isRtl, 333 dotWidth) 334 335 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 336 } 337 338 @Test 339 fun testCalculateInsetsForRotationWithRotatedResources_noCutout() { 340 // GIVEN device in portrait mode, where width < height and no cutout 341 val currentRotation = ROTATION_NONE 342 val screenBounds = Rect(0, 0, 1080, 2160) 343 val minLeftPadding = 20 344 val minRightPadding = 20 345 val sbHeightPortrait = 100 346 val sbHeightLandscape = 60 347 val isRtl = false 348 val dotWidth = 10 349 350 // THEN content insets should only use rounded corner padding 351 var targetRotation = ROTATION_NONE 352 var expectedBounds = Rect(minLeftPadding, 353 0, 354 screenBounds.right - minRightPadding, 355 sbHeightPortrait) 356 357 var bounds = calculateInsetsForRotationWithRotatedResources( 358 currentRotation, 359 targetRotation, 360 null, /* no cutout */ 361 screenBounds, 362 sbHeightPortrait, 363 minLeftPadding, 364 minRightPadding, 365 isRtl, 366 dotWidth) 367 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 368 369 targetRotation = ROTATION_LANDSCAPE 370 expectedBounds = Rect(minLeftPadding, 371 0, 372 screenBounds.height() - minRightPadding, 373 sbHeightLandscape) 374 375 bounds = calculateInsetsForRotationWithRotatedResources( 376 currentRotation, 377 targetRotation, 378 null, /* no cutout */ 379 screenBounds, 380 sbHeightLandscape, 381 minLeftPadding, 382 minRightPadding, 383 isRtl, 384 dotWidth) 385 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 386 387 targetRotation = ROTATION_UPSIDE_DOWN 388 expectedBounds = Rect(minLeftPadding, 389 0, 390 screenBounds.width() - minRightPadding, 391 sbHeightPortrait) 392 393 bounds = calculateInsetsForRotationWithRotatedResources( 394 currentRotation, 395 targetRotation, 396 null, /* no cutout */ 397 screenBounds, 398 sbHeightPortrait, 399 minLeftPadding, 400 minRightPadding, 401 isRtl, 402 dotWidth) 403 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 404 405 targetRotation = ROTATION_LANDSCAPE 406 expectedBounds = Rect(minLeftPadding, 407 0, 408 screenBounds.height() - minRightPadding, 409 sbHeightLandscape) 410 411 bounds = calculateInsetsForRotationWithRotatedResources( 412 currentRotation, 413 targetRotation, 414 null, /* no cutout */ 415 screenBounds, 416 sbHeightLandscape, 417 minLeftPadding, 418 minRightPadding, 419 isRtl, 420 dotWidth) 421 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 422 } 423 424 @Test 425 fun testMinLeftRight_accountsForDisplayCutout() { 426 // GIVEN a device in portrait mode with width < height and a display cutout in the top-left 427 val screenBounds = Rect(0, 0, 1080, 2160) 428 val dcBounds = Rect(0, 0, 100, 100) 429 val minLeftPadding = 80 430 val minRightPadding = 150 431 val sbHeightPortrait = 100 432 val sbHeightLandscape = 60 433 val currentRotation = ROTATION_NONE 434 val isRtl = false 435 val dotWidth = 10 436 437 `when`(dc.boundingRects).thenReturn(listOf(dcBounds)) 438 439 // THEN left should be set to the display cutout width, and right should use the minRight 440 var targetRotation = ROTATION_NONE 441 var expectedBounds = Rect(dcBounds.right, 442 0, 443 screenBounds.right - minRightPadding, 444 sbHeightPortrait) 445 446 var bounds = calculateInsetsForRotationWithRotatedResources( 447 currentRotation, 448 targetRotation, 449 dc, 450 screenBounds, 451 sbHeightPortrait, 452 minLeftPadding, 453 minRightPadding, 454 isRtl, 455 dotWidth) 456 457 assertRects(expectedBounds, bounds, currentRotation, targetRotation) 458 } 459 460 @Test 461 fun testDisplayChanged_returnsUpdatedInsets() { 462 // GIVEN: get insets on the first display and switch to the second display 463 val provider = StatusBarContentInsetsProvider(contextMock, configurationController, 464 mock(DumpManager::class.java)) 465 466 configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) 467 val firstDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) 468 469 configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600)) 470 471 // WHEN: get insets on the second display 472 val secondDisplayInsets = provider.getStatusBarContentAreaForRotation(ROTATION_NONE) 473 474 // THEN: insets are updated 475 assertThat(firstDisplayInsets).isNotEqualTo(secondDisplayInsets) 476 } 477 478 @Test 479 fun testDisplayChangedAndReturnedBack_returnsTheSameInsets() { 480 // GIVEN: get insets on the first display, switch to the second display, 481 // get insets and switch back 482 val provider = StatusBarContentInsetsProvider(contextMock, configurationController, 483 mock(DumpManager::class.java)) 484 485 configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) 486 val firstDisplayInsetsFirstCall = provider 487 .getStatusBarContentAreaForRotation(ROTATION_NONE) 488 489 configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 800, 600)) 490 provider.getStatusBarContentAreaForRotation(ROTATION_NONE) 491 492 configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) 493 494 // WHEN: get insets on the first display again 495 val firstDisplayInsetsSecondCall = provider 496 .getStatusBarContentAreaForRotation(ROTATION_NONE) 497 498 // THEN: insets for the first and second calls for the first display are the same 499 assertThat(firstDisplayInsetsFirstCall).isEqualTo(firstDisplayInsetsSecondCall) 500 } 501 502 // Regression test for b/245799099 503 @Test 504 fun onMaxBoundsChanged_listenerNotified() { 505 // Start out with an existing configuration with bounds 506 configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) 507 configurationController.onConfigurationChanged(configuration) 508 val provider = StatusBarContentInsetsProvider(contextMock, configurationController, 509 mock(DumpManager::class.java)) 510 val listener = object : StatusBarContentInsetsChangedListener { 511 var triggered = false 512 513 override fun onStatusBarContentInsetsChanged() { 514 triggered = true 515 } 516 } 517 provider.addCallback(listener) 518 519 // WHEN the config is updated with new bounds 520 configuration.windowConfiguration.setMaxBounds(0, 0, 456, 789) 521 configurationController.onConfigurationChanged(configuration) 522 523 // THEN the listener is notified 524 assertThat(listener.triggered).isTrue() 525 } 526 527 @Test 528 fun onDensityOrFontScaleChanged_listenerNotified() { 529 configuration.densityDpi = 12 530 val provider = StatusBarContentInsetsProvider(contextMock, configurationController, 531 mock(DumpManager::class.java)) 532 val listener = object : StatusBarContentInsetsChangedListener { 533 var triggered = false 534 535 override fun onStatusBarContentInsetsChanged() { 536 triggered = true 537 } 538 } 539 provider.addCallback(listener) 540 541 // WHEN the config is updated 542 configuration.densityDpi = 20 543 configurationController.onConfigurationChanged(configuration) 544 545 // THEN the listener is notified 546 assertThat(listener.triggered).isTrue() 547 } 548 549 @Test 550 fun onThemeChanged_listenerNotified() { 551 val provider = StatusBarContentInsetsProvider(contextMock, configurationController, 552 mock(DumpManager::class.java)) 553 val listener = object : StatusBarContentInsetsChangedListener { 554 var triggered = false 555 556 override fun onStatusBarContentInsetsChanged() { 557 triggered = true 558 } 559 } 560 provider.addCallback(listener) 561 562 configurationController.notifyThemeChanged() 563 564 // THEN the listener is notified 565 assertThat(listener.triggered).isTrue() 566 } 567 568 private fun assertRects( 569 expected: Rect, 570 actual: Rect, 571 @Rotation currentRotation: Int, 572 @Rotation targetRotation: Int 573 ) { 574 assertTrue( 575 "Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" + 576 " targetRotation=${RotationUtils.toString(targetRotation)}" + 577 " expected=$expected actual=$actual", 578 expected.equals(actual)) 579 } 580 } 581