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