• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.stack
18 
19 import android.annotation.DimenRes
20 import android.platform.test.annotations.EnableFlags
21 import android.service.notification.StatusBarNotification
22 import android.view.View.VISIBLE
23 import androidx.test.ext.junit.runners.AndroidJUnit4
24 import androidx.test.filters.SmallTest
25 import com.android.systemui.SysuiTestCase
26 import com.android.systemui.kosmos.testScope
27 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager
28 import com.android.systemui.res.R
29 import com.android.systemui.statusbar.LockscreenShadeTransitionController
30 import com.android.systemui.statusbar.StatusBarState
31 import com.android.systemui.statusbar.SysuiStatusBarStateController
32 import com.android.systemui.statusbar.notification.collection.EntryAdapter
33 import com.android.systemui.statusbar.notification.collection.NotificationEntry
34 import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
35 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
36 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
37 import com.android.systemui.statusbar.notification.row.ExpandableView
38 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController
39 import com.android.systemui.testKosmos
40 import com.android.systemui.util.mockito.any
41 import com.android.systemui.util.mockito.eq
42 import com.android.systemui.util.mockito.nullable
43 import com.google.common.truth.Truth.assertThat
44 import org.junit.Before
45 import org.junit.Test
46 import org.junit.runner.RunWith
47 import org.mockito.Mock
48 import org.mockito.Mockito.mock
49 import org.mockito.Mockito.`when` as whenever
50 import org.mockito.MockitoAnnotations
51 
52 @SmallTest
53 @RunWith(AndroidJUnit4::class)
54 class NotificationStackSizeCalculatorTest : SysuiTestCase() {
55 
56     @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController
57     @Mock
58     private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController
59     @Mock private lateinit var mediaDataManager: MediaDataManager
60     @Mock private lateinit var stackLayout: NotificationStackScrollLayout
61     @Mock private lateinit var seenNotificationsInteractor: SeenNotificationsInteractor
62 
63     private val testableResources = mContext.orCreateTestableResources
64     private val kosmos = testKosmos()
65     private val testScope = kosmos.testScope
66 
67     private lateinit var sizeCalculator: NotificationStackSizeCalculator
68 
69     private val gapHeight = px(R.dimen.notification_section_divider_height)
70     private val dividerHeight = px(R.dimen.notification_divider_height)
71     private val shelfHeight = px(R.dimen.notification_shelf_height)
72     private val rowHeight = px(R.dimen.notification_max_height)
73 
74     @Before
75     fun setUp() {
76         MockitoAnnotations.initMocks(this)
77 
78         sizeCalculator =
79             NotificationStackSizeCalculator(
80                 statusBarStateController = sysuiStatusBarStateController,
81                 lockscreenShadeTransitionController = lockscreenShadeTransitionController,
82                 mediaDataManager = mediaDataManager,
83                 testableResources.resources,
84                 ResourcesSplitShadeStateController(),
85                 seenNotificationsInteractor = seenNotificationsInteractor,
86                 scope = testScope,
87             )
88     }
89 
90     @Test
91     fun computeMaxKeyguardNotifications_zeroSpace_returnZero() {
92         val rows = listOf(createMockRow(height = rowHeight))
93 
94         val maxNotifications =
95             computeMaxKeyguardNotifications(
96                 rows,
97                 spaceForNotifications = 0f,
98                 spaceForShelf = 0f,
99                 shelfHeight = 0f,
100             )
101 
102         assertThat(maxNotifications).isEqualTo(0)
103     }
104 
105     @Test
106     fun computeMaxKeyguardNotifications_infiniteSpace_returnsAll() {
107         val numberOfRows = 30
108         val rows = createLockscreenRows(numberOfRows)
109 
110         val maxNotifications =
111             computeMaxKeyguardNotifications(
112                 rows,
113                 spaceForNotifications = Float.MAX_VALUE,
114                 spaceForShelf = Float.MAX_VALUE,
115                 shelfHeight,
116             )
117 
118         assertThat(maxNotifications).isEqualTo(numberOfRows)
119     }
120 
121     @Test
122     fun computeMaxKeyguardNotifications_spaceForOneAndShelf_returnsOne() {
123         setGapHeight(gapHeight)
124         val shelfHeight = rowHeight / 2 // Shelf absence won't leave room for another row.
125         val spaceForNotifications = rowHeight + dividerHeight
126         val spaceForShelf = gapHeight + dividerHeight + shelfHeight
127         val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight))
128 
129         val maxNotifications =
130             computeMaxKeyguardNotifications(rows, spaceForNotifications, spaceForShelf, shelfHeight)
131 
132         assertThat(maxNotifications).isEqualTo(1)
133     }
134 
135     @Test
136     fun computeMaxKeyguardNotifications_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() {
137         setGapHeight(0f)
138         // No divider height since we're testing one element where index = 0
139 
140         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
141         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f)
142 
143         val row = createMockRow(10f, isSticky = true)
144         whenever(row.getMinHeight(any())).thenReturn(5)
145 
146         val maxNotifications =
147             computeMaxKeyguardNotifications(
148                 listOf(row),
149                 /* spaceForNotifications= */ 5f,
150                 /* spaceForShelf= */ 0f,
151                 /* shelfHeight= */ 0f,
152             )
153 
154         assertThat(maxNotifications).isEqualTo(1)
155     }
156 
157     @Test
158     @EnableFlags(PromotedNotificationUi.FLAG_NAME)
159     fun maxKeyguardNotificationsForPromotedOngoing_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() {
160         setGapHeight(0f)
161         // No divider height since we're testing one element where index = 0
162 
163         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
164         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f)
165 
166         val row = createMockRow(10f, isPromotedOngoing = true)
167         whenever(row.getMinHeight(any())).thenReturn(5)
168 
169         val maxNotifications =
170             computeMaxKeyguardNotifications(
171                 listOf(row),
172                 /* spaceForNotifications= */ 5f,
173                 /* spaceForShelf= */ 0f,
174                 /* shelfHeight= */ 0f,
175             )
176 
177         assertThat(maxNotifications).isEqualTo(1)
178     }
179 
180     @Test
181     fun computeMaxKeyguardNotifications_spaceForTwo_returnsTwo() {
182         setGapHeight(gapHeight)
183         val shelfHeight = shelfHeight + dividerHeight
184         val spaceForNotifications =
185             listOf(rowHeight + dividerHeight, gapHeight + rowHeight + dividerHeight).sum()
186         val spaceForShelf = gapHeight + dividerHeight + shelfHeight
187         val rows =
188             listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight))
189 
190         val maxNotifications =
191             computeMaxKeyguardNotifications(
192                 rows,
193                 spaceForNotifications + 1,
194                 spaceForShelf,
195                 shelfHeight,
196             )
197 
198         assertThat(maxNotifications).isEqualTo(2)
199     }
200 
201     @Test
202     fun computeHeight_gapBeforeShelf_returnsSpaceUsed() {
203         // Each row in separate section.
204         setGapHeight(gapHeight)
205 
206         val notifSpace = listOf(rowHeight, dividerHeight + gapHeight + rowHeight).sum()
207 
208         val shelfSpace = dividerHeight + gapHeight + shelfHeight
209         val spaceUsed = notifSpace + shelfSpace
210         val rows =
211             listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight))
212 
213         val maxNotifications =
214             computeMaxKeyguardNotifications(rows, notifSpace, shelfSpace, shelfHeight)
215         assertThat(maxNotifications).isEqualTo(2)
216 
217         val height = sizeCalculator.computeHeight(stackLayout, maxNotifications, this.shelfHeight)
218         assertThat(height).isEqualTo(spaceUsed)
219     }
220 
221     @Test
222     fun computeHeight_noGapBeforeShelf_returnsSpaceUsed() {
223         // Both rows are in the same section.
224         setGapHeight(0f)
225 
226         val spaceForNotifications = rowHeight
227         val spaceForShelf = dividerHeight + shelfHeight
228         val spaceUsed = spaceForNotifications + spaceForShelf
229         val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight))
230 
231         // test that we only use space required
232         val maxNotifications =
233             computeMaxKeyguardNotifications(
234                 rows,
235                 spaceForNotifications + 1,
236                 spaceForShelf,
237                 shelfHeight,
238             )
239         assertThat(maxNotifications).isEqualTo(1)
240 
241         val height = sizeCalculator.computeHeight(stackLayout, maxNotifications, this.shelfHeight)
242         assertThat(height).isEqualTo(spaceUsed)
243     }
244 
245     @Test
246     fun onLockscreen_onKeyguard_AndNotGoingToShade_returnsTrue() {
247         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
248         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f)
249         assertThat(sizeCalculator.onLockscreen()).isTrue()
250     }
251 
252     @Test
253     fun onLockscreen_goingToShade_returnsFalse() {
254         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
255         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0.5f)
256         assertThat(sizeCalculator.onLockscreen()).isFalse()
257     }
258 
259     @Test
260     fun onLockscreen_notOnLockscreen_returnsFalse() {
261         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.SHADE)
262         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(1f)
263         assertThat(sizeCalculator.onLockscreen()).isFalse()
264     }
265 
266     @Test
267     fun getSpaceNeeded_onLockscreenEnoughSpaceStickyHun_intrinsicHeight() {
268         setGapHeight(0f)
269         // No divider height since we're testing one element where index = 0
270 
271         val row = createMockRow(10f, isSticky = true)
272         whenever(row.getMinHeight(any())).thenReturn(5)
273 
274         val space =
275             sizeCalculator.getSpaceNeeded(
276                 row,
277                 visibleIndex = 0,
278                 previousView = null,
279                 stack = stackLayout,
280                 onLockscreen = true,
281             )
282         assertThat(space.whenEnoughSpace).isEqualTo(10f)
283     }
284 
285     @Test
286     @EnableFlags(PromotedNotificationUi.FLAG_NAME)
287     fun getSpaceNeeded_onLockscreenEnoughSpacePromotedOngoing_intrinsicHeight() {
288         setGapHeight(0f)
289         // No divider height since we're testing one element where index = 0
290 
291         val row = createMockRow(10f, isPromotedOngoing = true)
292         whenever(row.getMinHeight(any())).thenReturn(5)
293 
294         val space =
295             sizeCalculator.getSpaceNeeded(
296                 row,
297                 visibleIndex = 0,
298                 previousView = null,
299                 stack = stackLayout,
300                 onLockscreen = true,
301             )
302         assertThat(space.whenEnoughSpace).isEqualTo(10f)
303     }
304 
305     @Test
306     fun getSpaceNeeded_onLockscreenEnoughSpaceNotStickyHun_minHeight() {
307         setGapHeight(0f)
308         // No divider height since we're testing one element where index = 0
309 
310         val row = createMockRow(rowHeight)
311         whenever(row.heightWithoutLockscreenConstraints).thenReturn(10)
312         whenever(row.getMinHeight(any())).thenReturn(5)
313 
314         val space =
315             sizeCalculator.getSpaceNeeded(
316                 row,
317                 visibleIndex = 0,
318                 previousView = null,
319                 stack = stackLayout,
320                 onLockscreen = true,
321             )
322         assertThat(space.whenEnoughSpace).isEqualTo(5)
323     }
324 
325     @Test
326     fun getSpaceNeeded_onLockscreenSavingSpaceStickyHun_minHeight() {
327         setGapHeight(0f)
328         // No divider height since we're testing one element where index = 0
329 
330         val expandableView = createMockRow(10f, isSticky = true)
331         whenever(expandableView.getMinHeight(any())).thenReturn(5)
332 
333         val space =
334             sizeCalculator.getSpaceNeeded(
335                 expandableView,
336                 visibleIndex = 0,
337                 previousView = null,
338                 stack = stackLayout,
339                 onLockscreen = true,
340             )
341         assertThat(space.whenSavingSpace).isEqualTo(5)
342     }
343 
344     @Test
345     @EnableFlags(PromotedNotificationUi.FLAG_NAME)
346     fun getSpaceNeeded_onLockscreenSavingSpacePromotedOngoing_minHeight() {
347         setGapHeight(0f)
348         // No divider height since we're testing one element where index = 0
349 
350         val expandableView = createMockRow(10f, isPromotedOngoing = true)
351         whenever(expandableView.getMinHeight(any())).thenReturn(5)
352 
353         val space =
354             sizeCalculator.getSpaceNeeded(
355                 expandableView,
356                 visibleIndex = 0,
357                 previousView = null,
358                 stack = stackLayout,
359                 onLockscreen = true,
360             )
361         assertThat(space.whenSavingSpace).isEqualTo(5)
362     }
363 
364     @Test
365     fun getSpaceNeeded_onLockscreenSavingSpaceNotStickyHun_minHeight() {
366         setGapHeight(0f)
367         // No divider height since we're testing one element where index = 0
368 
369         val expandableView = createMockRow(rowHeight)
370         whenever(expandableView.getMinHeight(any())).thenReturn(5)
371         whenever(expandableView.intrinsicHeight).thenReturn(10)
372 
373         val space =
374             sizeCalculator.getSpaceNeeded(
375                 expandableView,
376                 visibleIndex = 0,
377                 previousView = null,
378                 stack = stackLayout,
379                 onLockscreen = true,
380             )
381         assertThat(space.whenSavingSpace).isEqualTo(5)
382     }
383 
384     @Test
385     fun getSpaceNeeded_notOnLockscreen_intrinsicHeight() {
386         setGapHeight(0f)
387         // No divider height since we're testing one element where index = 0
388 
389         val expandableView = createMockRow(rowHeight)
390         whenever(expandableView.getMinHeight(any())).thenReturn(1)
391 
392         val space =
393             sizeCalculator.getSpaceNeeded(
394                 expandableView,
395                 visibleIndex = 0,
396                 previousView = null,
397                 stack = stackLayout,
398                 onLockscreen = false,
399             )
400         assertThat(space.whenEnoughSpace).isEqualTo(rowHeight)
401         assertThat(space.whenSavingSpace).isEqualTo(rowHeight)
402     }
403 
404     private fun computeMaxKeyguardNotifications(
405         rows: List<ExpandableView>,
406         spaceForNotifications: Float,
407         spaceForShelf: Float,
408         shelfHeight: Float = this.shelfHeight,
409     ): Int {
410         setupChildren(rows)
411         return sizeCalculator.computeMaxKeyguardNotifications(
412             stackLayout,
413             spaceForNotifications,
414             spaceForShelf,
415             shelfHeight,
416         )
417     }
418 
419     private fun setupChildren(children: List<ExpandableView>) {
420         whenever(stackLayout.getChildAt(any())).thenAnswer { invocation ->
421             val inx = invocation.getArgument<Int>(0)
422             return@thenAnswer children[inx]
423         }
424         whenever(stackLayout.childCount).thenReturn(children.size)
425     }
426 
427     private fun createLockscreenRows(number: Int): List<ExpandableNotificationRow> =
428         (1..number).map { createMockRow() }.toList()
429 
430     private fun createMockRow(
431         height: Float = rowHeight,
432         isSticky: Boolean = false,
433         isRemoved: Boolean = false,
434         visibility: Int = VISIBLE,
435         isPromotedOngoing: Boolean = false,
436     ): ExpandableNotificationRow {
437         val row = mock(ExpandableNotificationRow::class.java)
438         val entry = mock(NotificationEntry::class.java)
439         whenever(entry.isStickyAndNotDemoted).thenReturn(isSticky)
440         val entryAdapter = mock(EntryAdapter::class.java)
441         whenever(entryAdapter.canPeek()).thenReturn(isSticky)
442         whenever(row.entryAdapter).thenReturn(entryAdapter)
443         val sbn = mock(StatusBarNotification::class.java)
444         whenever(entry.sbn).thenReturn(sbn)
445         whenever(row.entry).thenReturn(entry)
446         whenever(row.isRemoved).thenReturn(isRemoved)
447         whenever(row.visibility).thenReturn(visibility)
448         whenever(row.getMinHeight(any())).thenReturn(height.toInt())
449         whenever(row.intrinsicHeight).thenReturn(height.toInt())
450         whenever(row.heightWithoutLockscreenConstraints).thenReturn(height.toInt())
451         whenever(row.isPromotedOngoing).thenReturn(isPromotedOngoing)
452         return row
453     }
454 
455     private fun setGapHeight(height: Float) {
456         whenever(stackLayout.calculateGapHeight(nullable(), nullable(), any())).thenReturn(height)
457         whenever(stackLayout.calculateGapHeight(nullable(), nullable(), /* visibleIndex= */ eq(0)))
458             .thenReturn(0f)
459     }
460 
461     private fun px(@DimenRes id: Int): Float =
462         testableResources.resources.getDimensionPixelSize(id).toFloat()
463 }
464