1 package com.android.systemui.statusbar.notification.stack
2 
3 import android.testing.AndroidTestingRunner
4 import android.testing.TestableLooper.RunWithLooper
5 import android.view.LayoutInflater
6 import android.widget.FrameLayout
7 import androidx.test.filters.SmallTest
8 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
9 import com.android.systemui.R
10 import com.android.systemui.SysuiTestCase
11 import com.android.systemui.animation.ShadeInterpolation
12 import com.android.systemui.flags.FakeFeatureFlags
13 import com.android.systemui.flags.FeatureFlags
14 import com.android.systemui.flags.Flags
15 import com.android.systemui.shade.transition.LargeScreenShadeInterpolator
16 import com.android.systemui.statusbar.NotificationShelf
17 import com.android.systemui.statusbar.StatusBarIconView
18 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
19 import com.android.systemui.statusbar.notification.row.ExpandableView
20 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.StackScrollAlgorithmState
21 import com.android.systemui.util.mockito.mock
22 import junit.framework.Assert.assertEquals
23 import junit.framework.Assert.assertFalse
24 import junit.framework.Assert.assertTrue
25 import org.junit.Assume.assumeTrue
26 import org.junit.Before
27 import org.junit.Test
28 import org.junit.runner.RunWith
29 import org.mockito.Mock
30 import org.mockito.Mockito.mock
31 import org.mockito.MockitoAnnotations
32 import org.mockito.Mockito.`when` as whenever
33 
34 /**
35  * Tests for {@link NotificationShelf}.
36  */
37 @SmallTest
38 @RunWith(AndroidTestingRunner::class)
39 @RunWithLooper
40 open class NotificationShelfTest : SysuiTestCase() {
41 
42     open val useShelfRefactor: Boolean = false
43     open val useSensitiveReveal: Boolean = false
44     private val flags = FakeFeatureFlags()
45 
46     @Mock
47     private lateinit var largeScreenShadeInterpolator: LargeScreenShadeInterpolator
48     @Mock
49     private lateinit var ambientState: AmbientState
50     @Mock
51     private lateinit var hostLayoutController: NotificationStackScrollLayoutController
52     @Mock
53     private lateinit var hostLayout: NotificationStackScrollLayout
54     @Mock
55     private lateinit var roundnessManager: NotificationRoundnessManager
56 
57     private lateinit var shelf: NotificationShelf
58 
59     @Before
60     fun setUp() {
61         MockitoAnnotations.initMocks(this)
62         mDependency.injectTestDependency(FeatureFlags::class.java, flags)
63         flags.set(Flags.NOTIFICATION_SHELF_REFACTOR, useShelfRefactor)
64         flags.set(Flags.SENSITIVE_REVEAL_ANIM, useSensitiveReveal)
65         flags.setDefault(Flags.IMPROVED_HUN_ANIMATIONS)
66         val root = FrameLayout(context)
67         shelf = LayoutInflater.from(root.context)
68                 .inflate(/* resource = */ R.layout.status_bar_notification_shelf,
69                     /* root = */root,
70                     /* attachToRoot = */false) as NotificationShelf
71 
72         whenever(ambientState.largeScreenShadeInterpolator).thenReturn(largeScreenShadeInterpolator)
73         whenever(ambientState.isSmallScreen).thenReturn(true)
74 
75         if (useShelfRefactor) {
76             shelf.bind(ambientState, hostLayout, roundnessManager)
77         } else {
78             shelf.bind(ambientState, hostLayoutController)
79         }
80         shelf.layout(/* left */ 0, /* top */ 0, /* right */ 30, /* bottom */5)
81     }
82 
83     @Test
84     fun testShadeWidth_BasedOnFractionToShade() {
85         setFractionToShade(0f)
86         setOnLockscreen(true)
87 
88         shelf.updateActualWidth(/* fractionToShade */ 0f, /* shortestWidth */ 10f)
89         assertTrue(shelf.actualWidth == 10)
90 
91         shelf.updateActualWidth(/* fractionToShade */ 0.5f, /* shortestWidth */ 10f)
92         assertTrue(shelf.actualWidth == 20)
93 
94         shelf.updateActualWidth(/* fractionToShade */ 1f, /* shortestWidth */ 10f)
95         assertTrue(shelf.actualWidth == 30)
96     }
97 
98     @Test
99     fun testShelfIsLong_WhenNotOnLockscreen() {
100         setFractionToShade(0f)
101         setOnLockscreen(false)
102 
103         shelf.updateActualWidth(/* fraction */ 0f, /* shortestWidth */ 10f)
104         assertTrue(shelf.actualWidth == 30)
105     }
106 
107     @Test
108     fun testX_inViewForClick() {
109         val isXInView = shelf.isXInView(
110                 /* localX */ 5f,
111                 /* slop */ 5f,
112                 /* left */ 0f,
113                 /* right */ 10f)
114         assertTrue(isXInView)
115     }
116 
117     @Test
118     fun testXSlop_inViewForClick() {
119         val isLeftXSlopInView = shelf.isXInView(
120                 /* localX */ -3f,
121                 /* slop */ 5f,
122                 /* left */ 0f,
123                 /* right */ 10f)
124         assertTrue(isLeftXSlopInView)
125 
126         val isRightXSlopInView = shelf.isXInView(
127                 /* localX */ 13f,
128                 /* slop */ 5f,
129                 /* left */ 0f,
130                 /* right */ 10f)
131         assertTrue(isRightXSlopInView)
132     }
133 
134     @Test
135     fun testX_notInViewForClick() {
136         val isXLeftOfShelfInView = shelf.isXInView(
137                 /* localX */ -10f,
138                 /* slop */ 5f,
139                 /* left */ 0f,
140                 /* right */ 10f)
141         assertFalse(isXLeftOfShelfInView)
142 
143         val isXRightOfShelfInView = shelf.isXInView(
144                 /* localX */ 20f,
145                 /* slop */ 5f,
146                 /* left */ 0f,
147                 /* right */ 10f)
148         assertFalse(isXRightOfShelfInView)
149     }
150 
151     @Test
152     fun testY_inViewForClick() {
153         val isYInView = shelf.isYInView(
154                 /* localY */ 5f,
155                 /* slop */ 5f,
156                 /* top */ 0f,
157                 /* bottom */ 10f)
158         assertTrue(isYInView)
159     }
160 
161     @Test
162     fun testYSlop_inViewForClick() {
163         val isTopYSlopInView = shelf.isYInView(
164                 /* localY */ -3f,
165                 /* slop */ 5f,
166                 /* top */ 0f,
167                 /* bottom */ 10f)
168         assertTrue(isTopYSlopInView)
169 
170         val isBottomYSlopInView = shelf.isYInView(
171                 /* localY */ 13f,
172                 /* slop */ 5f,
173                 /* top */ 0f,
174                 /* bottom */ 10f)
175         assertTrue(isBottomYSlopInView)
176     }
177 
178     @Test
179     fun testY_notInViewForClick() {
180         val isYAboveShelfInView = shelf.isYInView(
181                 /* localY */ -10f,
182                 /* slop */ 5f,
183                 /* top */ 0f,
184                 /* bottom */ 5f)
185         assertFalse(isYAboveShelfInView)
186 
187         val isYBelowShelfInView = shelf.isYInView(
188                 /* localY */ 15f,
189                 /* slop */ 5f,
190                 /* top */ 0f,
191                 /* bottom */ 5f)
192         assertFalse(isYBelowShelfInView)
193     }
194 
195     @Test
196     fun getAmountInShelf_lastViewBelowShelf_completelyInShelf() {
197         val shelfClipStart = 0f
198         val viewStart = 1f
199 
200         val expandableView = mock(ExpandableView::class.java)
201         whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
202         whenever(expandableView.translationY).thenReturn(viewStart)
203         whenever(expandableView.actualHeight).thenReturn(20)
204 
205         whenever(expandableView.minHeight).thenReturn(20)
206         whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY
207         whenever(expandableView.isInShelf).thenReturn(true)
208 
209         whenever(ambientState.isOnKeyguard).thenReturn(true)
210         whenever(ambientState.isExpansionChanging).thenReturn(false)
211         whenever(ambientState.isShadeExpanded).thenReturn(true)
212 
213         val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
214                 /* view= */ expandableView,
215                 /* scrollingFast= */ false,
216                 /* expandingAnimated= */ false,
217                 /* isLastChild= */ true,
218                 shelfClipStart)
219         assertEquals(1f, amountInShelf)
220     }
221 
222     @Test
223     fun getAmountInShelf_lastViewAlmostBelowShelf_completelyInShelf() {
224         val viewStart = 0f
225         val shelfClipStart = 0.001f
226 
227         val expandableView = mock(ExpandableView::class.java)
228         whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
229         whenever(expandableView.translationY).thenReturn(viewStart)
230         whenever(expandableView.actualHeight).thenReturn(20)
231 
232         whenever(expandableView.minHeight).thenReturn(20)
233         whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY
234         whenever(expandableView.isInShelf).thenReturn(true)
235 
236         whenever(ambientState.isOnKeyguard).thenReturn(true)
237         whenever(ambientState.isExpansionChanging).thenReturn(false)
238         whenever(ambientState.isShadeExpanded).thenReturn(true)
239 
240         val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
241                 /* view= */ expandableView,
242                 /* scrollingFast= */ false,
243                 /* expandingAnimated= */ false,
244                 /* isLastChild= */ true,
245                 shelfClipStart)
246         assertEquals(1f, amountInShelf)
247     }
248 
249     @Test
250     fun getAmountInShelf_lastViewHalfClippedByShelf_halfInShelf() {
251         val viewStart = 0f
252         val shelfClipStart = 10f
253 
254         val expandableView = mock(ExpandableView::class.java)
255         whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
256         whenever(expandableView.translationY).thenReturn(viewStart)
257         whenever(expandableView.actualHeight).thenReturn(25)
258 
259         whenever(expandableView.minHeight).thenReturn(25)
260         whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY
261         whenever(expandableView.isInShelf).thenReturn(true)
262 
263         whenever(ambientState.isOnKeyguard).thenReturn(true)
264         whenever(ambientState.isExpansionChanging).thenReturn(false)
265         whenever(ambientState.isShadeExpanded).thenReturn(true)
266 
267         val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
268                 /* view= */ expandableView,
269                 /* scrollingFast= */ false,
270                 /* expandingAnimated= */ false,
271                 /* isLastChild= */ true,
272                 shelfClipStart)
273         assertEquals(0.5f, amountInShelf)
274     }
275 
276     @Test
277     fun getAmountInShelf_lastViewAboveShelf_notInShelf() {
278         val viewStart = 0f
279         val shelfClipStart = 15f
280 
281         val expandableView = mock(ExpandableView::class.java)
282         whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
283         whenever(expandableView.translationY).thenReturn(viewStart)
284         whenever(expandableView.actualHeight).thenReturn(10)
285 
286         whenever(expandableView.minHeight).thenReturn(10)
287         whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY
288         whenever(expandableView.isInShelf).thenReturn(false)
289 
290         whenever(ambientState.isExpansionChanging).thenReturn(false)
291         whenever(ambientState.isOnKeyguard).thenReturn(true)
292 
293         val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
294                 /* view= */ expandableView,
295                 /* scrollingFast= */ false,
296                 /* expandingAnimated= */ false,
297                 /* isLastChild= */ true,
298                 shelfClipStart)
299         assertEquals(0f, amountInShelf)
300     }
301 
302     @Test
303     fun updateState_expansionChanging_shelfTransparent() {
304         updateState_expansionChanging_shelfAlphaUpdated(
305                 expansionFraction = 0.25f,
306                 expectedAlpha = 0.0f
307         )
308     }
309 
310     @Test
311     fun updateState_expansionChangingWhileBouncerInTransit_shelfTransparent() {
312         whenever(ambientState.isBouncerInTransit).thenReturn(true)
313 
314         updateState_expansionChanging_shelfAlphaUpdated(
315                 expansionFraction = 0.85f,
316                 expectedAlpha = 0.0f
317         )
318     }
319 
320     @Test
321     fun updateState_expansionChanging_shelfAlphaUpdated() {
322         updateState_expansionChanging_shelfAlphaUpdated(
323                 expansionFraction = 0.6f,
324                 expectedAlpha = ShadeInterpolation.getContentAlpha(0.6f),
325         )
326     }
327 
328     @Test
329     fun updateState_largeScreen_expansionChanging_shelfAlphaUpdated_largeScreenValue() {
330         val expansionFraction = 0.6f
331         whenever(ambientState.isSmallScreen).thenReturn(false)
332         whenever(largeScreenShadeInterpolator.getNotificationContentAlpha(expansionFraction))
333             .thenReturn(0.123f)
334 
335         updateState_expansionChanging_shelfAlphaUpdated(
336             expansionFraction = expansionFraction,
337             expectedAlpha = 0.123f
338         )
339     }
340 
341     @Test
342     fun updateState_expansionChangingWhileBouncerInTransit_shelfAlphaUpdated() {
343         whenever(ambientState.isBouncerInTransit).thenReturn(true)
344 
345         updateState_expansionChanging_shelfAlphaUpdated(
346                 expansionFraction = 0.95f,
347                 expectedAlpha = aboutToShowBouncerProgress(0.95f),
348         )
349     }
350 
351     @Test
352     fun updateState_largeScreen_expansionChangingWhileBouncerInTransit_bouncerInterpolatorUsed() {
353         whenever(ambientState.isBouncerInTransit).thenReturn(true)
354 
355         updateState_expansionChanging_shelfAlphaUpdated(
356                 expansionFraction = 0.95f,
357                 expectedAlpha = aboutToShowBouncerProgress(0.95f),
358         )
359     }
360 
361     @Test
362     fun updateState_withNullLastVisibleBackgroundChild_hideShelf() {
363         // GIVEN
364         assumeTrue(useSensitiveReveal)
365         whenever(ambientState.stackY).thenReturn(100f)
366         whenever(ambientState.stackHeight).thenReturn(100f)
367         val paddingBetweenElements =
368             context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
369         val endOfStack = 200f + paddingBetweenElements
370         whenever(ambientState.isShadeExpanded).thenReturn(true)
371         val lastVisibleBackgroundChild = mock<ExpandableView>()
372         val expandableViewState = ExpandableViewState()
373         whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
374         val stackScrollAlgorithmState = StackScrollAlgorithmState()
375         stackScrollAlgorithmState.firstViewInShelf = mock()
376 
377         whenever(ambientState.lastVisibleBackgroundChild).thenReturn(null)
378 
379         // WHEN
380         shelf.updateState(stackScrollAlgorithmState, ambientState)
381 
382         // THEN
383         val shelfState = shelf.viewState as NotificationShelf.ShelfState
384         assertEquals(true, shelfState.hidden)
385         assertEquals(endOfStack, shelfState.yTranslation)
386     }
387 
388     @Test
389     fun updateState_withNullFirstViewInShelf_hideShelf() {
390         // GIVEN
391         assumeTrue(useSensitiveReveal)
392         whenever(ambientState.stackY).thenReturn(100f)
393         whenever(ambientState.stackHeight).thenReturn(100f)
394         val paddingBetweenElements =
395             context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
396         val endOfStack = 200f + paddingBetweenElements
397         whenever(ambientState.isShadeExpanded).thenReturn(true)
398         val lastVisibleBackgroundChild = mock<ExpandableView>()
399         val expandableViewState = ExpandableViewState()
400         whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
401         whenever(ambientState.lastVisibleBackgroundChild).thenReturn(lastVisibleBackgroundChild)
402         val stackScrollAlgorithmState = StackScrollAlgorithmState()
403 
404         stackScrollAlgorithmState.firstViewInShelf = null
405 
406         // WHEN
407         shelf.updateState(stackScrollAlgorithmState, ambientState)
408 
409         // THEN
410         val shelfState = shelf.viewState as NotificationShelf.ShelfState
411         assertEquals(true, shelfState.hidden)
412         assertEquals(endOfStack, shelfState.yTranslation)
413     }
414 
415     @Test
416     fun updateState_withCollapsedShade_hideShelf() {
417         // GIVEN
418         assumeTrue(useSensitiveReveal)
419         whenever(ambientState.stackY).thenReturn(100f)
420         whenever(ambientState.stackHeight).thenReturn(100f)
421         val paddingBetweenElements =
422             context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
423         val endOfStack = 200f + paddingBetweenElements
424         val lastVisibleBackgroundChild = mock<ExpandableView>()
425         val expandableViewState = ExpandableViewState()
426         whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
427         whenever(ambientState.lastVisibleBackgroundChild).thenReturn(lastVisibleBackgroundChild)
428         val stackScrollAlgorithmState = StackScrollAlgorithmState()
429         stackScrollAlgorithmState.firstViewInShelf = mock()
430 
431         whenever(ambientState.isShadeExpanded).thenReturn(false)
432 
433         // WHEN
434         shelf.updateState(stackScrollAlgorithmState, ambientState)
435 
436         // THEN
437         val shelfState = shelf.viewState as NotificationShelf.ShelfState
438         assertEquals(true, shelfState.hidden)
439         assertEquals(endOfStack, shelfState.yTranslation)
440     }
441 
442     @Test
443     fun updateState_withHiddenSectionBeforeShelf_hideShelf() {
444         // GIVEN
445         assumeTrue(useSensitiveReveal)
446         whenever(ambientState.stackY).thenReturn(100f)
447         whenever(ambientState.stackHeight).thenReturn(100f)
448         val paddingBetweenElements =
449             context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
450         val endOfStack = 200f + paddingBetweenElements
451         whenever(ambientState.isShadeExpanded).thenReturn(true)
452         val lastVisibleBackgroundChild = mock<ExpandableView>()
453         val expandableViewState = ExpandableViewState()
454         whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
455         val stackScrollAlgorithmState = StackScrollAlgorithmState()
456         whenever(ambientState.lastVisibleBackgroundChild).thenReturn(lastVisibleBackgroundChild)
457 
458         val ssaVisibleChild = mock<ExpandableView>()
459         val ssaVisibleChildState = ExpandableViewState()
460         ssaVisibleChildState.hidden = true
461         whenever(ssaVisibleChild.viewState).thenReturn(ssaVisibleChildState)
462 
463         val ssaVisibleChild1 = mock<ExpandableView>()
464         val ssaVisibleChildState1 = ExpandableViewState()
465         ssaVisibleChildState1.hidden = true
466         whenever(ssaVisibleChild1.viewState).thenReturn(ssaVisibleChildState1)
467 
468         stackScrollAlgorithmState.visibleChildren.add(ssaVisibleChild)
469         stackScrollAlgorithmState.visibleChildren.add(ssaVisibleChild1)
470         whenever(ambientState.isExpansionChanging).thenReturn(true)
471         stackScrollAlgorithmState.firstViewInShelf = ssaVisibleChild1
472 
473         // WHEN
474         shelf.updateState(stackScrollAlgorithmState, ambientState)
475 
476         // THEN
477         val shelfState = shelf.viewState as NotificationShelf.ShelfState
478         assertEquals(true, shelfState.hidden)
479         assertEquals(endOfStack, shelfState.yTranslation)
480     }
481 
482     private fun setFractionToShade(fraction: Float) {
483         whenever(ambientState.fractionToShade).thenReturn(fraction)
484     }
485 
486     private fun setOnLockscreen(isOnLockscreen: Boolean) {
487         whenever(ambientState.isOnKeyguard).thenReturn(isOnLockscreen)
488     }
489 
490     private fun updateState_expansionChanging_shelfAlphaUpdated(
491             expansionFraction: Float,
492             expectedAlpha: Float
493     ) {
494         whenever(ambientState.lastVisibleBackgroundChild)
495                 .thenReturn(ExpandableNotificationRow(mContext, null))
496         whenever(ambientState.isExpansionChanging).thenReturn(true)
497         whenever(ambientState.expansionFraction).thenReturn(expansionFraction)
498         whenever(hostLayoutController.speedBumpIndex).thenReturn(0)
499 
500         shelf.updateState(StackScrollAlgorithmState(), ambientState)
501 
502         assertEquals(expectedAlpha, shelf.viewState.alpha)
503     }
504 }
505 
506 @SmallTest
507 @RunWith(AndroidTestingRunner::class)
508 @RunWithLooper
509 class NotificationShelfWithRefactorTest : NotificationShelfTest() {
510     override val useShelfRefactor: Boolean = true
511 }
512 
513 @SmallTest
514 @RunWith(AndroidTestingRunner::class)
515 @RunWithLooper
516 class NotificationShelfWithSensitiveRevealTest : NotificationShelfTest() {
517     override val useSensitiveReveal: Boolean = true
518 }
519 
520 @SmallTest
521 @RunWith(AndroidTestingRunner::class)
522 @RunWithLooper
523 class NotificationShelfWithBothFlagsTest : NotificationShelfTest() {
524     override val useShelfRefactor: Boolean = true
525     override val useSensitiveReveal: Boolean = true
526 }
527