<lambda>null1 package com.android.systemui.statusbar.notification.stack
2
3 import android.annotation.DimenRes
4 import android.content.pm.PackageManager
5 import android.platform.test.annotations.DisableFlags
6 import android.platform.test.annotations.EnableFlags
7 import android.widget.FrameLayout
8 import androidx.test.filters.SmallTest
9 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
10 import com.android.systemui.SysuiTestCase
11 import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
12 import com.android.systemui.dump.DumpManager
13 import com.android.systemui.flags.FeatureFlags
14 import com.android.systemui.flags.FeatureFlagsClassic
15 import com.android.systemui.res.R
16 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
17 import com.android.systemui.statusbar.EmptyShadeView
18 import com.android.systemui.statusbar.NotificationShelf
19 import com.android.systemui.statusbar.StatusBarState
20 import com.android.systemui.statusbar.notification.RoundableState
21 import com.android.systemui.statusbar.notification.collection.NotificationEntry
22 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
23 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView.FooterViewState
24 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
25 import com.android.systemui.statusbar.notification.row.ExpandableView
26 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation
27 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
28 import com.android.systemui.statusbar.policy.AvalancheController
29 import com.android.systemui.util.mockito.mock
30 import com.google.common.truth.Expect
31 import com.google.common.truth.Truth.assertThat
32 import junit.framework.Assert.assertEquals
33 import junit.framework.Assert.assertFalse
34 import junit.framework.Assert.assertTrue
35 import kotlinx.coroutines.ExperimentalCoroutinesApi
36 import org.junit.Assume
37 import org.junit.Before
38 import org.junit.Rule
39 import org.junit.Test
40 import org.mockito.Mockito.any
41 import org.mockito.Mockito.eq
42 import org.mockito.Mockito.mock
43 import org.mockito.Mockito.verify
44 import org.mockito.Mockito.`when` as whenever
45
46 @SmallTest
47 class StackScrollAlgorithmTest : SysuiTestCase() {
48
49 @JvmField @Rule var expect: Expect = Expect.create()
50
51 private val largeScreenShadeInterpolator = mock<LargeScreenShadeInterpolator>()
52 private val avalancheController = mock<AvalancheController>()
53
54 private val hostView = FrameLayout(context)
55 private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
56 private val notificationRow = mock<ExpandableNotificationRow>()
57 private val notificationEntry = mock<NotificationEntry>()
58 private val dumpManager = mock<DumpManager>()
59 @OptIn(ExperimentalCoroutinesApi::class)
60 private val mStatusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>()
61 private val notificationShelf = mock<NotificationShelf>()
62 private val emptyShadeView =
63 EmptyShadeView(context, /* attrs= */ null).apply {
64 layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
65 }
66 private val footerView = FooterView(context, /*attrs=*/ null)
67 @OptIn(ExperimentalCoroutinesApi::class)
68 private val ambientState =
69 AmbientState(
70 context,
71 dumpManager,
72 /* sectionProvider */ { _, _ -> false },
73 /* bypassController */ { false },
74 mStatusBarKeyguardViewManager,
75 largeScreenShadeInterpolator,
76 avalancheController
77 )
78
79 private val testableResources = mContext.getOrCreateTestableResources()
80 private val featureFlags = mock<FeatureFlagsClassic>()
81 private val maxPanelHeight =
82 mContext.resources.displayMetrics.heightPixels -
83 px(R.dimen.notification_panel_margin_top) -
84 px(R.dimen.notification_panel_margin_bottom)
85
86 private fun px(@DimenRes id: Int): Float =
87 testableResources.resources.getDimensionPixelSize(id).toFloat()
88
89 private val bigGap = px(R.dimen.notification_section_divider_height)
90 private val smallGap = px(R.dimen.notification_section_divider_height_lockscreen)
91 private val scrimPadding = px(R.dimen.notification_side_paddings)
92
93 @Before
94 fun setUp() {
95 Assume.assumeFalse(isTv())
96 mDependency.injectTestDependency(FeatureFlags::class.java, featureFlags)
97 whenever(notificationShelf.viewState).thenReturn(ExpandableViewState())
98 whenever(notificationRow.viewState).thenReturn(ExpandableViewState())
99 whenever(notificationRow.entry).thenReturn(notificationEntry)
100 whenever(notificationRow.roundableState)
101 .thenReturn(RoundableState(notificationRow, notificationRow, 0f))
102 ambientState.isSmallScreen = true
103
104 hostView.addView(notificationRow)
105 }
106
107 private fun isTv(): Boolean {
108 return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
109 }
110
111 @Test
112 fun resetViewStates_defaultHun_yTranslationIsInset() {
113 whenever(notificationRow.isPinned).thenReturn(true)
114 whenever(notificationRow.isHeadsUp).thenReturn(true)
115 resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
116 }
117
118 @Test
119 fun resetViewStates_defaultHunWithStackMargin_changesHunYTranslation() {
120 whenever(notificationRow.isPinned).thenReturn(true)
121 whenever(notificationRow.isHeadsUp).thenReturn(true)
122 resetViewStates_stackMargin_changesHunYTranslation()
123 }
124
125 @Test
126 fun resetViewStates_defaultHunWhenShadeIsOpening_yTranslationIsInset() {
127 whenever(notificationRow.isPinned).thenReturn(true)
128 whenever(notificationRow.isHeadsUp).thenReturn(true)
129
130 // scroll the panel over the HUN inset
131 ambientState.stackY = stackScrollAlgorithm.mHeadsUpInset + bigGap
132
133 // the HUN translation should be the panel scroll position + the scrim padding
134 resetViewStates_hunYTranslationIs(ambientState.stackY + scrimPadding)
135 }
136
137 @Test
138 @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
139 fun resetViewStates_hunAnimatingAway_yTranslationIsInset() {
140 whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
141 resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
142 }
143
144 @Test
145 @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
146 fun resetViewStates_hunAnimatingAway_StackMarginChangesHunYTranslation() {
147 whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
148 resetViewStates_stackMargin_changesHunYTranslation()
149 }
150
151 @Test
152 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
153 fun resetViewStates_defaultHun_newHeadsUpAnim_yTranslationIsInset() {
154 whenever(notificationRow.isPinned).thenReturn(true)
155 whenever(notificationRow.isHeadsUp).thenReturn(true)
156 resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
157 }
158
159 @Test
160 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
161 fun resetViewStates_defaultHunWithStackMargin_newHeadsUpAnim_changesHunYTranslation() {
162 whenever(notificationRow.isPinned).thenReturn(true)
163 whenever(notificationRow.isHeadsUp).thenReturn(true)
164 resetViewStates_stackMargin_changesHunYTranslation()
165 }
166
167 @Test
168 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
169 fun resetViewStates_defaultHun_showingQS_newHeadsUpAnim_hunTranslatedToMax() {
170 // Given: the shade is open and scrolled to the bottom to show the QuickSettings
171 val maxHunTranslation = 2000f
172 ambientState.maxHeadsUpTranslation = maxHunTranslation
173 ambientState.setLayoutMinHeight(2500) // Mock the height of shade
174 ambientState.stackY = 2500f // Scroll over the max translation
175 stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open
176 whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
177 whenever(notificationRow.isHeadsUp).thenReturn(true)
178 whenever(notificationRow.isAboveShelf).thenReturn(true)
179
180 resetViewStates_hunYTranslationIs(maxHunTranslation)
181 }
182
183 @Test
184 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
185 fun resetViewStates_hunAnimatingAway_showingQS_newHeadsUpAnim_hunTranslatedToBottomOfScreen() {
186 // Given: the shade is open and scrolled to the bottom to show the QuickSettings
187 val bottomOfScreen = 2600f
188 val maxHunTranslation = 2000f
189 ambientState.maxHeadsUpTranslation = maxHunTranslation
190 ambientState.setLayoutMinHeight(2500) // Mock the height of shade
191 ambientState.stackY = 2500f // Scroll over the max translation
192 stackScrollAlgorithm.setIsExpanded(true) // Mark the shade open
193 stackScrollAlgorithm.setHeadsUpAppearHeightBottom(bottomOfScreen.toInt())
194 whenever(notificationRow.mustStayOnScreen()).thenReturn(true)
195 whenever(notificationRow.isHeadsUp).thenReturn(true)
196 whenever(notificationRow.isAboveShelf).thenReturn(true)
197 whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
198
199 resetViewStates_hunYTranslationIs(
200 expected = bottomOfScreen + stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen
201 )
202 }
203
204 @Test
205 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
206 fun resetViewStates_hunAnimatingAway_newHeadsUpAnim_hunTranslatedToTopOfScreen() {
207 val topMargin = 100f
208 ambientState.maxHeadsUpTranslation = 2000f
209 ambientState.stackTopMargin = topMargin.toInt()
210 whenever(notificationRow.intrinsicHeight).thenReturn(100)
211 whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
212
213 resetViewStates_hunYTranslationIs(
214 expected = -topMargin - stackScrollAlgorithm.mHeadsUpAppearStartAboveScreen
215 )
216 }
217
218 @Test
219 fun resetViewStates_hunAnimatingAway_bottomNotClipped() {
220 whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
221
222 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
223
224 assertThat(notificationRow.viewState.clipBottomAmount).isEqualTo(0)
225 }
226
227 @Test
228 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
229 fun resetViewStates_hunAnimatingAwayWhileDozing_yTranslationIsInset() {
230 whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
231
232 ambientState.isDozing = true
233
234 resetViewStates_hunYTranslationIs(stackScrollAlgorithm.mHeadsUpInset)
235 }
236
237 @Test
238 @EnableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
239 fun resetViewStates_hunAnimatingAwayWhileDozing_hasStackMargin_changesHunYTranslation() {
240 whenever(notificationRow.isHeadsUpAnimatingAway).thenReturn(true)
241
242 ambientState.isDozing = true
243
244 resetViewStates_stackMargin_changesHunYTranslation()
245 }
246
247 @Test
248 fun resetViewStates_hunsOverlapping_bottomHunClipped() {
249 val topHun = mockExpandableNotificationRow()
250 val bottomHun = mockExpandableNotificationRow()
251 whenever(topHun.isHeadsUp).thenReturn(true)
252 whenever(topHun.isPinned).thenReturn(true)
253 whenever(bottomHun.isHeadsUp).thenReturn(true)
254 whenever(bottomHun.isPinned).thenReturn(true)
255
256 resetViewStates_hunsOverlapping_bottomHunClipped(topHun, bottomHun)
257 }
258
259 @Test
260 @DisableFlags(NotificationsImprovedHunAnimation.FLAG_NAME)
261 fun resetViewStates_hunsOverlappingAndBottomHunAnimatingAway_bottomHunClipped() {
262 val topHun = mockExpandableNotificationRow()
263 val bottomHun = mockExpandableNotificationRow()
264 whenever(topHun.isHeadsUp).thenReturn(true)
265 whenever(topHun.isPinned).thenReturn(true)
266 whenever(bottomHun.isHeadsUpAnimatingAway).thenReturn(true)
267
268 resetViewStates_hunsOverlapping_bottomHunClipped(topHun, bottomHun)
269 }
270
271 @Test
272 fun resetViewStates_emptyShadeView_isCenteredVertically() {
273 stackScrollAlgorithm.initView(context)
274 hostView.removeAllViews()
275 hostView.addView(emptyShadeView)
276 ambientState.layoutMaxHeight = maxPanelHeight.toInt()
277
278 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
279
280 val marginBottom =
281 context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
282 val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY
283 val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f
284 assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY)
285 }
286
287 @Test
288 fun resetViewStates_hunGoingToShade_viewBecomesOpaque() {
289 whenever(notificationRow.isAboveShelf).thenReturn(true)
290 ambientState.isShadeExpanded = true
291 ambientState.trackedHeadsUpRow = notificationRow
292 stackScrollAlgorithm.initView(context)
293
294 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
295
296 assertThat(notificationRow.viewState.alpha).isEqualTo(1f)
297 }
298
299 @OptIn(ExperimentalCoroutinesApi::class)
300 @Test
301 fun resetViewStates_expansionChanging_notificationBecomesTransparent() {
302 whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
303 resetViewStates_expansionChanging_notificationAlphaUpdated(
304 expansionFraction = 0.25f,
305 expectedAlpha = 0.0f
306 )
307 }
308
309 @OptIn(ExperimentalCoroutinesApi::class)
310 @Test
311 fun resetViewStates_expansionChangingWhileBouncerInTransit_viewBecomesTransparent() {
312 whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
313 resetViewStates_expansionChanging_notificationAlphaUpdated(
314 expansionFraction = 0.85f,
315 expectedAlpha = 0.0f
316 )
317 }
318
319 @OptIn(ExperimentalCoroutinesApi::class)
320 @Test
321 fun resetViewStates_expansionChanging_notificationAlphaUpdated() {
322 whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
323 resetViewStates_expansionChanging_notificationAlphaUpdated(
324 expansionFraction = 0.6f,
325 expectedAlpha = getContentAlpha(0.6f)
326 )
327 }
328
329 @OptIn(ExperimentalCoroutinesApi::class)
330 @Test
331 fun resetViewStates_largeScreen_expansionChanging_alphaUpdated_largeScreenValue() {
332 val expansionFraction = 0.6f
333 val surfaceAlpha = 123f
334 ambientState.isSmallScreen = false
335 whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(false)
336 whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction))
337 .thenReturn(surfaceAlpha)
338
339 resetViewStates_expansionChanging_notificationAlphaUpdated(
340 expansionFraction = expansionFraction,
341 expectedAlpha = surfaceAlpha,
342 )
343 }
344
345 @OptIn(ExperimentalCoroutinesApi::class)
346 @Test
347 fun expansionChanging_largeScreen_bouncerInTransit_alphaUpdated_bouncerValues() {
348 ambientState.isSmallScreen = false
349 whenever(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit).thenReturn(true)
350 resetViewStates_expansionChanging_notificationAlphaUpdated(
351 expansionFraction = 0.95f,
352 expectedAlpha = aboutToShowBouncerProgress(0.95f),
353 )
354 }
355
356 @Test
357 fun resetViewStates_expansionChanging_shelfUpdated() {
358 ambientState.shelf = notificationShelf
359 ambientState.isExpansionChanging = true
360 ambientState.expansionFraction = 0.6f
361 stackScrollAlgorithm.initView(context)
362
363 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
364
365 verify(notificationShelf)
366 .updateState(/* algorithmState= */ any(), /* ambientState= */ eq(ambientState))
367 }
368
369 @Test
370 fun resetViewStates_isOnKeyguard_viewBecomesTransparent() {
371 ambientState.setStatusBarState(StatusBarState.KEYGUARD)
372 ambientState.hideAmount = 0.25f
373 whenever(notificationRow.isHeadsUpState).thenReturn(true)
374
375 stackScrollAlgorithm.initView(context)
376
377 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
378
379 assertThat(notificationRow.viewState.alpha).isEqualTo(1f - ambientState.hideAmount)
380 }
381
382 @Test
383 fun resetViewStates_isOnKeyguard_emptyShadeViewBecomesTransparent() {
384 ambientState.setStatusBarState(StatusBarState.KEYGUARD)
385 ambientState.fractionToShade = 0.25f
386 stackScrollAlgorithm.initView(context)
387 hostView.removeAllViews()
388 hostView.addView(emptyShadeView)
389
390 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
391
392 val expected = getContentAlpha(ambientState.fractionToShade)
393 assertThat(emptyShadeView.viewState.alpha).isEqualTo(expected)
394 }
395
396 @Test
397 fun resetViewStates_shadeCollapsed_emptyShadeViewBecomesTransparent() {
398 ambientState.expansionFraction = 0f
399 stackScrollAlgorithm.initView(context)
400 hostView.removeAllViews()
401 hostView.addView(emptyShadeView)
402
403 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
404
405 assertThat(emptyShadeView.viewState.alpha).isEqualTo(0f)
406 }
407
408 @Test
409 fun resetViewStates_isOnKeyguard_emptyShadeViewBecomesOpaque() {
410 ambientState.setStatusBarState(StatusBarState.KEYGUARD)
411 ambientState.fractionToShade = 0.25f
412 stackScrollAlgorithm.initView(context)
413 hostView.removeAllViews()
414 hostView.addView(emptyShadeView)
415
416 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
417
418 val expected = getContentAlpha(ambientState.fractionToShade)
419 assertThat(emptyShadeView.viewState.alpha).isEqualTo(expected)
420 }
421
422 @Test
423 fun resetViewStates_hiddenShelf_allRowsBecomesTransparent() {
424 hostView.removeAllViews()
425 val row1 = mockExpandableNotificationRow()
426 hostView.addView(row1)
427 val row2 = mockExpandableNotificationRow()
428 hostView.addView(row2)
429
430 whenever(row1.isHeadsUpState).thenReturn(true)
431 whenever(row2.isHeadsUpState).thenReturn(false)
432
433 ambientState.setStatusBarState(StatusBarState.KEYGUARD)
434 ambientState.hideAmount = 0.25f
435 ambientState.dozeAmount = 0.33f
436 notificationShelf.viewState.hidden = true
437 ambientState.shelf = notificationShelf
438 stackScrollAlgorithm.initView(context)
439
440 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
441
442 assertThat(row1.viewState.alpha).isEqualTo(1f - ambientState.hideAmount)
443 assertThat(row2.viewState.alpha).isEqualTo(1f - ambientState.dozeAmount)
444 }
445
446 @Test
447 fun resetViewStates_hiddenShelf_shelfAlphaDoesNotChange() {
448 val expected = notificationShelf.viewState.alpha
449 notificationShelf.viewState.hidden = true
450 ambientState.shelf = notificationShelf
451 stackScrollAlgorithm.initView(context)
452
453 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
454
455 assertThat(notificationShelf.viewState.alpha).isEqualTo(expected)
456 }
457
458 @Test
459 fun resetViewStates_shelfTopLessThanViewTop_hidesView() {
460 notificationRow.viewState.yTranslation = 10f
461 notificationShelf.viewState.yTranslation = 0.9f
462 notificationShelf.viewState.hidden = false
463 ambientState.shelf = notificationShelf
464 stackScrollAlgorithm.initView(context)
465
466 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
467
468 assertThat(notificationRow.viewState.alpha).isEqualTo(0f)
469 }
470
471 @Test
472 fun resetViewStates_shelfTopGreaterOrEqualThanViewTop_viewAlphaDoesNotChange() {
473 val expected = notificationRow.viewState.alpha
474 notificationRow.viewState.yTranslation = 10f
475 notificationShelf.viewState.yTranslation = 10f
476 notificationShelf.viewState.hidden = false
477 ambientState.shelf = notificationShelf
478 stackScrollAlgorithm.initView(context)
479
480 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
481
482 assertThat(notificationRow.viewState.alpha).isEqualTo(expected)
483 }
484
485 @Test
486 fun resetViewStates_noSpaceForFooter_footerHidden() {
487 ambientState.isShadeExpanded = true
488 ambientState.stackEndHeight = 0f // no space for the footer in the stack
489 hostView.addView(footerView)
490
491 stackScrollAlgorithm.resetViewStates(ambientState, 0)
492
493 assertThat((footerView.viewState as FooterViewState).hideContent).isTrue()
494 }
495
496 @Test
497 fun resetViewStates_clearAllInProgress_hasNonClearableRow_footerVisible() {
498 whenever(notificationRow.canViewBeCleared()).thenReturn(false)
499 ambientState.isClearAllInProgress = true
500 ambientState.isShadeExpanded = true
501 ambientState.stackEndHeight = maxPanelHeight // plenty space for the footer in the stack
502 hostView.addView(footerView)
503
504 stackScrollAlgorithm.resetViewStates(ambientState, 0)
505
506 assertThat(footerView.viewState.hidden).isFalse()
507 assertThat((footerView.viewState as FooterViewState).hideContent).isFalse()
508 }
509
510 @Test
511 fun resetViewStates_clearAllInProgress_allRowsClearable_footerHidden() {
512 whenever(notificationRow.canViewBeCleared()).thenReturn(true)
513 ambientState.isClearAllInProgress = true
514 ambientState.isShadeExpanded = true
515 ambientState.stackEndHeight = maxPanelHeight // plenty space for the footer in the stack
516 hostView.addView(footerView)
517
518 stackScrollAlgorithm.resetViewStates(ambientState, 0)
519
520 assertThat((footerView.viewState as FooterViewState).hideContent).isTrue()
521 }
522
523 @Test
524 fun resetViewStates_clearAllInProgress_allRowsRemoved_emptyShade_footerHidden() {
525 ambientState.isClearAllInProgress = true
526 ambientState.isShadeExpanded = true
527 ambientState.stackEndHeight = maxPanelHeight // plenty space for the footer in the stack
528 hostView.removeAllViews() // remove all rows
529 hostView.addView(footerView)
530
531 stackScrollAlgorithm.resetViewStates(ambientState, 0)
532
533 assertThat((footerView.viewState as FooterViewState).hideContent).isTrue()
534 }
535
536 @Test
537 fun getGapForLocation_onLockscreen_returnsSmallGap() {
538 val gap =
539 stackScrollAlgorithm.getGapForLocation(
540 /* fractionToShade= */ 0f,
541 /* onKeyguard= */ true
542 )
543 assertThat(gap).isEqualTo(smallGap)
544 }
545
546 @Test
547 fun getGapForLocation_goingToShade_interpolatesGap() {
548 val gap =
549 stackScrollAlgorithm.getGapForLocation(
550 /* fractionToShade= */ 0.5f,
551 /* onKeyguard= */ true
552 )
553 assertThat(gap).isEqualTo(smallGap * 0.5f + bigGap * 0.5f)
554 }
555
556 @Test
557 fun getGapForLocation_notOnLockscreen_returnsBigGap() {
558 val gap =
559 stackScrollAlgorithm.getGapForLocation(
560 /* fractionToShade= */ 0f,
561 /* onKeyguard= */ false
562 )
563 assertThat(gap).isEqualTo(bigGap)
564 }
565
566 @Test
567 fun updateViewWithShelf_viewAboveShelf_viewShown() {
568 val viewStart = 0f
569 val shelfStart = 1f
570
571 val expandableView = mock(ExpandableView::class.java)
572 whenever(expandableView.isExpandAnimationRunning).thenReturn(false)
573 whenever(expandableView.hasExpandingChild()).thenReturn(false)
574
575 val expandableViewState = ExpandableViewState()
576 expandableViewState.yTranslation = viewStart
577
578 stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
579 assertFalse(expandableViewState.hidden)
580 }
581
582 @Test
583 fun updateViewWithShelf_viewBelowShelf_viewHidden() {
584 val shelfStart = 0f
585 val viewStart = 1f
586
587 val expandableView = mock(ExpandableView::class.java)
588 whenever(expandableView.isExpandAnimationRunning).thenReturn(false)
589 whenever(expandableView.hasExpandingChild()).thenReturn(false)
590
591 val expandableViewState = ExpandableViewState()
592 expandableViewState.yTranslation = viewStart
593
594 stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
595 assertTrue(expandableViewState.hidden)
596 }
597
598 @Test
599 fun updateViewWithShelf_viewBelowShelfButIsExpanding_viewShown() {
600 val shelfStart = 0f
601 val viewStart = 1f
602
603 val expandableView = mock(ExpandableView::class.java)
604 whenever(expandableView.isExpandAnimationRunning).thenReturn(true)
605 whenever(expandableView.hasExpandingChild()).thenReturn(true)
606
607 val expandableViewState = ExpandableViewState()
608 expandableViewState.yTranslation = viewStart
609
610 stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
611 assertFalse(expandableViewState.hidden)
612 }
613
614 @Test
615 fun maybeUpdateHeadsUpIsVisible_endVisible_true() {
616 val expandableViewState = ExpandableViewState()
617 expandableViewState.headsUpIsVisible = false
618
619 stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
620 expandableViewState,
621 /* isShadeExpanded= */ true,
622 /* mustStayOnScreen= */ true,
623 /* isViewEndVisible= */ true,
624 /* viewEnd= */ 0f,
625 /* maxHunY= */ 10f
626 )
627
628 assertTrue(expandableViewState.headsUpIsVisible)
629 }
630
631 @Test
632 fun maybeUpdateHeadsUpIsVisible_endHidden_false() {
633 val expandableViewState = ExpandableViewState()
634 expandableViewState.headsUpIsVisible = true
635
636 stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
637 expandableViewState,
638 /* isShadeExpanded= */ true,
639 /* mustStayOnScreen= */ true,
640 /* isViewEndVisible= */ true,
641 /* viewEnd= */ 10f,
642 /* maxHunY= */ 0f
643 )
644
645 assertFalse(expandableViewState.headsUpIsVisible)
646 }
647
648 @Test
649 fun maybeUpdateHeadsUpIsVisible_shadeClosed_noUpdate() {
650 val expandableViewState = ExpandableViewState()
651 expandableViewState.headsUpIsVisible = true
652
653 stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
654 expandableViewState,
655 /* isShadeExpanded= */ false,
656 /* mustStayOnScreen= */ true,
657 /* isViewEndVisible= */ true,
658 /* viewEnd= */ 10f,
659 /* maxHunY= */ 1f
660 )
661
662 assertTrue(expandableViewState.headsUpIsVisible)
663 }
664
665 @Test
666 fun maybeUpdateHeadsUpIsVisible_notHUN_noUpdate() {
667 val expandableViewState = ExpandableViewState()
668 expandableViewState.headsUpIsVisible = true
669
670 stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
671 expandableViewState,
672 /* isShadeExpanded= */ true,
673 /* mustStayOnScreen= */ false,
674 /* isViewEndVisible= */ true,
675 /* viewEnd= */ 10f,
676 /* maxHunY= */ 1f
677 )
678
679 assertTrue(expandableViewState.headsUpIsVisible)
680 }
681
682 @Test
683 fun maybeUpdateHeadsUpIsVisible_topHidden_noUpdate() {
684 val expandableViewState = ExpandableViewState()
685 expandableViewState.headsUpIsVisible = true
686
687 stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(
688 expandableViewState,
689 /* isShadeExpanded= */ true,
690 /* mustStayOnScreen= */ true,
691 /* isViewEndVisible= */ false,
692 /* viewEnd= */ 10f,
693 /* maxHunY= */ 1f
694 )
695
696 assertTrue(expandableViewState.headsUpIsVisible)
697 }
698
699 @Test
700 fun clampHunToTop_viewYGreaterThanQqs_viewYUnchanged() {
701 val expandableViewState = ExpandableViewState()
702 expandableViewState.yTranslation = 50f
703
704 stackScrollAlgorithm.clampHunToTop(
705 /* quickQsOffsetHeight= */ 10f,
706 /* stackTranslation= */ 0f,
707 /* collapsedHeight= */ 1f,
708 expandableViewState
709 )
710
711 // qqs (10 + 0) < viewY (50)
712 assertEquals(50f, expandableViewState.yTranslation)
713 }
714
715 @Test
716 fun clampHunToTop_viewYLessThanQqs_viewYChanged() {
717 val expandableViewState = ExpandableViewState()
718 expandableViewState.yTranslation = -10f
719
720 stackScrollAlgorithm.clampHunToTop(
721 /* quickQsOffsetHeight= */ 10f,
722 /* stackTranslation= */ 0f,
723 /* collapsedHeight= */ 1f,
724 expandableViewState
725 )
726
727 // qqs (10 + 0) > viewY (-10)
728 assertEquals(10f, expandableViewState.yTranslation)
729 }
730
731 @Test
732 fun clampHunToTop_viewYFarAboveVisibleStack_heightCollapsed() {
733 val expandableViewState = ExpandableViewState()
734 expandableViewState.height = 20
735 expandableViewState.yTranslation = -100f
736
737 stackScrollAlgorithm.clampHunToTop(
738 /* quickQsOffsetHeight= */ 10f,
739 /* stackTranslation= */ 0f,
740 /* collapsedHeight= */ 10f,
741 expandableViewState
742 )
743
744 // newTranslation = max(10, -100) = 10
745 // distToRealY = 10 - (-100f) = 110
746 // height = max(20 - 110, 10f)
747 assertEquals(10, expandableViewState.height)
748 }
749
750 @Test
751 fun clampHunToTop_viewYNearVisibleStack_heightTallerThanCollapsed() {
752 val expandableViewState = ExpandableViewState()
753 expandableViewState.height = 20
754 expandableViewState.yTranslation = 5f
755
756 stackScrollAlgorithm.clampHunToTop(
757 /* quickQsOffsetHeight= */ 10f,
758 /* stackTranslation= */ 0f,
759 /* collapsedHeight= */ 10f,
760 expandableViewState
761 )
762
763 // newTranslation = max(10, 5) = 10
764 // distToRealY = 10 - 5 = 5
765 // height = max(20 - 5, 10) = 15
766 assertEquals(15, expandableViewState.height)
767 }
768
769 @Test
770 fun computeCornerRoundnessForPinnedHun_stackBelowScreen_round() {
771 val currentRoundness =
772 stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
773 /* hostViewHeight= */ 100f,
774 /* stackY= */ 110f,
775 /* viewMaxHeight= */ 20f,
776 /* originalCornerRoundness= */ 0f
777 )
778 assertEquals(1f, currentRoundness)
779 }
780
781 @Test
782 fun computeCornerRoundnessForPinnedHun_stackAboveScreenBelowPinPoint_halfRound() {
783 val currentRoundness =
784 stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
785 /* hostViewHeight= */ 100f,
786 /* stackY= */ 90f,
787 /* viewMaxHeight= */ 20f,
788 /* originalCornerRoundness= */ 0f
789 )
790 assertEquals(0.5f, currentRoundness)
791 }
792
793 @Test
794 fun computeCornerRoundnessForPinnedHun_stackAbovePinPoint_notRound() {
795 val currentRoundness =
796 stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
797 /* hostViewHeight= */ 100f,
798 /* stackY= */ 0f,
799 /* viewMaxHeight= */ 20f,
800 /* originalCornerRoundness= */ 0f
801 )
802 assertEquals(0f, currentRoundness)
803 }
804
805 @Test
806 fun computeCornerRoundnessForPinnedHun_originallyRoundAndStackAbovePinPoint_round() {
807 val currentRoundness =
808 stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
809 /* hostViewHeight= */ 100f,
810 /* stackY= */ 0f,
811 /* viewMaxHeight= */ 20f,
812 /* originalCornerRoundness= */ 1f
813 )
814 assertEquals(1f, currentRoundness)
815 }
816
817 @Test
818 fun shadeOpened_hunFullyOverlapsQqsPanel_hunShouldHaveFullShadow() {
819 // Given: shade is opened, yTranslation of HUN is 0,
820 // the height of HUN equals to the height of QQS Panel,
821 // and HUN fully overlaps with QQS Panel
822 ambientState.stackTranslation =
823 px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
824 val childHunView =
825 createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f)
826 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
827 algorithmState.visibleChildren.add(childHunView)
828
829 // When: updateChildZValue() is called for the top HUN
830 stackScrollAlgorithm.updateChildZValue(
831 /* i= */ 0,
832 /* childrenOnTop= */ 0.0f,
833 /* StackScrollAlgorithmState= */ algorithmState,
834 /* ambientState= */ ambientState,
835 /* shouldElevateHun= */ true
836 )
837
838 // Then: full shadow would be applied
839 assertEquals(px(R.dimen.heads_up_pinned_elevation), childHunView.viewState.zTranslation)
840 }
841
842 @Test
843 fun shadeOpened_hunPartiallyOverlapsQQS_hunShouldHavePartialShadow() {
844 // Given: shade is opened, yTranslation of HUN is greater than 0,
845 // the height of HUN is equal to the height of QQS Panel,
846 // and HUN partially overlaps with QQS Panel
847 ambientState.stackTranslation =
848 px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
849 val childHunView =
850 createHunViewMock(isShadeOpen = true, fullyVisible = false, headerVisibleAmount = 1f)
851 // Use half of the HUN's height as overlap
852 childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat()
853 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
854 algorithmState.visibleChildren.add(childHunView)
855
856 // When: updateChildZValue() is called for the top HUN
857 stackScrollAlgorithm.updateChildZValue(
858 /* i= */ 0,
859 /* childrenOnTop= */ 0.0f,
860 /* StackScrollAlgorithmState= */ algorithmState,
861 /* ambientState= */ ambientState,
862 /* shouldElevateHun= */ true
863 )
864
865 // Then: HUN should have shadow, but not as full size
866 assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f)
867 assertThat(childHunView.viewState.zTranslation)
868 .isLessThan(px(R.dimen.heads_up_pinned_elevation))
869 }
870
871 @Test
872 fun shadeOpened_hunDoesNotOverlapQQS_hunShouldHaveNoShadow() {
873 // Given: shade is opened, yTranslation of HUN is equal to QQS Panel's height,
874 // the height of HUN is equal to the height of QQS Panel,
875 // and HUN doesn't overlap with QQS Panel
876 ambientState.stackTranslation =
877 px(R.dimen.qqs_layout_margin_top) + px(R.dimen.qqs_layout_padding_bottom)
878 // Mock the height of shade
879 ambientState.setLayoutMinHeight(1000)
880 val childHunView =
881 createHunViewMock(isShadeOpen = true, fullyVisible = true, headerVisibleAmount = 1f)
882 // HUN doesn't overlap with QQS Panel
883 childHunView.viewState.yTranslation =
884 ambientState.topPadding + ambientState.stackTranslation
885 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
886 algorithmState.visibleChildren.add(childHunView)
887
888 // When: updateChildZValue() is called for the top HUN
889 stackScrollAlgorithm.updateChildZValue(
890 /* i= */ 0,
891 /* childrenOnTop= */ 0.0f,
892 /* StackScrollAlgorithmState= */ algorithmState,
893 /* ambientState= */ ambientState,
894 /* shouldElevateHun= */ true
895 )
896
897 // Then: HUN should not have shadow
898 assertEquals(0f, childHunView.viewState.zTranslation)
899 }
900
901 @Test
902 fun shadeClosed_hunShouldHaveFullShadow() {
903 // Given: shade is closed, ambientState.stackTranslation == -ambientState.topPadding,
904 // the height of HUN is equal to the height of QQS Panel,
905 ambientState.stackTranslation = (-ambientState.topPadding).toFloat()
906 // Mock the height of shade
907 ambientState.setLayoutMinHeight(1000)
908 val childHunView =
909 createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0f)
910 childHunView.viewState.yTranslation = 0f
911 // Shade is closed, thus childHunView's headerVisibleAmount is 0
912 childHunView.headerVisibleAmount = 0f
913 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
914 algorithmState.visibleChildren.add(childHunView)
915
916 // When: updateChildZValue() is called for the top HUN
917 stackScrollAlgorithm.updateChildZValue(
918 /* i= */ 0,
919 /* childrenOnTop= */ 0.0f,
920 /* StackScrollAlgorithmState= */ algorithmState,
921 /* ambientState= */ ambientState,
922 /* shouldElevateHun= */ true
923 )
924
925 // Then: HUN should have full shadow
926 assertEquals(px(R.dimen.heads_up_pinned_elevation), childHunView.viewState.zTranslation)
927 }
928
929 @Test
930 fun draggingHunToOpenShade_hunShouldHavePartialShadow() {
931 // Given: shade is closed when HUN pops up,
932 // now drags down the HUN to open shade
933 ambientState.stackTranslation = (-ambientState.topPadding).toFloat()
934 // Mock the height of shade
935 ambientState.setLayoutMinHeight(1000)
936 val childHunView =
937 createHunViewMock(isShadeOpen = false, fullyVisible = false, headerVisibleAmount = 0.5f)
938 childHunView.viewState.yTranslation = 0f
939 // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1
940 // use 0.5 as headerVisibleAmount here
941 childHunView.headerVisibleAmount = 0.5f
942 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
943 algorithmState.visibleChildren.add(childHunView)
944
945 // When: updateChildZValue() is called for the top HUN
946 stackScrollAlgorithm.updateChildZValue(
947 /* i= */ 0,
948 /* childrenOnTop= */ 0.0f,
949 /* StackScrollAlgorithmState= */ algorithmState,
950 /* ambientState= */ ambientState,
951 /* shouldElevateHun= */ true
952 )
953
954 // Then: HUN should have shadow, but not as full size
955 assertThat(childHunView.viewState.zTranslation).isGreaterThan(0.0f)
956 assertThat(childHunView.viewState.zTranslation)
957 .isLessThan(px(R.dimen.heads_up_pinned_elevation))
958 }
959
960 @Test
961 fun aodToLockScreen_hasPulsingNotification_pulsingNotificationRowDoesNotChange() {
962 // Given: Before AOD to LockScreen, there was a pulsing notification
963 val pulsingNotificationView = createPulsingViewMock()
964 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
965 algorithmState.visibleChildren.add(pulsingNotificationView)
966 ambientState.setPulsingRow(pulsingNotificationView)
967
968 // When: during AOD to LockScreen, any dozeAmount between (0, 1.0) is equivalent as a middle
969 // stage; here we use 0.5 for testing.
970 // stackScrollAlgorithm.updatePulsingStates is called
971 ambientState.dozeAmount = 0.5f
972 stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState)
973
974 // Then: ambientState.pulsingRow should still be pulsingNotificationView
975 assertTrue(ambientState.isPulsingRow(pulsingNotificationView))
976 }
977
978 @Test
979 fun deviceOnAod_hasPulsingNotification_recordPulsingNotificationRow() {
980 // Given: Device is on AOD, there is a pulsing notification
981 // ambientState.pulsingRow is null before stackScrollAlgorithm.updatePulsingStates
982 ambientState.dozeAmount = 1.0f
983 val pulsingNotificationView = createPulsingViewMock()
984 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
985 algorithmState.visibleChildren.add(pulsingNotificationView)
986 ambientState.setPulsingRow(null)
987
988 // When: stackScrollAlgorithm.updatePulsingStates is called
989 stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState)
990
991 // Then: ambientState.pulsingRow should record the pulsingNotificationView
992 assertTrue(ambientState.isPulsingRow(pulsingNotificationView))
993 }
994
995 @Test
996 fun deviceOnLockScreen_hasPulsingNotificationBefore_clearPulsingNotificationRowRecord() {
997 // Given: Device finished AOD to LockScreen, there was a pulsing notification, and
998 // ambientState.pulsingRow was not null before AOD to LockScreen
999 // pulsingNotificationView.showingPulsing() returns false since the device is on LockScreen
1000 ambientState.dozeAmount = 0.0f
1001 val pulsingNotificationView = createPulsingViewMock()
1002 whenever(pulsingNotificationView.showingPulsing()).thenReturn(false)
1003 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
1004 algorithmState.visibleChildren.add(pulsingNotificationView)
1005 ambientState.setPulsingRow(pulsingNotificationView)
1006
1007 // When: stackScrollAlgorithm.updatePulsingStates is called
1008 stackScrollAlgorithm.updatePulsingStates(algorithmState, ambientState)
1009
1010 // Then: ambientState.pulsingRow should be null
1011 assertTrue(ambientState.isPulsingRow(null))
1012 }
1013
1014 @Test
1015 fun aodToLockScreen_hasPulsingNotification_pulsingNotificationRowShowAtFullHeight() {
1016 // Given: Before AOD to LockScreen, there was a pulsing notification
1017 val pulsingNotificationView = createPulsingViewMock()
1018 val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
1019 algorithmState.visibleChildren.add(pulsingNotificationView)
1020 ambientState.setPulsingRow(pulsingNotificationView)
1021
1022 // When: during AOD to LockScreen, any dozeAmount between (0, 1.0) is equivalent as a middle
1023 // stage; here we use 0.5 for testing. The expansionFraction is also 0.5.
1024 // stackScrollAlgorithm.resetViewStates is called.
1025 ambientState.dozeAmount = 0.5f
1026 setExpansionFractionWithoutShelfDuringAodToLockScreen(
1027 ambientState,
1028 algorithmState,
1029 fraction = 0.5f
1030 )
1031 stackScrollAlgorithm.resetViewStates(ambientState, 0)
1032
1033 // Then: pulsingNotificationView should show at full height
1034 assertEquals(
1035 stackScrollAlgorithm.getMaxAllowedChildHeight(pulsingNotificationView),
1036 pulsingNotificationView.viewState.height
1037 )
1038
1039 // After: reset dozeAmount and expansionFraction
1040 ambientState.dozeAmount = 0f
1041 setExpansionFractionWithoutShelfDuringAodToLockScreen(
1042 ambientState,
1043 algorithmState,
1044 fraction = 1f
1045 )
1046 }
1047
1048 // region shouldPinHunToBottomOfExpandedQs
1049 @Test
1050 fun shouldHunBeVisibleWhenScrolled_mustStayOnScreenFalse_false() {
1051 assertThat(
1052 stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
1053 /* mustStayOnScreen= */ false,
1054 /* headsUpIsVisible= */ false,
1055 /* showingPulsing= */ false,
1056 /* isOnKeyguard=*/ false,
1057 /*headsUpOnKeyguard=*/ false
1058 )
1059 )
1060 .isFalse()
1061 }
1062
1063 @Test
1064 fun shouldPinHunToBottomOfExpandedQs_headsUpIsVisible_false() {
1065 assertThat(
1066 stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
1067 /* mustStayOnScreen= */ true,
1068 /* headsUpIsVisible= */ true,
1069 /* showingPulsing= */ false,
1070 /* isOnKeyguard=*/ false,
1071 /*headsUpOnKeyguard=*/ false
1072 )
1073 )
1074 .isFalse()
1075 }
1076
1077 @Test
1078 fun shouldHunBeVisibleWhenScrolled_showingPulsing_false() {
1079 assertThat(
1080 stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
1081 /* mustStayOnScreen= */ true,
1082 /* headsUpIsVisible= */ false,
1083 /* showingPulsing= */ true,
1084 /* isOnKeyguard=*/ false,
1085 /* headsUpOnKeyguard= */ false
1086 )
1087 )
1088 .isFalse()
1089 }
1090
1091 @Test
1092 fun shouldHunBeVisibleWhenScrolled_isOnKeyguard_false() {
1093 assertThat(
1094 stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
1095 /* mustStayOnScreen= */ true,
1096 /* headsUpIsVisible= */ false,
1097 /* showingPulsing= */ false,
1098 /* isOnKeyguard=*/ true,
1099 /* headsUpOnKeyguard= */ false
1100 )
1101 )
1102 .isFalse()
1103 }
1104
1105 @Test
1106 fun shouldHunBeVisibleWhenScrolled_isNotOnKeyguard_true() {
1107 assertThat(
1108 stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
1109 /* mustStayOnScreen= */ true,
1110 /* headsUpIsVisible= */ false,
1111 /* showingPulsing= */ false,
1112 /* isOnKeyguard=*/ false,
1113 /* headsUpOnKeyguard= */ false
1114 )
1115 )
1116 .isTrue()
1117 }
1118
1119 @Test
1120 fun shouldHunBeVisibleWhenScrolled_headsUpOnKeyguard_true() {
1121 assertThat(
1122 stackScrollAlgorithm.shouldHunBeVisibleWhenScrolled(
1123 /* mustStayOnScreen= */ true,
1124 /* headsUpIsVisible= */ false,
1125 /* showingPulsing= */ false,
1126 /* isOnKeyguard=*/ true,
1127 /* headsUpOnKeyguard= */ true
1128 )
1129 )
1130 .isTrue()
1131 }
1132
1133 @Test
1134 fun shouldHunAppearFromBottom_hunAtMaxHunTranslation() {
1135 ambientState.maxHeadsUpTranslation = 400f
1136 val viewState =
1137 ExpandableViewState().apply {
1138 height = 100
1139 yTranslation = ambientState.maxHeadsUpTranslation - height // move it to the max
1140 }
1141
1142 assertTrue(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState))
1143 }
1144
1145 @Test
1146 fun shouldHunAppearFromBottom_hunBelowMaxHunTranslation() {
1147 ambientState.maxHeadsUpTranslation = 400f
1148 val viewState =
1149 ExpandableViewState().apply {
1150 height = 100
1151 yTranslation =
1152 ambientState.maxHeadsUpTranslation - height - 1 // move it below the max
1153 }
1154
1155 assertFalse(stackScrollAlgorithm.shouldHunAppearFromBottom(ambientState, viewState))
1156 }
1157 // endregion
1158
1159 private fun createHunViewMock(
1160 isShadeOpen: Boolean,
1161 fullyVisible: Boolean,
1162 headerVisibleAmount: Float
1163 ) =
1164 mock<ExpandableNotificationRow>().apply {
1165 val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
1166 whenever(this.viewState).thenReturn(childViewStateMock)
1167
1168 whenever(this.mustStayOnScreen()).thenReturn(true)
1169 whenever(this.headerVisibleAmount).thenReturn(headerVisibleAmount)
1170 }
1171
1172 private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) =
1173 ExpandableViewState().apply {
1174 // Mock the HUN's height with ambientState.topPadding +
1175 // ambientState.stackTranslation
1176 height = (ambientState.topPadding + ambientState.stackTranslation).toInt()
1177 if (isShadeOpen && fullyVisible) {
1178 yTranslation = ambientState.topPadding + ambientState.stackTranslation
1179 } else {
1180 yTranslation = 0f
1181 }
1182 headsUpIsVisible = fullyVisible
1183 }
1184
1185 private fun createPulsingViewMock() =
1186 mock<ExpandableNotificationRow>().apply {
1187 whenever(this.viewState).thenReturn(ExpandableViewState())
1188 whenever(this.showingPulsing()).thenReturn(true)
1189 }
1190
1191 private fun setExpansionFractionWithoutShelfDuringAodToLockScreen(
1192 ambientState: AmbientState,
1193 algorithmState: StackScrollAlgorithm.StackScrollAlgorithmState,
1194 fraction: Float
1195 ) {
1196 // showingShelf: false
1197 algorithmState.firstViewInShelf = null
1198 // scrimPadding: 0, because device is on lock screen
1199 ambientState.setStatusBarState(StatusBarState.KEYGUARD)
1200 ambientState.dozeAmount = 0.0f
1201 // set stackEndHeight and stackHeight
1202 // ExpansionFractionWithoutShelf == stackHeight / stackEndHeight
1203 ambientState.stackEndHeight = 100f
1204 ambientState.stackHeight = ambientState.stackEndHeight * fraction
1205 }
1206
1207 private fun resetViewStates_hunYTranslationIs(expected: Float) {
1208 stackScrollAlgorithm.resetViewStates(ambientState, 0)
1209
1210 assertThat(notificationRow.viewState.yTranslation).isEqualTo(expected)
1211 }
1212
1213 private fun resetViewStates_stackMargin_changesHunYTranslation() {
1214 val stackTopMargin = bigGap.toInt() // a gap smaller than the headsUpInset
1215 val headsUpTranslationY = stackScrollAlgorithm.mHeadsUpInset - stackTopMargin
1216
1217 // we need the shelf to mock the real-life behaviour of StackScrollAlgorithm#updateChild
1218 ambientState.shelf = notificationShelf
1219
1220 // split shade case with top margin introduced by shade's status bar
1221 ambientState.stackTopMargin = stackTopMargin
1222 stackScrollAlgorithm.resetViewStates(ambientState, 0)
1223
1224 // heads up translation should be decreased by the top margin
1225 assertThat(notificationRow.viewState.yTranslation).isEqualTo(headsUpTranslationY)
1226 }
1227
1228 private fun resetViewStates_hunsOverlapping_bottomHunClipped(
1229 topHun: ExpandableNotificationRow,
1230 bottomHun: ExpandableNotificationRow
1231 ) {
1232 val topHunHeight =
1233 mContext.resources.getDimensionPixelSize(R.dimen.notification_content_min_height)
1234 val bottomHunHeight =
1235 mContext.resources.getDimensionPixelSize(R.dimen.notification_max_heads_up_height)
1236 whenever(topHun.intrinsicHeight).thenReturn(topHunHeight)
1237 whenever(bottomHun.intrinsicHeight).thenReturn(bottomHunHeight)
1238
1239 // we need the shelf to mock the real-life behaviour of StackScrollAlgorithm#updateChild
1240 ambientState.shelf = notificationShelf
1241
1242 // add two overlapping HUNs
1243 hostView.removeAllViews()
1244 hostView.addView(topHun)
1245 hostView.addView(bottomHun)
1246
1247 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
1248
1249 // the height shouldn't change
1250 assertThat(topHun.viewState.height).isEqualTo(topHunHeight)
1251 assertThat(bottomHun.viewState.height).isEqualTo(bottomHunHeight)
1252 // the HUN at the bottom should be clipped
1253 assertThat(topHun.viewState.clipBottomAmount).isEqualTo(0)
1254 assertThat(bottomHun.viewState.clipBottomAmount).isEqualTo(bottomHunHeight - topHunHeight)
1255 }
1256
1257 private fun resetViewStates_expansionChanging_notificationAlphaUpdated(
1258 expansionFraction: Float,
1259 expectedAlpha: Float,
1260 ) {
1261 ambientState.isExpansionChanging = true
1262 ambientState.expansionFraction = expansionFraction
1263 stackScrollAlgorithm.initView(context)
1264
1265 stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
1266
1267 expect.that(notificationRow.viewState.alpha).isEqualTo(expectedAlpha)
1268 }
1269 }
1270
mockExpandableNotificationRownull1271 private fun mockExpandableNotificationRow(): ExpandableNotificationRow {
1272 return mock(ExpandableNotificationRow::class.java).apply {
1273 whenever(viewState).thenReturn(ExpandableViewState())
1274 }
1275 }
1276