1 /* <lambda>null2 * Copyright (C) 2022 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 package com.android.systemui.shade 17 18 import android.animation.Animator 19 import android.app.AlarmManager 20 import android.app.PendingIntent 21 import android.app.StatusBarManager 22 import android.content.Context 23 import android.content.res.Resources 24 import android.content.res.XmlResourceParser 25 import android.graphics.Insets 26 import android.graphics.Rect 27 import android.testing.AndroidTestingRunner 28 import android.view.Display 29 import android.view.DisplayCutout 30 import android.view.View 31 import android.view.ViewPropertyAnimator 32 import android.view.WindowInsets 33 import android.widget.LinearLayout 34 import android.widget.TextView 35 import androidx.constraintlayout.motion.widget.MotionLayout 36 import androidx.constraintlayout.widget.ConstraintSet 37 import androidx.test.filters.SmallTest 38 import com.android.app.animation.Interpolators 39 import com.android.systemui.SysuiTestCase 40 import com.android.systemui.animation.ShadeInterpolation 41 import com.android.systemui.battery.BatteryMeterView 42 import com.android.systemui.battery.BatteryMeterViewController 43 import com.android.systemui.demomode.DemoMode 44 import com.android.systemui.demomode.DemoModeController 45 import com.android.systemui.dump.DumpManager 46 import com.android.systemui.plugins.ActivityStarter 47 import com.android.systemui.qs.ChipVisibilityListener 48 import com.android.systemui.qs.HeaderPrivacyIconsController 49 import com.android.systemui.res.R 50 import com.android.systemui.shade.ShadeHeaderController.Companion.DEFAULT_CLOCK_INTENT 51 import com.android.systemui.shade.ShadeHeaderController.Companion.LARGE_SCREEN_HEADER_CONSTRAINT 52 import com.android.systemui.shade.ShadeHeaderController.Companion.QQS_HEADER_CONSTRAINT 53 import com.android.systemui.shade.ShadeHeaderController.Companion.QS_HEADER_CONSTRAINT 54 import com.android.systemui.shade.carrier.ShadeCarrierGroup 55 import com.android.systemui.shade.carrier.ShadeCarrierGroupController 56 import com.android.systemui.statusbar.data.repository.fakeStatusBarContentInsetsProviderStore 57 import com.android.systemui.statusbar.phone.StatusIconContainer 58 import com.android.systemui.statusbar.phone.StatusOverlayHoverListenerFactory 59 import com.android.systemui.statusbar.phone.ui.StatusBarIconController 60 import com.android.systemui.statusbar.phone.ui.TintedIconManager 61 import com.android.systemui.statusbar.policy.Clock 62 import com.android.systemui.statusbar.policy.FakeConfigurationController 63 import com.android.systemui.statusbar.policy.NextAlarmController 64 import com.android.systemui.statusbar.policy.VariableDateView 65 import com.android.systemui.statusbar.policy.VariableDateViewController 66 import com.android.systemui.testKosmos 67 import com.android.systemui.util.mockito.any 68 import com.android.systemui.util.mockito.argumentCaptor 69 import com.android.systemui.util.mockito.capture 70 import com.android.systemui.util.mockito.eq 71 import com.android.systemui.util.mockito.mock 72 import com.google.common.truth.Truth.assertThat 73 import org.junit.Before 74 import org.junit.Rule 75 import org.junit.Test 76 import org.junit.runner.RunWith 77 import org.mockito.Answers 78 import org.mockito.ArgumentCaptor 79 import org.mockito.ArgumentMatchers.anyFloat 80 import org.mockito.ArgumentMatchers.anyInt 81 import org.mockito.Captor 82 import org.mockito.Mock 83 import org.mockito.Mockito 84 import org.mockito.Mockito.mock 85 import org.mockito.Mockito.reset 86 import org.mockito.Mockito.times 87 import org.mockito.Mockito.verify 88 import org.mockito.Mockito.`when` as whenever 89 import org.mockito.junit.MockitoJUnit 90 91 private val EMPTY_CHANGES = ConstraintsChanges() 92 93 @SmallTest 94 @RunWith(AndroidTestingRunner::class) 95 class ShadeHeaderControllerTest : SysuiTestCase() { 96 97 private val kosmos = testKosmos() 98 private val insetsProviderStore = kosmos.fakeStatusBarContentInsetsProviderStore 99 private val insetsProvider = insetsProviderStore.defaultDisplay 100 101 @Mock(answer = Answers.RETURNS_MOCKS) private lateinit var view: MotionLayout 102 @Mock private lateinit var statusIcons: StatusIconContainer 103 @Mock private lateinit var statusBarIconController: StatusBarIconController 104 @Mock private lateinit var iconManagerFactory: TintedIconManager.Factory 105 @Mock private lateinit var iconManager: TintedIconManager 106 @Mock private lateinit var mShadeCarrierGroupController: ShadeCarrierGroupController 107 @Mock 108 private lateinit var mShadeCarrierGroupControllerBuilder: ShadeCarrierGroupController.Builder 109 @Mock private lateinit var clock: Clock 110 @Mock private lateinit var date: VariableDateView 111 @Mock private lateinit var carrierGroup: ShadeCarrierGroup 112 @Mock private lateinit var batteryMeterView: BatteryMeterView 113 @Mock private lateinit var batteryMeterViewController: BatteryMeterViewController 114 @Mock private lateinit var privacyIconsController: HeaderPrivacyIconsController 115 @Mock private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory 116 @Mock private lateinit var variableDateViewController: VariableDateViewController 117 @Mock private lateinit var dumpManager: DumpManager 118 @Mock 119 private lateinit var combinedShadeHeadersConstraintManager: 120 CombinedShadeHeadersConstraintManager 121 122 @Mock private lateinit var mockedContext: Context 123 private lateinit var viewContext: Context 124 125 @Mock private lateinit var qqsConstraints: ConstraintSet 126 @Mock private lateinit var qsConstraints: ConstraintSet 127 @Mock private lateinit var largeScreenConstraints: ConstraintSet 128 129 @Mock private lateinit var demoModeController: DemoModeController 130 @Mock private lateinit var qsBatteryModeController: QsBatteryModeController 131 @Mock private lateinit var nextAlarmController: NextAlarmController 132 @Mock private lateinit var activityStarter: ActivityStarter 133 @Mock private lateinit var mStatusOverlayHoverListenerFactory: StatusOverlayHoverListenerFactory 134 135 @JvmField @Rule val mockitoRule = MockitoJUnit.rule() 136 var viewVisibility = View.GONE 137 var viewAlpha = 1f 138 139 private val systemIconsHoverContainer = LinearLayout(context) 140 private lateinit var shadeHeaderController: ShadeHeaderController 141 private lateinit var carrierIconSlots: List<String> 142 private val configurationController = FakeConfigurationController() 143 @Captor private lateinit var demoModeControllerCapture: ArgumentCaptor<DemoMode> 144 145 @Before 146 fun setup() { 147 whenever<Clock>(view.requireViewById(R.id.clock)).thenReturn(clock) 148 whenever(clock.context).thenReturn(mockedContext) 149 150 whenever<TextView>(view.requireViewById(R.id.date)).thenReturn(date) 151 whenever(date.context).thenReturn(mockedContext) 152 153 whenever<ShadeCarrierGroup>(view.requireViewById(R.id.carrier_group)) 154 .thenReturn(carrierGroup) 155 156 whenever<BatteryMeterView>(view.requireViewById(R.id.batteryRemainingIcon)) 157 .thenReturn(batteryMeterView) 158 159 whenever<StatusIconContainer>(view.requireViewById(R.id.statusIcons)) 160 .thenReturn(statusIcons) 161 whenever<View>(view.requireViewById(R.id.hover_system_icons_container)) 162 .thenReturn(systemIconsHoverContainer) 163 164 viewContext = Mockito.spy(context) 165 whenever(view.context).thenReturn(viewContext) 166 whenever(view.resources).thenReturn(context.resources) 167 whenever(statusIcons.context).thenReturn(context) 168 whenever(mShadeCarrierGroupControllerBuilder.setShadeCarrierGroup(any())) 169 .thenReturn(mShadeCarrierGroupControllerBuilder) 170 whenever(mShadeCarrierGroupControllerBuilder.build()) 171 .thenReturn(mShadeCarrierGroupController) 172 whenever(view.setVisibility(anyInt())).then { 173 viewVisibility = it.arguments[0] as Int 174 null 175 } 176 whenever(view.visibility).thenAnswer { _ -> viewVisibility } 177 178 whenever(view.setAlpha(anyFloat())).then { 179 viewAlpha = it.arguments[0] as Float 180 null 181 } 182 whenever(view.alpha).thenAnswer { _ -> viewAlpha } 183 184 whenever(variableDateViewControllerFactory.create(any())) 185 .thenReturn(variableDateViewController) 186 whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) 187 188 setUpDefaultInsets() 189 setUpMotionLayout(view) 190 191 shadeHeaderController = 192 ShadeHeaderController( 193 view, 194 statusBarIconController, 195 iconManagerFactory, 196 privacyIconsController, 197 insetsProviderStore, 198 configurationController, 199 variableDateViewControllerFactory, 200 batteryMeterViewController, 201 dumpManager, 202 mShadeCarrierGroupControllerBuilder, 203 combinedShadeHeadersConstraintManager, 204 demoModeController, 205 qsBatteryModeController, 206 nextAlarmController, 207 activityStarter, 208 mStatusOverlayHoverListenerFactory, 209 ) 210 whenever(view.isAttachedToWindow).thenReturn(true) 211 shadeHeaderController.init() 212 carrierIconSlots = 213 listOf(context.getString(com.android.internal.R.string.status_bar_mobile)) 214 } 215 216 @Test 217 fun updateListeners_registersWhenVisible() { 218 makeShadeVisible() 219 verify(mShadeCarrierGroupController).setListening(true) 220 verify(statusBarIconController).addIconGroup(any()) 221 } 222 223 @Test 224 fun statusIconsAddedWhenAttached() { 225 verify(statusBarIconController).addIconGroup(any()) 226 } 227 228 @Test 229 fun statusIconsRemovedWhenDettached() { 230 shadeHeaderController.simulateViewDetached() 231 verify(statusBarIconController).removeIconGroup(any()) 232 } 233 234 @Test 235 fun shadeExpandedFraction_updatesAlpha() { 236 makeShadeVisible() 237 shadeHeaderController.shadeExpandedFraction = 0.5f 238 verify(view).setAlpha(ShadeInterpolation.getContentAlpha(0.5f)) 239 } 240 241 @Test 242 fun singleCarrier_enablesCarrierIconsInStatusIcons() { 243 whenever(mShadeCarrierGroupController.isSingleCarrier).thenReturn(true) 244 245 makeShadeVisible() 246 247 verify(statusIcons).removeIgnoredSlots(carrierIconSlots) 248 } 249 250 @Test 251 fun dualCarrier_disablesCarrierIconsInStatusIcons_qs() { 252 whenever(mShadeCarrierGroupController.isSingleCarrier).thenReturn(false) 253 254 makeShadeVisible() 255 shadeHeaderController.qsExpandedFraction = 1.0f 256 257 verify(statusIcons, times(2)).addIgnoredSlots(carrierIconSlots) 258 } 259 260 @Test 261 fun dualCarrier_disablesCarrierIconsInStatusIcons_qqs() { 262 whenever(mShadeCarrierGroupController.isSingleCarrier).thenReturn(false) 263 264 makeShadeVisible() 265 shadeHeaderController.qsExpandedFraction = 0.0f 266 267 verify(statusIcons, times(2)).addIgnoredSlots(carrierIconSlots) 268 } 269 270 @Test 271 fun disableQS_notDisabled_visible() { 272 makeShadeVisible() 273 shadeHeaderController.disable(0, 0, false) 274 275 assertThat(viewVisibility).isEqualTo(View.VISIBLE) 276 } 277 278 @Test 279 fun disableQS_disabled_gone() { 280 makeShadeVisible() 281 shadeHeaderController.disable(0, StatusBarManager.DISABLE2_QUICK_SETTINGS, false) 282 283 assertThat(viewVisibility).isEqualTo(View.GONE) 284 } 285 286 private fun makeShadeVisible() { 287 shadeHeaderController.largeScreenActive = true 288 shadeHeaderController.qsVisible = true 289 } 290 291 @Test 292 fun updateConfig_changesFontStyle() { 293 configurationController.notifyDensityOrFontScaleChanged() 294 295 verify(clock).setTextAppearance(R.style.TextAppearance_QS_Status) 296 verify(date).setTextAppearance(R.style.TextAppearance_QS_Status) 297 verify(carrierGroup).updateTextAppearance(R.style.TextAppearance_QS_Status_Carriers) 298 } 299 300 @Test 301 fun animateOutOnStartCustomizing() { 302 val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF) 303 val duration = 1000L 304 whenever(view.animate()).thenReturn(animator) 305 306 shadeHeaderController.startCustomizingAnimation(show = true, duration) 307 308 verify(animator).setDuration(duration) 309 verify(animator).alpha(0f) 310 verify(animator).setInterpolator(Interpolators.ALPHA_OUT) 311 verify(animator).start() 312 } 313 314 @Test 315 fun animateInOnEndCustomizing() { 316 val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF) 317 val duration = 1000L 318 whenever(view.animate()).thenReturn(animator) 319 320 shadeHeaderController.startCustomizingAnimation(show = false, duration) 321 322 verify(animator).setDuration(duration) 323 verify(animator).alpha(1f) 324 verify(animator).setInterpolator(Interpolators.ALPHA_IN) 325 verify(animator).start() 326 } 327 328 @Test 329 fun customizerAnimatorChangesViewVisibility() { 330 makeShadeVisible() 331 332 val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF) 333 val duration = 1000L 334 whenever(view.animate()).thenReturn(animator) 335 val listenerCaptor = argumentCaptor<Animator.AnimatorListener>() 336 337 shadeHeaderController.startCustomizingAnimation(show = true, duration) 338 verify(animator).setListener(capture(listenerCaptor)) 339 // Start and end the animation 340 listenerCaptor.value.onAnimationStart(mock()) 341 listenerCaptor.value.onAnimationEnd(mock()) 342 assertThat(viewVisibility).isEqualTo(View.INVISIBLE) 343 344 reset(animator) 345 shadeHeaderController.startCustomizingAnimation(show = false, duration) 346 verify(animator).setListener(capture(listenerCaptor)) 347 // Start and end the animation 348 listenerCaptor.value.onAnimationStart(mock()) 349 listenerCaptor.value.onAnimationEnd(mock()) 350 assertThat(viewVisibility).isEqualTo(View.VISIBLE) 351 } 352 353 @Test 354 fun animatorListenersClearedAtEnd() { 355 val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF) 356 whenever(view.animate()).thenReturn(animator) 357 358 shadeHeaderController.startCustomizingAnimation(show = true, 0L) 359 val listenerCaptor = argumentCaptor<Animator.AnimatorListener>() 360 verify(animator).setListener(capture(listenerCaptor)) 361 362 listenerCaptor.value.onAnimationEnd(mock()) 363 verify(animator).setListener(null) 364 } 365 366 @Test 367 fun demoMode_attachDemoMode() { 368 val cb = argumentCaptor<DemoMode>() 369 verify(demoModeController).addCallback(capture(cb)) 370 cb.value.onDemoModeStarted() 371 verify(clock).onDemoModeStarted() 372 } 373 374 @Test 375 fun demoMode_detachDemoMode() { 376 shadeHeaderController.simulateViewDetached() 377 val cb = argumentCaptor<DemoMode>() 378 verify(demoModeController).removeCallback(capture(cb)) 379 cb.value.onDemoModeFinished() 380 verify(clock).onDemoModeFinished() 381 } 382 383 @Test 384 fun testControllersCreatedAndInitialized() { 385 verify(variableDateViewController).init() 386 387 verify(batteryMeterViewController).init() 388 verify(batteryMeterViewController).ignoreTunerUpdates() 389 390 val inOrder = Mockito.inOrder(mShadeCarrierGroupControllerBuilder) 391 inOrder.verify(mShadeCarrierGroupControllerBuilder).setShadeCarrierGroup(carrierGroup) 392 inOrder.verify(mShadeCarrierGroupControllerBuilder).build() 393 } 394 395 @Test 396 fun batteryModeControllerCalledWhenQsExpandedFractionChanges() { 397 whenever(qsBatteryModeController.getBatteryMode(Mockito.same(null), eq(0f))) 398 .thenReturn(BatteryMeterView.MODE_ON) 399 whenever(qsBatteryModeController.getBatteryMode(Mockito.same(null), eq(1f))) 400 .thenReturn(BatteryMeterView.MODE_ESTIMATE) 401 shadeHeaderController.qsVisible = true 402 403 val times = 10 404 repeat(times) { shadeHeaderController.qsExpandedFraction = it / (times - 1).toFloat() } 405 406 verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ON) 407 verify(batteryMeterView).setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) 408 } 409 410 @Test 411 fun testClockPivotLtr() { 412 val width = 200 413 whenever(clock.width).thenReturn(width) 414 whenever(clock.isLayoutRtl).thenReturn(false) 415 416 val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java) 417 verify(clock, times(2)).addOnLayoutChangeListener(capture(captor)) 418 419 captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7) 420 verify(clock).pivotX = 0f 421 } 422 423 @Test 424 fun testClockPivotRtl() { 425 val width = 200 426 whenever(clock.width).thenReturn(width) 427 whenever(clock.isLayoutRtl).thenReturn(true) 428 429 val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java) 430 verify(clock, times(2)).addOnLayoutChangeListener(capture(captor)) 431 432 captor.value.onLayoutChange(clock, 0, 1, 2, 3, 4, 5, 6, 7) 433 verify(clock).pivotX = width.toFloat() 434 } 435 436 @Test 437 fun testShadeExpanded_true() { 438 // When shade is expanded, view should be visible regardless of largeScreenActive 439 shadeHeaderController.largeScreenActive = false 440 shadeHeaderController.qsVisible = true 441 assertThat(viewVisibility).isEqualTo(View.VISIBLE) 442 443 shadeHeaderController.largeScreenActive = true 444 assertThat(viewVisibility).isEqualTo(View.VISIBLE) 445 } 446 447 @Test 448 fun testShadeExpanded_false() { 449 // When shade is not expanded, view should be invisible regardless of largeScreenActive 450 shadeHeaderController.largeScreenActive = false 451 shadeHeaderController.qsVisible = false 452 assertThat(viewVisibility).isEqualTo(View.INVISIBLE) 453 454 shadeHeaderController.largeScreenActive = true 455 assertThat(viewVisibility).isEqualTo(View.INVISIBLE) 456 } 457 458 @Test 459 fun testLargeScreenActive_false() { 460 shadeHeaderController.largeScreenActive = true // Make sure there's a change 461 Mockito.clearInvocations(view) 462 463 shadeHeaderController.largeScreenActive = false 464 465 verify(view).setTransition(ShadeHeaderController.HEADER_TRANSITION_ID) 466 } 467 468 @Test 469 fun testLargeScreenActive_collapseActionRun_onSystemIconsHoverContainerClick() { 470 shadeHeaderController.largeScreenActive = true 471 var wasRun = false 472 shadeHeaderController.shadeCollapseAction = Runnable { wasRun = true } 473 474 systemIconsHoverContainer.performClick() 475 476 assertThat(wasRun).isTrue() 477 } 478 479 @Test 480 fun testShadeExpandedFraction() { 481 // View needs to be visible for this to actually take effect 482 shadeHeaderController.qsVisible = true 483 484 Mockito.clearInvocations(view) 485 shadeHeaderController.shadeExpandedFraction = 0.3f 486 verify(view).alpha = ShadeInterpolation.getContentAlpha(0.3f) 487 488 Mockito.clearInvocations(view) 489 shadeHeaderController.shadeExpandedFraction = 1f 490 verify(view).alpha = ShadeInterpolation.getContentAlpha(1f) 491 492 Mockito.clearInvocations(view) 493 shadeHeaderController.shadeExpandedFraction = 0f 494 verify(view).alpha = ShadeInterpolation.getContentAlpha(0f) 495 } 496 497 @Test 498 fun testQsExpandedFraction_headerTransition() { 499 shadeHeaderController.qsVisible = true 500 shadeHeaderController.largeScreenActive = false 501 502 Mockito.clearInvocations(view) 503 shadeHeaderController.qsExpandedFraction = 0.3f 504 verify(view).progress = 0.3f 505 } 506 507 @Test 508 fun testQsExpandedFraction_largeScreen() { 509 shadeHeaderController.qsVisible = true 510 shadeHeaderController.largeScreenActive = true 511 512 Mockito.clearInvocations(view) 513 shadeHeaderController.qsExpandedFraction = 0.3f 514 verify(view, Mockito.never()).progress = anyFloat() 515 } 516 517 @Test 518 fun testScrollY_headerTransition() { 519 shadeHeaderController.largeScreenActive = false 520 521 Mockito.clearInvocations(view) 522 shadeHeaderController.qsScrollY = 20 523 verify(view).scrollY = 20 524 } 525 526 @Test 527 fun testScrollY_largeScreen() { 528 shadeHeaderController.largeScreenActive = true 529 530 Mockito.clearInvocations(view) 531 shadeHeaderController.qsScrollY = 20 532 verify(view, Mockito.never()).scrollY = anyInt() 533 } 534 535 @Test 536 fun testPrivacyChipVisibilityChanged_visible_changesCorrectConstraints() { 537 val chipVisibleChanges = createMockConstraintChanges() 538 val chipNotVisibleChanges = createMockConstraintChanges() 539 540 whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true)) 541 .thenReturn(chipVisibleChanges) 542 whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false)) 543 .thenReturn(chipNotVisibleChanges) 544 545 val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java) 546 verify(privacyIconsController).chipVisibilityListener = capture(captor) 547 548 captor.value.onChipVisibilityRefreshed(true) 549 550 verify(chipVisibleChanges.qqsConstraintsChanges)!!.invoke(qqsConstraints) 551 verify(chipVisibleChanges.qsConstraintsChanges)!!.invoke(qsConstraints) 552 verify(chipVisibleChanges.largeScreenConstraintsChanges)!!.invoke(largeScreenConstraints) 553 554 verify(chipNotVisibleChanges.qqsConstraintsChanges, Mockito.never())!!.invoke(any()) 555 verify(chipNotVisibleChanges.qsConstraintsChanges, Mockito.never())!!.invoke(any()) 556 verify(chipNotVisibleChanges.largeScreenConstraintsChanges, Mockito.never())!!.invoke(any()) 557 } 558 559 @Test 560 fun testPrivacyChipVisibilityChanged_notVisible_changesCorrectConstraints() { 561 val chipVisibleChanges = createMockConstraintChanges() 562 val chipNotVisibleChanges = createMockConstraintChanges() 563 564 whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(true)) 565 .thenReturn(chipVisibleChanges) 566 whenever(combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(false)) 567 .thenReturn(chipNotVisibleChanges) 568 569 val captor = ArgumentCaptor.forClass(ChipVisibilityListener::class.java) 570 verify(privacyIconsController).chipVisibilityListener = capture(captor) 571 572 captor.value.onChipVisibilityRefreshed(false) 573 574 verify(chipVisibleChanges.qqsConstraintsChanges, Mockito.never())!!.invoke(qqsConstraints) 575 verify(chipVisibleChanges.qsConstraintsChanges, Mockito.never())!!.invoke(qsConstraints) 576 verify(chipVisibleChanges.largeScreenConstraintsChanges, Mockito.never())!!.invoke( 577 largeScreenConstraints 578 ) 579 580 verify(chipNotVisibleChanges.qqsConstraintsChanges)!!.invoke(any()) 581 verify(chipNotVisibleChanges.qsConstraintsChanges)!!.invoke(any()) 582 verify(chipNotVisibleChanges.largeScreenConstraintsChanges)!!.invoke(any()) 583 } 584 585 @Test 586 fun testInsetsGuides_ltr() { 587 whenever(view.isLayoutRtl).thenReturn(false) 588 val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) 589 verify(view).setOnApplyWindowInsetsListener(capture(captor)) 590 val mockConstraintsChanges = createMockConstraintChanges() 591 592 val (insetLeft, insetRight) = 30 to 40 593 val (paddingStart, paddingEnd) = 10 to 20 594 whenever(view.paddingStart).thenReturn(paddingStart) 595 whenever(view.paddingEnd).thenReturn(paddingEnd) 596 597 mockInsetsProvider(insetLeft to insetRight, false) 598 599 whenever( 600 combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints( 601 anyInt(), 602 anyInt(), 603 anyInt(), 604 anyInt(), 605 ) 606 ) 607 .thenReturn(mockConstraintsChanges) 608 609 captor.value.onApplyWindowInsets(view, createWindowInsets()) 610 611 verify(combinedShadeHeadersConstraintManager) 612 .edgesGuidelinesConstraints(insetLeft, paddingStart, insetRight, paddingEnd) 613 614 verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any()) 615 verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any()) 616 verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any()) 617 } 618 619 @Test 620 fun testInsetsGuides_rtl() { 621 whenever(view.isLayoutRtl).thenReturn(true) 622 val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) 623 verify(view).setOnApplyWindowInsetsListener(capture(captor)) 624 val mockConstraintsChanges = createMockConstraintChanges() 625 626 val (insetLeft, insetRight) = 30 to 40 627 val (paddingStart, paddingEnd) = 10 to 20 628 whenever(view.paddingStart).thenReturn(paddingStart) 629 whenever(view.paddingEnd).thenReturn(paddingEnd) 630 631 mockInsetsProvider(insetLeft to insetRight, false) 632 633 whenever( 634 combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints( 635 anyInt(), 636 anyInt(), 637 anyInt(), 638 anyInt(), 639 ) 640 ) 641 .thenReturn(mockConstraintsChanges) 642 643 captor.value.onApplyWindowInsets(view, createWindowInsets()) 644 645 verify(combinedShadeHeadersConstraintManager) 646 .edgesGuidelinesConstraints(insetRight, paddingStart, insetLeft, paddingEnd) 647 648 verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any()) 649 verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any()) 650 verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any()) 651 } 652 653 @Test 654 fun testNullCutout() { 655 val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) 656 verify(view).setOnApplyWindowInsetsListener(capture(captor)) 657 val mockConstraintsChanges = createMockConstraintChanges() 658 659 whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints()) 660 .thenReturn(mockConstraintsChanges) 661 662 captor.value.onApplyWindowInsets(view, createWindowInsets(null)) 663 664 verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints() 665 verify(combinedShadeHeadersConstraintManager, Mockito.never()) 666 .centerCutoutConstraints(Mockito.anyBoolean(), anyInt()) 667 668 verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any()) 669 verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any()) 670 verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any()) 671 } 672 673 @Test 674 fun testEmptyCutout() { 675 val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) 676 verify(view).setOnApplyWindowInsetsListener(capture(captor)) 677 val mockConstraintsChanges = createMockConstraintChanges() 678 679 whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints()) 680 .thenReturn(mockConstraintsChanges) 681 682 captor.value.onApplyWindowInsets(view, createWindowInsets()) 683 684 verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints() 685 verify(combinedShadeHeadersConstraintManager, Mockito.never()) 686 .centerCutoutConstraints(Mockito.anyBoolean(), anyInt()) 687 688 verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any()) 689 verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any()) 690 verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any()) 691 } 692 693 @Test 694 fun testCornerCutout_emptyRect() { 695 val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) 696 verify(view).setOnApplyWindowInsetsListener(capture(captor)) 697 val mockConstraintsChanges = createMockConstraintChanges() 698 699 mockInsetsProvider(0 to 0, true) 700 701 whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints()) 702 .thenReturn(mockConstraintsChanges) 703 704 captor.value.onApplyWindowInsets(view, createWindowInsets()) 705 706 verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints() 707 verify(combinedShadeHeadersConstraintManager, Mockito.never()) 708 .centerCutoutConstraints(Mockito.anyBoolean(), anyInt()) 709 710 verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any()) 711 verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any()) 712 verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any()) 713 } 714 715 @Test 716 fun testCornerCutout_nonEmptyRect() { 717 val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) 718 verify(view).setOnApplyWindowInsetsListener(capture(captor)) 719 val mockConstraintsChanges = createMockConstraintChanges() 720 721 mockInsetsProvider(0 to 0, true) 722 723 whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints()) 724 .thenReturn(mockConstraintsChanges) 725 726 captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(1, 2, 3, 4))) 727 728 verify(combinedShadeHeadersConstraintManager).emptyCutoutConstraints() 729 verify(combinedShadeHeadersConstraintManager, Mockito.never()) 730 .centerCutoutConstraints(Mockito.anyBoolean(), anyInt()) 731 732 verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any()) 733 verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any()) 734 verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any()) 735 } 736 737 @Test 738 fun testTopCutout_ltr() { 739 val width = 100 740 val paddingLeft = 10 741 val paddingRight = 20 742 val cutoutWidth = 30 743 744 whenever(view.isLayoutRtl).thenReturn(false) 745 whenever(view.width).thenReturn(width) 746 whenever(view.paddingLeft).thenReturn(paddingLeft) 747 whenever(view.paddingRight).thenReturn(paddingRight) 748 749 val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) 750 verify(view).setOnApplyWindowInsetsListener(capture(captor)) 751 val mockConstraintsChanges = createMockConstraintChanges() 752 753 mockInsetsProvider(0 to 0, false) 754 755 whenever( 756 combinedShadeHeadersConstraintManager.centerCutoutConstraints( 757 Mockito.anyBoolean(), 758 anyInt(), 759 ) 760 ) 761 .thenReturn(mockConstraintsChanges) 762 763 captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1))) 764 765 verify(combinedShadeHeadersConstraintManager, Mockito.never()).emptyCutoutConstraints() 766 val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2 767 verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(false, offset) 768 769 verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any()) 770 verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any()) 771 verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any()) 772 } 773 774 @Test 775 fun testTopCutout_rtl() { 776 val width = 100 777 val paddingLeft = 10 778 val paddingRight = 20 779 val cutoutWidth = 30 780 781 whenever(view.isLayoutRtl).thenReturn(true) 782 whenever(view.width).thenReturn(width) 783 whenever(view.paddingLeft).thenReturn(paddingLeft) 784 whenever(view.paddingRight).thenReturn(paddingRight) 785 786 val captor = ArgumentCaptor.forClass(View.OnApplyWindowInsetsListener::class.java) 787 verify(view).setOnApplyWindowInsetsListener(capture(captor)) 788 val mockConstraintsChanges = createMockConstraintChanges() 789 790 mockInsetsProvider(0 to 0, false) 791 792 whenever( 793 combinedShadeHeadersConstraintManager.centerCutoutConstraints( 794 Mockito.anyBoolean(), 795 anyInt(), 796 ) 797 ) 798 .thenReturn(mockConstraintsChanges) 799 800 captor.value.onApplyWindowInsets(view, createWindowInsets(Rect(0, 0, cutoutWidth, 1))) 801 802 verify(combinedShadeHeadersConstraintManager, Mockito.never()).emptyCutoutConstraints() 803 val offset = (width - paddingLeft - paddingRight - cutoutWidth) / 2 804 verify(combinedShadeHeadersConstraintManager).centerCutoutConstraints(true, offset) 805 806 verify(mockConstraintsChanges.qqsConstraintsChanges)!!.invoke(any()) 807 verify(mockConstraintsChanges.qsConstraintsChanges)!!.invoke(any()) 808 verify(mockConstraintsChanges.largeScreenConstraintsChanges)!!.invoke(any()) 809 } 810 811 @Test 812 fun alarmIconNotIgnored() { 813 verify(statusIcons, Mockito.never()) 814 .addIgnoredSlot(context.getString(com.android.internal.R.string.status_bar_alarm_clock)) 815 } 816 817 @Test 818 fun privacyChipParentVisibleFromStart() { 819 verify(privacyIconsController).onParentVisible() 820 } 821 822 @Test 823 fun privacyChipParentVisibleAlways() { 824 shadeHeaderController.largeScreenActive = true 825 shadeHeaderController.largeScreenActive = false 826 shadeHeaderController.largeScreenActive = true 827 828 verify(privacyIconsController, Mockito.never()).onParentInvisible() 829 } 830 831 @Test 832 fun clockPivotYInCenter() { 833 val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java) 834 verify(clock, times(2)).addOnLayoutChangeListener(capture(captor)) 835 var height = 100 836 val width = 50 837 838 clock.executeLayoutChange(0, 0, width, height, captor.value) 839 verify(clock).pivotY = height.toFloat() / 2 840 841 height = 150 842 clock.executeLayoutChange(0, 0, width, height, captor.value) 843 verify(clock).pivotY = height.toFloat() / 2 844 } 845 846 @Test 847 fun onDensityOrFontScaleChanged_reloadConstraints() { 848 // After density or font scale change, constraints need to be reloaded to reflect new 849 // dimensions. 850 Mockito.reset(qqsConstraints) 851 Mockito.reset(qsConstraints) 852 Mockito.reset(largeScreenConstraints) 853 854 configurationController.notifyDensityOrFontScaleChanged() 855 856 val captor = ArgumentCaptor.forClass(XmlResourceParser::class.java) 857 verify(qqsConstraints).load(eq(viewContext), capture(captor)) 858 assertThat(captor.value.getResId()).isEqualTo(R.xml.qqs_header) 859 verify(qsConstraints).load(eq(viewContext), capture(captor)) 860 assertThat(captor.value.getResId()).isEqualTo(R.xml.qs_header) 861 verify(largeScreenConstraints).load(eq(viewContext), capture(captor)) 862 assertThat(captor.value.getResId()).isEqualTo(R.xml.large_screen_shade_header) 863 } 864 865 @Test 866 fun carrierStartPaddingIsSetOnClockLayout() { 867 val clockWidth = 200 868 val maxClockScale = context.resources.getFloat(R.dimen.qqs_expand_clock_scale) 869 val expectedStartPadding = (clockWidth * maxClockScale).toInt() 870 whenever(clock.width).thenReturn(clockWidth) 871 872 val captor = ArgumentCaptor.forClass(View.OnLayoutChangeListener::class.java) 873 verify(clock, times(2)).addOnLayoutChangeListener(capture(captor)) 874 captor.allValues.forEach { clock.executeLayoutChange(0, 0, clockWidth, 0, it) } 875 876 verify(carrierGroup).setPaddingRelative(expectedStartPadding, 0, 0, 0) 877 } 878 879 @Test 880 fun launchClock_launchesDefaultIntentWhenNoAlarmSet() { 881 shadeHeaderController.launchClockActivity() 882 883 verify(activityStarter).postStartActivityDismissingKeyguard(DEFAULT_CLOCK_INTENT, 0) 884 } 885 886 @Test 887 fun launchClock_launchesNextAlarmWhenExists() { 888 val pendingIntent = mock<PendingIntent>() 889 val aci = AlarmManager.AlarmClockInfo(12345, pendingIntent) 890 val captor = 891 ArgumentCaptor.forClass(NextAlarmController.NextAlarmChangeCallback::class.java) 892 893 verify(nextAlarmController).addCallback(capture(captor)) 894 captor.value.onNextAlarmChanged(aci) 895 896 shadeHeaderController.launchClockActivity() 897 898 verify(activityStarter).postStartActivityDismissingKeyguard(pendingIntent) 899 } 900 901 private fun View.executeLayoutChange( 902 left: Int, 903 top: Int, 904 right: Int, 905 bottom: Int, 906 listener: View.OnLayoutChangeListener, 907 ) { 908 val oldLeft = this.left 909 val oldTop = this.top 910 val oldRight = this.right 911 val oldBottom = this.bottom 912 whenever(this.left).thenReturn(left) 913 whenever(this.top).thenReturn(top) 914 whenever(this.right).thenReturn(right) 915 whenever(this.bottom).thenReturn(bottom) 916 whenever(this.height).thenReturn(bottom - top) 917 whenever(this.width).thenReturn(right - left) 918 listener.onLayoutChange( 919 this, 920 oldLeft, 921 oldTop, 922 oldRight, 923 oldBottom, 924 left, 925 top, 926 right, 927 bottom, 928 ) 929 } 930 931 private fun createWindowInsets(topCutout: Rect? = Rect()): WindowInsets { 932 val windowInsets: WindowInsets = mock() 933 val displayCutout: DisplayCutout = mock() 934 whenever(windowInsets.displayCutout) 935 .thenReturn(if (topCutout != null) displayCutout else null) 936 whenever(displayCutout.boundingRectTop).thenReturn(topCutout) 937 938 return windowInsets 939 } 940 941 private fun mockInsetsProvider(insets: Pair<Int, Int> = 0 to 0, cornerCutout: Boolean = false) { 942 whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation()) 943 .thenReturn( 944 Insets.of( 945 /* left= */ insets.first, 946 /* top= */ 0, 947 /* right= */ insets.second, 948 /* bottom= */ 0, 949 ) 950 ) 951 whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(cornerCutout) 952 } 953 954 private fun createMockConstraintChanges(): ConstraintsChanges { 955 return ConstraintsChanges(mock(), mock(), mock()) 956 } 957 958 private fun XmlResourceParser.getResId(): Int { 959 return Resources.getAttributeSetSourceResId(this) 960 } 961 962 private fun setUpMotionLayout(motionLayout: MotionLayout) { 963 whenever(motionLayout.getConstraintSet(QQS_HEADER_CONSTRAINT)).thenReturn(qqsConstraints) 964 whenever(motionLayout.getConstraintSet(QS_HEADER_CONSTRAINT)).thenReturn(qsConstraints) 965 whenever(motionLayout.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT)) 966 .thenReturn(largeScreenConstraints) 967 } 968 969 private fun setUpDefaultInsets() { 970 whenever( 971 combinedShadeHeadersConstraintManager.edgesGuidelinesConstraints( 972 anyInt(), 973 anyInt(), 974 anyInt(), 975 anyInt(), 976 ) 977 ) 978 .thenReturn(EMPTY_CHANGES) 979 whenever(combinedShadeHeadersConstraintManager.emptyCutoutConstraints()) 980 .thenReturn(EMPTY_CHANGES) 981 whenever( 982 combinedShadeHeadersConstraintManager.centerCutoutConstraints( 983 Mockito.anyBoolean(), 984 anyInt(), 985 ) 986 ) 987 .thenReturn(EMPTY_CHANGES) 988 whenever( 989 combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints( 990 Mockito.anyBoolean() 991 ) 992 ) 993 .thenReturn(EMPTY_CHANGES) 994 whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation()) 995 .thenReturn(Insets.NONE) 996 whenever(insetsProvider.currentRotationHasCornerCutout()).thenReturn(false) 997 setupCurrentInsets(null) 998 } 999 1000 private fun setupCurrentInsets(cutout: DisplayCutout?) { 1001 val mockedDisplay = 1002 mock<Display>().also { display -> whenever(display.cutout).thenReturn(cutout) } 1003 whenever(viewContext.display).thenReturn(mockedDisplay) 1004 } 1005 1006 private fun <T, U> Pair<T, U>.toAndroidPair(): android.util.Pair<T, U> { 1007 return android.util.Pair(first, second) 1008 } 1009 } 1010