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 
17 package com.android.systemui.statusbar.notification.stack
18 
19 import android.annotation.DimenRes
20 import android.service.notification.StatusBarNotification
21 import android.testing.AndroidTestingRunner
22 import android.view.View.VISIBLE
23 import androidx.test.filters.SmallTest
24 import com.android.systemui.R
25 import com.android.systemui.SysuiTestCase
26 import com.android.systemui.media.controls.pipeline.MediaDataManager
27 import com.android.systemui.statusbar.LockscreenShadeTransitionController
28 import com.android.systemui.statusbar.StatusBarState
29 import com.android.systemui.statusbar.SysuiStatusBarStateController
30 import com.android.systemui.statusbar.notification.collection.NotificationEntry
31 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
32 import com.android.systemui.statusbar.notification.row.ExpandableView
33 import com.android.systemui.util.mockito.any
34 import com.android.systemui.util.mockito.eq
35 import com.android.systemui.util.mockito.nullable
36 import com.google.common.truth.Truth.assertThat
37 import org.junit.Before
38 import org.junit.Test
39 import org.junit.runner.RunWith
40 import org.mockito.Mock
41 import org.mockito.Mockito.mock
42 import org.mockito.Mockito.`when` as whenever
43 import org.mockito.MockitoAnnotations
44 
45 @SmallTest
46 @RunWith(AndroidTestingRunner::class)
47 class NotificationStackSizeCalculatorTest : SysuiTestCase() {
48 
49     @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController
50     @Mock
51     private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController
52     @Mock private lateinit var mediaDataManager: MediaDataManager
53     @Mock private lateinit var stackLayout: NotificationStackScrollLayout
54 
55     private val testableResources = mContext.orCreateTestableResources
56 
57     private lateinit var sizeCalculator: NotificationStackSizeCalculator
58 
59     private val gapHeight = px(R.dimen.notification_section_divider_height)
60     private val dividerHeight = px(R.dimen.notification_divider_height)
61     private val shelfHeight = px(R.dimen.notification_shelf_height)
62     private val rowHeight = px(R.dimen.notification_max_height)
63 
64     @Before
65     fun setUp() {
66         MockitoAnnotations.initMocks(this)
67 
68         sizeCalculator =
69             NotificationStackSizeCalculator(
70                 statusBarStateController = sysuiStatusBarStateController,
71                 lockscreenShadeTransitionController = lockscreenShadeTransitionController,
72                 mediaDataManager = mediaDataManager,
73                 testableResources.resources
74             )
75     }
76 
77     @Test
78     fun computeMaxKeyguardNotifications_zeroSpace_returnZero() {
79         val rows = listOf(createMockRow(height = rowHeight))
80 
81         val maxNotifications =
82             computeMaxKeyguardNotifications(
83                 rows,
84                 spaceForNotifications = 0f,
85                 spaceForShelf = 0f,
86                 shelfHeight = 0f
87             )
88 
89         assertThat(maxNotifications).isEqualTo(0)
90     }
91 
92     @Test
93     fun computeMaxKeyguardNotifications_infiniteSpace_returnsAll() {
94         val numberOfRows = 30
95         val rows = createLockscreenRows(numberOfRows)
96 
97         val maxNotifications =
98             computeMaxKeyguardNotifications(
99                 rows,
100                 spaceForNotifications = Float.MAX_VALUE,
101                 spaceForShelf = Float.MAX_VALUE,
102                 shelfHeight
103             )
104 
105         assertThat(maxNotifications).isEqualTo(numberOfRows)
106     }
107 
108     @Test
109     fun computeMaxKeyguardNotifications_spaceForOneAndShelf_returnsOne() {
110         setGapHeight(gapHeight)
111         val shelfHeight = rowHeight / 2 // Shelf absence won't leave room for another row.
112         val spaceForNotifications = rowHeight + dividerHeight
113         val spaceForShelf = gapHeight + dividerHeight + shelfHeight
114         val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight))
115 
116         val maxNotifications =
117             computeMaxKeyguardNotifications(rows, spaceForNotifications, spaceForShelf, shelfHeight)
118 
119         assertThat(maxNotifications).isEqualTo(1)
120     }
121 
122     @Test
123     fun computeMaxKeyguardNotifications_onLockscreenSpaceForMinHeightButNotIntrinsicHeight_returnsOne() {
124         setGapHeight(0f)
125         // No divider height since we're testing one element where index = 0
126 
127         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
128         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f)
129 
130         val row = createMockRow(10f, isSticky = true)
131         whenever(row.getMinHeight(any())).thenReturn(5)
132 
133         val maxNotifications =
134             computeMaxKeyguardNotifications(
135                 listOf(row),
136                 /* spaceForNotifications= */ 5f,
137                 /* spaceForShelf= */ 0f,
138                 /* shelfHeight= */ 0f
139             )
140 
141         assertThat(maxNotifications).isEqualTo(1)
142     }
143 
144     @Test
145     fun computeMaxKeyguardNotifications_spaceForTwo_returnsTwo() {
146         setGapHeight(gapHeight)
147         val shelfHeight = shelfHeight + dividerHeight
148         val spaceForNotifications =
149             listOf(
150                     rowHeight + dividerHeight,
151                     gapHeight + rowHeight + dividerHeight,
152                 )
153                 .sum()
154         val spaceForShelf = gapHeight + dividerHeight + shelfHeight
155         val rows =
156             listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight))
157 
158         val maxNotifications =
159             computeMaxKeyguardNotifications(
160                 rows,
161                 spaceForNotifications + 1,
162                 spaceForShelf,
163                 shelfHeight
164             )
165 
166         assertThat(maxNotifications).isEqualTo(2)
167     }
168 
169     @Test
170     fun computeHeight_gapBeforeShelf_returnsSpaceUsed() {
171         // Each row in separate section.
172         setGapHeight(gapHeight)
173 
174         val notifSpace =
175             listOf(
176                     rowHeight,
177                     dividerHeight + gapHeight + rowHeight,
178                 )
179                 .sum()
180 
181         val shelfSpace = dividerHeight + gapHeight + shelfHeight
182         val spaceUsed = notifSpace + shelfSpace
183         val rows =
184             listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight))
185 
186         val maxNotifications =
187             computeMaxKeyguardNotifications(rows, notifSpace, shelfSpace, shelfHeight)
188         assertThat(maxNotifications).isEqualTo(2)
189 
190         val height = sizeCalculator.computeHeight(stackLayout, maxNotifications, this.shelfHeight)
191         assertThat(height).isEqualTo(spaceUsed)
192     }
193 
194     @Test
195     fun computeHeight_noGapBeforeShelf_returnsSpaceUsed() {
196         // Both rows are in the same section.
197         setGapHeight(0f)
198 
199         val spaceForNotifications = rowHeight
200         val spaceForShelf = dividerHeight + shelfHeight
201         val spaceUsed = spaceForNotifications + spaceForShelf
202         val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight))
203 
204         // test that we only use space required
205         val maxNotifications =
206             computeMaxKeyguardNotifications(
207                 rows,
208                 spaceForNotifications + 1,
209                 spaceForShelf,
210                 shelfHeight
211             )
212         assertThat(maxNotifications).isEqualTo(1)
213 
214         val height = sizeCalculator.computeHeight(stackLayout, maxNotifications, this.shelfHeight)
215         assertThat(height).isEqualTo(spaceUsed)
216     }
217 
218     @Test
219     fun onLockscreen_onKeyguard_AndNotGoingToShade_returnsTrue() {
220         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
221         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0f)
222         assertThat(sizeCalculator.onLockscreen()).isTrue()
223     }
224 
225     @Test
226     fun onLockscreen_goingToShade_returnsFalse() {
227         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
228         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(0.5f)
229         assertThat(sizeCalculator.onLockscreen()).isFalse()
230     }
231 
232     @Test
233     fun onLockscreen_notOnLockscreen_returnsFalse() {
234         whenever(sysuiStatusBarStateController.state).thenReturn(StatusBarState.SHADE)
235         whenever(lockscreenShadeTransitionController.fractionToShade).thenReturn(1f)
236         assertThat(sizeCalculator.onLockscreen()).isFalse()
237     }
238 
239     @Test
240     fun getSpaceNeeded_onLockscreenEnoughSpaceStickyHun_intrinsicHeight() {
241         setGapHeight(0f)
242         // No divider height since we're testing one element where index = 0
243 
244         val row = createMockRow(10f, isSticky = true)
245         whenever(row.getMinHeight(any())).thenReturn(5)
246 
247         val space =
248             sizeCalculator.getSpaceNeeded(
249                 row,
250                 visibleIndex = 0,
251                 previousView = null,
252                 stack = stackLayout,
253                 onLockscreen = true
254             )
255         assertThat(space.whenEnoughSpace).isEqualTo(10f)
256     }
257 
258     @Test
259     fun getSpaceNeeded_onLockscreenEnoughSpaceNotStickyHun_minHeight() {
260         setGapHeight(0f)
261         // No divider height since we're testing one element where index = 0
262 
263         val row = createMockRow(rowHeight)
264         whenever(row.heightWithoutLockscreenConstraints).thenReturn(10)
265         whenever(row.getMinHeight(any())).thenReturn(5)
266 
267         val space =
268             sizeCalculator.getSpaceNeeded(
269                 row,
270                 visibleIndex = 0,
271                 previousView = null,
272                 stack = stackLayout,
273                 onLockscreen = true
274             )
275         assertThat(space.whenEnoughSpace).isEqualTo(5)
276     }
277 
278     @Test
279     fun getSpaceNeeded_onLockscreenSavingSpaceStickyHun_minHeight() {
280         setGapHeight(0f)
281         // No divider height since we're testing one element where index = 0
282 
283         val expandableView = createMockRow(10f, isSticky = true)
284         whenever(expandableView.getMinHeight(any())).thenReturn(5)
285 
286         val space =
287             sizeCalculator.getSpaceNeeded(
288                 expandableView,
289                 visibleIndex = 0,
290                 previousView = null,
291                 stack = stackLayout,
292                 onLockscreen = true
293             )
294         assertThat(space.whenSavingSpace).isEqualTo(5)
295     }
296 
297     @Test
298     fun getSpaceNeeded_onLockscreenSavingSpaceNotStickyHun_minHeight() {
299         setGapHeight(0f)
300         // No divider height since we're testing one element where index = 0
301 
302         val expandableView = createMockRow(rowHeight)
303         whenever(expandableView.getMinHeight(any())).thenReturn(5)
304         whenever(expandableView.intrinsicHeight).thenReturn(10)
305 
306         val space =
307             sizeCalculator.getSpaceNeeded(
308                 expandableView,
309                 visibleIndex = 0,
310                 previousView = null,
311                 stack = stackLayout,
312                 onLockscreen = true
313             )
314         assertThat(space.whenSavingSpace).isEqualTo(5)
315     }
316 
317     @Test
318     fun getSpaceNeeded_notOnLockscreen_intrinsicHeight() {
319         setGapHeight(0f)
320         // No divider height since we're testing one element where index = 0
321 
322         val expandableView = createMockRow(rowHeight)
323         whenever(expandableView.getMinHeight(any())).thenReturn(1)
324 
325         val space =
326             sizeCalculator.getSpaceNeeded(
327                 expandableView,
328                 visibleIndex = 0,
329                 previousView = null,
330                 stack = stackLayout,
331                 onLockscreen = false
332             )
333         assertThat(space.whenEnoughSpace).isEqualTo(rowHeight)
334         assertThat(space.whenSavingSpace).isEqualTo(rowHeight)
335     }
336 
337     private fun computeMaxKeyguardNotifications(
338         rows: List<ExpandableView>,
339         spaceForNotifications: Float,
340         spaceForShelf: Float,
341         shelfHeight: Float = this.shelfHeight
342     ): Int {
343         setupChildren(rows)
344         return sizeCalculator.computeMaxKeyguardNotifications(
345             stackLayout,
346             spaceForNotifications,
347             spaceForShelf,
348             shelfHeight
349         )
350     }
351 
352     private fun setupChildren(children: List<ExpandableView>) {
353         whenever(stackLayout.getChildAt(any())).thenAnswer { invocation ->
354             val inx = invocation.getArgument<Int>(0)
355             return@thenAnswer children[inx]
356         }
357         whenever(stackLayout.childCount).thenReturn(children.size)
358     }
359 
360     private fun createLockscreenRows(number: Int): List<ExpandableNotificationRow> =
361         (1..number).map { createMockRow() }.toList()
362 
363     private fun createMockRow(
364         height: Float = rowHeight,
365         isSticky: Boolean = false,
366         isRemoved: Boolean = false,
367         visibility: Int = VISIBLE,
368     ): ExpandableNotificationRow {
369         val row = mock(ExpandableNotificationRow::class.java)
370         val entry = mock(NotificationEntry::class.java)
371         whenever(entry.isStickyAndNotDemoted).thenReturn(isSticky)
372         val sbn = mock(StatusBarNotification::class.java)
373         whenever(entry.sbn).thenReturn(sbn)
374         whenever(row.entry).thenReturn(entry)
375         whenever(row.isRemoved).thenReturn(isRemoved)
376         whenever(row.visibility).thenReturn(visibility)
377         whenever(row.getMinHeight(any())).thenReturn(height.toInt())
378         whenever(row.intrinsicHeight).thenReturn(height.toInt())
379         whenever(row.heightWithoutLockscreenConstraints).thenReturn(height.toInt())
380         return row
381     }
382 
383     private fun setGapHeight(height: Float) {
384         whenever(stackLayout.calculateGapHeight(nullable(), nullable(), any())).thenReturn(height)
385         whenever(stackLayout.calculateGapHeight(nullable(), nullable(), /* visibleIndex= */ eq(0)))
386             .thenReturn(0f)
387     }
388 
389     private fun px(@DimenRes id: Int): Float =
390         testableResources.resources.getDimensionPixelSize(id).toFloat()
391 }
392