1 /* 2 * Copyright (C) 2023 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.data.repository 18 19 import android.graphics.Rect 20 import android.view.WindowInsets 21 import android.view.WindowInsetsController 22 import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS 23 import android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS 24 import android.view.WindowInsetsController.APPEARANCE_OPAQUE_STATUS_BARS 25 import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS 26 import androidx.test.filters.SmallTest 27 import com.android.internal.statusbar.LetterboxDetails 28 import com.android.internal.view.AppearanceRegion 29 import com.android.systemui.SysuiTestCase 30 import com.android.systemui.coroutines.collectLastValue 31 import com.android.systemui.statusbar.CommandQueue 32 import com.android.systemui.statusbar.data.model.StatusBarMode 33 import com.android.systemui.statusbar.phone.BoundsPair 34 import com.android.systemui.statusbar.phone.LetterboxAppearance 35 import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator 36 import com.android.systemui.statusbar.phone.StatusBarBoundsProvider 37 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent 38 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository 39 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel 40 import com.android.systemui.util.mockito.any 41 import com.android.systemui.util.mockito.argumentCaptor 42 import com.android.systemui.util.mockito.capture 43 import com.android.systemui.util.mockito.eq 44 import com.android.systemui.util.mockito.mock 45 import com.android.systemui.util.mockito.whenever 46 import com.google.common.truth.Truth.assertThat 47 import kotlinx.coroutines.test.TestScope 48 import kotlinx.coroutines.test.runTest 49 import org.junit.Before 50 import org.junit.Test 51 import org.mockito.Mockito.verify 52 53 @SmallTest 54 class StatusBarModeRepositoryImplTest : SysuiTestCase() { 55 private val testScope = TestScope() 56 private val commandQueue = mock<CommandQueue>() 57 private val letterboxAppearanceCalculator = mock<LetterboxAppearanceCalculator>() 58 private val statusBarBoundsProvider = mock<StatusBarBoundsProvider>() 59 private val statusBarFragmentComponent = <lambda>null60 mock<StatusBarFragmentComponent>().also { 61 whenever(it.boundsProvider).thenReturn(statusBarBoundsProvider) 62 } 63 private val ongoingCallRepository = OngoingCallRepository() 64 65 private val underTest = 66 StatusBarModePerDisplayRepositoryImpl( 67 testScope.backgroundScope, 68 DISPLAY_ID, 69 commandQueue, 70 letterboxAppearanceCalculator, 71 ongoingCallRepository, 72 ) <lambda>null73 .apply { 74 this.start() 75 this.onStatusBarViewInitialized(statusBarFragmentComponent) 76 } 77 78 private val commandQueueCallback: CommandQueue.Callbacks 79 get() { 80 val callbackCaptor = argumentCaptor<CommandQueue.Callbacks>() 81 verify(commandQueue).addCallback(callbackCaptor.capture()) 82 return callbackCaptor.value 83 } 84 85 private val statusBarBoundsChangeListener: StatusBarBoundsProvider.BoundsChangeListener 86 get() { 87 val callbackCaptor = argumentCaptor<StatusBarBoundsProvider.BoundsChangeListener>() 88 verify(statusBarBoundsProvider).addChangeListener(capture(callbackCaptor)) 89 return callbackCaptor.value 90 } 91 setUpnull92 @Before fun setUp() {} 93 94 @Test isTransientShown_commandQueueShow_wrongDisplayId_notUpdatednull95 fun isTransientShown_commandQueueShow_wrongDisplayId_notUpdated() { 96 commandQueueCallback.showTransient( 97 DISPLAY_ID + 1, 98 WindowInsets.Type.statusBars(), 99 /* isGestureOnSystemBar= */ false, 100 ) 101 102 assertThat(underTest.isTransientShown.value).isFalse() 103 } 104 105 @Test isTransientShown_commandQueueShow_notStatusBarType_notUpdatednull106 fun isTransientShown_commandQueueShow_notStatusBarType_notUpdated() { 107 commandQueueCallback.showTransient( 108 DISPLAY_ID, 109 WindowInsets.Type.navigationBars(), 110 /* isGestureOnSystemBar= */ false, 111 ) 112 113 assertThat(underTest.isTransientShown.value).isFalse() 114 } 115 116 @Test isTransientShown_commandQueueShow_truenull117 fun isTransientShown_commandQueueShow_true() { 118 commandQueueCallback.showTransient( 119 DISPLAY_ID, 120 WindowInsets.Type.statusBars(), 121 /* isGestureOnSystemBar= */ false, 122 ) 123 124 assertThat(underTest.isTransientShown.value).isTrue() 125 } 126 127 @Test isTransientShown_commandQueueShow_statusBarAndOtherTypes_truenull128 fun isTransientShown_commandQueueShow_statusBarAndOtherTypes_true() { 129 commandQueueCallback.showTransient( 130 DISPLAY_ID, 131 WindowInsets.Type.statusBars().or(WindowInsets.Type.navigationBars()), 132 /* isGestureOnSystemBar= */ false, 133 ) 134 135 assertThat(underTest.isTransientShown.value).isTrue() 136 } 137 138 @Test isTransientShown_commandQueueAbort_wrongDisplayId_notUpdatednull139 fun isTransientShown_commandQueueAbort_wrongDisplayId_notUpdated() { 140 // Start as true 141 commandQueueCallback.showTransient( 142 DISPLAY_ID, 143 WindowInsets.Type.statusBars(), 144 /* isGestureOnSystemBar= */ false, 145 ) 146 assertThat(underTest.isTransientShown.value).isTrue() 147 148 // GIVEN the wrong display ID 149 commandQueueCallback.abortTransient(DISPLAY_ID + 1, WindowInsets.Type.statusBars()) 150 151 // THEN the old value remains 152 assertThat(underTest.isTransientShown.value).isTrue() 153 } 154 155 @Test isTransientShown_commandQueueAbort_notStatusBarType_notUpdatednull156 fun isTransientShown_commandQueueAbort_notStatusBarType_notUpdated() { 157 // Start as true 158 commandQueueCallback.showTransient( 159 DISPLAY_ID, 160 WindowInsets.Type.statusBars(), 161 /* isGestureOnSystemBar= */ false, 162 ) 163 assertThat(underTest.isTransientShown.value).isTrue() 164 165 // GIVEN the wrong type 166 commandQueueCallback.abortTransient(DISPLAY_ID, WindowInsets.Type.navigationBars()) 167 168 // THEN the old value remains 169 assertThat(underTest.isTransientShown.value).isTrue() 170 } 171 172 @Test isTransientShown_commandQueueAbort_falsenull173 fun isTransientShown_commandQueueAbort_false() { 174 // Start as true 175 commandQueueCallback.showTransient( 176 DISPLAY_ID, 177 WindowInsets.Type.statusBars(), 178 /* isGestureOnSystemBar= */ false, 179 ) 180 assertThat(underTest.isTransientShown.value).isTrue() 181 182 commandQueueCallback.abortTransient(DISPLAY_ID, WindowInsets.Type.statusBars()) 183 184 assertThat(underTest.isTransientShown.value).isFalse() 185 } 186 187 @Test isTransientShown_commandQueueAbort_statusBarAndOtherTypes_falsenull188 fun isTransientShown_commandQueueAbort_statusBarAndOtherTypes_false() { 189 // Start as true 190 commandQueueCallback.showTransient( 191 DISPLAY_ID, 192 WindowInsets.Type.statusBars(), 193 /* isGestureOnSystemBar= */ false, 194 ) 195 assertThat(underTest.isTransientShown.value).isTrue() 196 197 commandQueueCallback.abortTransient( 198 DISPLAY_ID, 199 WindowInsets.Type.statusBars().or(WindowInsets.Type.captionBar()), 200 ) 201 202 assertThat(underTest.isTransientShown.value).isFalse() 203 } 204 205 @Test isTransientShown_showTransient_truenull206 fun isTransientShown_showTransient_true() { 207 underTest.showTransient() 208 209 assertThat(underTest.isTransientShown.value).isTrue() 210 } 211 212 @Test isTransientShown_clearTransient_falsenull213 fun isTransientShown_clearTransient_false() { 214 // Start as true 215 commandQueueCallback.showTransient( 216 DISPLAY_ID, 217 WindowInsets.Type.statusBars(), 218 /* isGestureOnSystemBar= */ false, 219 ) 220 assertThat(underTest.isTransientShown.value).isTrue() 221 222 underTest.clearTransient() 223 224 assertThat(underTest.isTransientShown.value).isFalse() 225 } 226 227 @Test isInFullscreenMode_visibleTypesHasStatusBar_falsenull228 fun isInFullscreenMode_visibleTypesHasStatusBar_false() = 229 testScope.runTest { 230 val latest by collectLastValue(underTest.isInFullscreenMode) 231 232 onSystemBarAttributesChanged( 233 requestedVisibleTypes = WindowInsets.Type.statusBars(), 234 ) 235 236 assertThat(latest).isFalse() 237 } 238 239 @Test isInFullscreenMode_visibleTypesDoesNotHaveStatusBar_truenull240 fun isInFullscreenMode_visibleTypesDoesNotHaveStatusBar_true() = 241 testScope.runTest { 242 val latest by collectLastValue(underTest.isInFullscreenMode) 243 244 onSystemBarAttributesChanged( 245 requestedVisibleTypes = WindowInsets.Type.navigationBars(), 246 ) 247 248 assertThat(latest).isTrue() 249 } 250 251 @Test isInFullscreenMode_wrongDisplayId_notUpdatednull252 fun isInFullscreenMode_wrongDisplayId_notUpdated() = 253 testScope.runTest { 254 val latest by collectLastValue(underTest.isInFullscreenMode) 255 256 onSystemBarAttributesChanged( 257 requestedVisibleTypes = WindowInsets.Type.navigationBars(), 258 ) 259 assertThat(latest).isTrue() 260 261 onSystemBarAttributesChanged( 262 displayId = DISPLAY_ID + 1, 263 requestedVisibleTypes = WindowInsets.Type.statusBars(), 264 ) 265 266 assertThat(latest).isTrue() 267 } 268 269 @Test statusBarAppearance_navBarColorManaged_matchesCallbackValuenull270 fun statusBarAppearance_navBarColorManaged_matchesCallbackValue() = 271 testScope.runTest { 272 val latest by collectLastValue(underTest.statusBarAppearance) 273 274 onSystemBarAttributesChanged(navbarColorManagedByIme = true) 275 276 assertThat(latest!!.navbarColorManagedByIme).isTrue() 277 278 onSystemBarAttributesChanged(navbarColorManagedByIme = false) 279 280 assertThat(latest!!.navbarColorManagedByIme).isFalse() 281 } 282 283 @Test statusBarAppearance_appearanceRegions_noLetterboxDetails_usesCallbackValuesnull284 fun statusBarAppearance_appearanceRegions_noLetterboxDetails_usesCallbackValues() = 285 testScope.runTest { 286 val latest by collectLastValue(underTest.statusBarAppearance) 287 288 whenever( 289 letterboxAppearanceCalculator.getLetterboxAppearance( 290 eq(APPEARANCE), 291 eq(APPEARANCE_REGIONS), 292 eq(LETTERBOX_DETAILS), 293 any(), 294 ) 295 ) 296 .thenReturn(CALCULATOR_LETTERBOX_APPEARANCE) 297 298 // WHEN the letterbox details are empty 299 onSystemBarAttributesChanged( 300 appearance = APPEARANCE, 301 appearanceRegions = APPEARANCE_REGIONS.toTypedArray(), 302 letterboxDetails = emptyArray(), 303 ) 304 305 // THEN the appearance regions passed to the callback are used, *not* 306 // REGIONS_FROM_LETTERBOX_CALCULATOR 307 assertThat(latest!!.appearanceRegions).isEqualTo(APPEARANCE_REGIONS) 308 assertThat(latest!!.appearanceRegions).isNotEqualTo(REGIONS_FROM_LETTERBOX_CALCULATOR) 309 } 310 311 @Test statusBarAppearance_appearanceRegions_letterboxDetails_usesLetterboxCalculatornull312 fun statusBarAppearance_appearanceRegions_letterboxDetails_usesLetterboxCalculator() = 313 testScope.runTest { 314 val latest by collectLastValue(underTest.statusBarAppearance) 315 316 whenever( 317 letterboxAppearanceCalculator.getLetterboxAppearance( 318 eq(APPEARANCE), 319 eq(APPEARANCE_REGIONS), 320 eq(LETTERBOX_DETAILS), 321 any(), 322 ) 323 ) 324 .thenReturn(CALCULATOR_LETTERBOX_APPEARANCE) 325 326 onSystemBarAttributesChanged( 327 appearance = APPEARANCE, 328 appearanceRegions = APPEARANCE_REGIONS.toTypedArray(), 329 letterboxDetails = LETTERBOX_DETAILS.toTypedArray(), 330 ) 331 332 assertThat(latest!!.appearanceRegions).isEqualTo(REGIONS_FROM_LETTERBOX_CALCULATOR) 333 } 334 335 @Test statusBarAppearance_boundsChanged_appearanceReFetchednull336 fun statusBarAppearance_boundsChanged_appearanceReFetched() = 337 testScope.runTest { 338 val latest by collectLastValue(underTest.statusBarAppearance) 339 340 // First, start with some appearances 341 val startingLetterboxAppearance = 342 LetterboxAppearance( 343 APPEARANCE_LIGHT_STATUS_BARS, 344 listOf(AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, Rect(0, 0, 1, 1))) 345 ) 346 whenever( 347 letterboxAppearanceCalculator.getLetterboxAppearance( 348 eq(APPEARANCE), 349 eq(APPEARANCE_REGIONS), 350 eq(LETTERBOX_DETAILS), 351 any(), 352 ) 353 ) 354 .thenReturn(startingLetterboxAppearance) 355 onSystemBarAttributesChanged( 356 appearance = APPEARANCE, 357 appearanceRegions = APPEARANCE_REGIONS.toTypedArray(), 358 letterboxDetails = LETTERBOX_DETAILS.toTypedArray(), 359 ) 360 assertThat(latest!!.mode).isEqualTo(StatusBarMode.TRANSPARENT) 361 assertThat(latest!!.appearanceRegions) 362 .isEqualTo(startingLetterboxAppearance.appearanceRegions) 363 364 // WHEN there's a new appearance and we get new status bar bounds 365 val newLetterboxAppearance = 366 LetterboxAppearance( 367 APPEARANCE_LOW_PROFILE_BARS, 368 listOf(AppearanceRegion(APPEARANCE_LOW_PROFILE_BARS, Rect(10, 20, 30, 40))) 369 ) 370 whenever( 371 letterboxAppearanceCalculator.getLetterboxAppearance( 372 eq(APPEARANCE), 373 eq(APPEARANCE_REGIONS), 374 eq(LETTERBOX_DETAILS), 375 any(), 376 ) 377 ) 378 .thenReturn(newLetterboxAppearance) 379 statusBarBoundsChangeListener.onStatusBarBoundsChanged( 380 BoundsPair(Rect(0, 0, 50, 50), Rect(0, 0, 60, 60)) 381 ) 382 383 // THEN the new appearances are used 384 assertThat(latest!!.mode).isEqualTo(StatusBarMode.LIGHTS_OUT_TRANSPARENT) 385 assertThat(latest!!.appearanceRegions) 386 .isEqualTo(newLetterboxAppearance.appearanceRegions) 387 } 388 389 @Test statusBarMode_ongoingCallAndFullscreen_semiTransparentnull390 fun statusBarMode_ongoingCallAndFullscreen_semiTransparent() = 391 testScope.runTest { 392 val latest by collectLastValue(underTest.statusBarAppearance) 393 394 ongoingCallRepository.setOngoingCallState( 395 OngoingCallModel.InCall(startTimeMs = 34, intent = null) 396 ) 397 onSystemBarAttributesChanged( 398 requestedVisibleTypes = WindowInsets.Type.navigationBars(), 399 ) 400 401 assertThat(latest!!.mode).isEqualTo(StatusBarMode.SEMI_TRANSPARENT) 402 } 403 404 @Test statusBarMode_ongoingCallButNotFullscreen_matchesAppearancenull405 fun statusBarMode_ongoingCallButNotFullscreen_matchesAppearance() = 406 testScope.runTest { 407 val latest by collectLastValue(underTest.statusBarAppearance) 408 409 ongoingCallRepository.setOngoingCallState( 410 OngoingCallModel.InCall(startTimeMs = 789, intent = null) 411 ) 412 onSystemBarAttributesChanged( 413 requestedVisibleTypes = WindowInsets.Type.statusBars(), 414 appearance = APPEARANCE_OPAQUE_STATUS_BARS, 415 ) 416 417 assertThat(latest!!.mode).isEqualTo(StatusBarMode.OPAQUE) 418 } 419 420 @Test statusBarMode_fullscreenButNotOngoingCall_matchesAppearancenull421 fun statusBarMode_fullscreenButNotOngoingCall_matchesAppearance() = 422 testScope.runTest { 423 val latest by collectLastValue(underTest.statusBarAppearance) 424 425 ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall) 426 onSystemBarAttributesChanged( 427 requestedVisibleTypes = WindowInsets.Type.navigationBars(), 428 appearance = APPEARANCE_OPAQUE_STATUS_BARS, 429 ) 430 431 assertThat(latest!!.mode).isEqualTo(StatusBarMode.OPAQUE) 432 } 433 434 @Test statusBarMode_transientShown_semiTransparentnull435 fun statusBarMode_transientShown_semiTransparent() = 436 testScope.runTest { 437 val latest by collectLastValue(underTest.statusBarAppearance) 438 onSystemBarAttributesChanged( 439 appearance = APPEARANCE_OPAQUE_STATUS_BARS, 440 ) 441 442 underTest.showTransient() 443 444 assertThat(latest!!.mode).isEqualTo(StatusBarMode.SEMI_TRANSPARENT) 445 } 446 447 @Test statusBarMode_appearanceLowProfileAndOpaque_lightsOutnull448 fun statusBarMode_appearanceLowProfileAndOpaque_lightsOut() = 449 testScope.runTest { 450 val latest by collectLastValue(underTest.statusBarAppearance) 451 452 onSystemBarAttributesChanged( 453 appearance = APPEARANCE_LOW_PROFILE_BARS or APPEARANCE_OPAQUE_STATUS_BARS, 454 ) 455 456 assertThat(latest!!.mode).isEqualTo(StatusBarMode.LIGHTS_OUT) 457 } 458 459 @Test statusBarMode_appearanceLowProfile_lightsOutTransparentnull460 fun statusBarMode_appearanceLowProfile_lightsOutTransparent() = 461 testScope.runTest { 462 val latest by collectLastValue(underTest.statusBarAppearance) 463 464 onSystemBarAttributesChanged( 465 appearance = APPEARANCE_LOW_PROFILE_BARS, 466 ) 467 468 assertThat(latest!!.mode).isEqualTo(StatusBarMode.LIGHTS_OUT_TRANSPARENT) 469 } 470 471 @Test statusBarMode_appearanceOpaque_opaquenull472 fun statusBarMode_appearanceOpaque_opaque() = 473 testScope.runTest { 474 val latest by collectLastValue(underTest.statusBarAppearance) 475 476 onSystemBarAttributesChanged( 477 appearance = APPEARANCE_OPAQUE_STATUS_BARS, 478 ) 479 480 assertThat(latest!!.mode).isEqualTo(StatusBarMode.OPAQUE) 481 } 482 483 @Test statusBarMode_appearanceSemiTransparent_semiTransparentnull484 fun statusBarMode_appearanceSemiTransparent_semiTransparent() = 485 testScope.runTest { 486 val latest by collectLastValue(underTest.statusBarAppearance) 487 488 onSystemBarAttributesChanged( 489 appearance = APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS, 490 ) 491 492 assertThat(latest!!.mode).isEqualTo(StatusBarMode.SEMI_TRANSPARENT) 493 } 494 495 @Test statusBarMode_appearanceNone_transparentnull496 fun statusBarMode_appearanceNone_transparent() = 497 testScope.runTest { 498 val latest by collectLastValue(underTest.statusBarAppearance) 499 500 onSystemBarAttributesChanged( 501 appearance = 0, 502 ) 503 504 assertThat(latest!!.mode).isEqualTo(StatusBarMode.TRANSPARENT) 505 } 506 onSystemBarAttributesChangednull507 private fun onSystemBarAttributesChanged( 508 displayId: Int = DISPLAY_ID, 509 @WindowInsetsController.Appearance appearance: Int = APPEARANCE_OPAQUE_STATUS_BARS, 510 appearanceRegions: Array<AppearanceRegion> = emptyArray(), 511 navbarColorManagedByIme: Boolean = false, 512 @WindowInsetsController.Behavior behavior: Int = WindowInsetsController.BEHAVIOR_DEFAULT, 513 @WindowInsets.Type.InsetsType 514 requestedVisibleTypes: Int = WindowInsets.Type.defaultVisible(), 515 packageName: String = "package name", 516 letterboxDetails: Array<LetterboxDetails> = emptyArray(), 517 ) { 518 commandQueueCallback.onSystemBarAttributesChanged( 519 displayId, 520 appearance, 521 appearanceRegions, 522 navbarColorManagedByIme, 523 behavior, 524 requestedVisibleTypes, 525 packageName, 526 letterboxDetails, 527 ) 528 } 529 530 private companion object { 531 const val DISPLAY_ID = 5 532 private const val APPEARANCE = APPEARANCE_LIGHT_STATUS_BARS 533 private val APPEARANCE_REGION = AppearanceRegion(APPEARANCE, Rect(0, 0, 150, 300)) 534 private val APPEARANCE_REGIONS = listOf(APPEARANCE_REGION) 535 private val LETTERBOX_DETAILS = 536 listOf( 537 LetterboxDetails( 538 /* letterboxInnerBounds= */ Rect(0, 0, 10, 10), 539 /* letterboxFullBounds= */ Rect(0, 0, 20, 20), 540 /* appAppearance= */ 0 541 ) 542 ) 543 private val REGIONS_FROM_LETTERBOX_CALCULATOR = 544 listOf(AppearanceRegion(APPEARANCE, Rect(0, 0, 10, 20))) 545 private const val LETTERBOXED_APPEARANCE = APPEARANCE_LOW_PROFILE_BARS 546 private val CALCULATOR_LETTERBOX_APPEARANCE = 547 LetterboxAppearance(LETTERBOXED_APPEARANCE, REGIONS_FROM_LETTERBOX_CALCULATOR) 548 } 549 } 550