1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 package com.android.systemui.statusbar.notification.row
17 
18 import android.graphics.BitmapFactory
19 import android.graphics.drawable.BitmapDrawable
20 import android.graphics.drawable.Drawable
21 import android.graphics.drawable.Icon
22 import android.net.Uri
23 import android.testing.TestableLooper.RunWithLooper
24 import androidx.test.ext.junit.runners.AndroidJUnit4
25 import androidx.test.filters.SmallTest
26 import com.android.internal.widget.NotificationDrawableConsumer
27 import com.android.systemui.SysuiTestCase
28 import com.android.systemui.graphics.ImageLoader
29 import com.android.systemui.res.R
30 import com.android.systemui.util.mockito.argumentCaptor
31 import com.android.systemui.util.mockito.mock
32 import com.google.common.truth.Truth.assertThat
33 import kotlinx.coroutines.ExperimentalCoroutinesApi
34 import kotlinx.coroutines.test.StandardTestDispatcher
35 import kotlinx.coroutines.test.TestScope
36 import kotlinx.coroutines.test.advanceTimeBy
37 import kotlinx.coroutines.test.runCurrent
38 import kotlinx.coroutines.test.runTest
39 import org.junit.After
40 import org.junit.Before
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 import org.mockito.Mockito.clearInvocations
44 import org.mockito.Mockito.verify
45 import org.mockito.Mockito.verifyZeroInteractions
46 
47 private const val FREE_IMAGE_DELAY_MS = 4000L
48 private const val MAX_IMAGE_SIZE = 512 // size of the test drawables in pixels
49 
50 @OptIn(ExperimentalCoroutinesApi::class)
51 @SmallTest
52 @RunWithLooper
53 @RunWith(AndroidJUnit4::class)
54 class BigPictureIconManagerTest : SysuiTestCase() {
55 
56     private val testDispatcher = StandardTestDispatcher()
57     private val testScope = TestScope(testDispatcher)
58     private val testableResources = context.orCreateTestableResources
59     private val imageLoader: ImageLoader = ImageLoader(context, testDispatcher)
60     private val statsManager = BigPictureStatsManager(mock(), testDispatcher, mock())
61     private var mockConsumer: NotificationDrawableConsumer = mock()
62     private val drawableCaptor = argumentCaptor<Drawable>()
63 
64     private lateinit var iconManager: BigPictureIconManager
65 
<lambda>null66     private val expectedDrawable by lazy {
67         context.resources.getDrawable(R.drawable.dessert_zombiegingerbread, context.theme)
68     }
<lambda>null69     private val supportedIcon by lazy {
70         Icon.createWithContentUri(
71             Uri.parse(
72                 "android.resource://${context.packageName}/${R.drawable.dessert_zombiegingerbread}"
73             )
74         )
75     }
<lambda>null76     private val unsupportedIcon by lazy {
77         Icon.createWithBitmap(
78             BitmapFactory.decodeResource(context.resources, R.drawable.dessert_donutburger)
79         )
80     }
<lambda>null81     private val invalidIcon by lazy { Icon.createWithContentUri(Uri.parse("this.is/broken")) }
82 
83     @Before
setUpnull84     fun setUp() {
85         allowTestableLooperAsMainThread()
86         overrideMaxImageSizes()
87         iconManager =
88             BigPictureIconManager(
89                 context,
90                 imageLoader,
91                 statsManager,
92                 scope = testScope,
93                 mainDispatcher = testDispatcher,
94                 bgDispatcher = testDispatcher
95             )
96     }
97 
98     @After
tearDownnull99     fun tearDown() {
100         disallowTestableLooperAsMainThread()
101     }
102 
103     @Test
onIconUpdated_supportedType_placeholderLoadednull104     fun onIconUpdated_supportedType_placeholderLoaded() =
105         testScope.runTest {
106             // WHEN update with a supported icon
107             iconManager.updateIcon(mockConsumer, supportedIcon).run()
108 
109             // THEN consumer is updated with a placeholder
110             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
111             assertIsPlaceHolder(drawableCaptor.value)
112             assertSize(drawableCaptor.value)
113         }
114 
115     @Test
onIconUpdated_unsupportedType_fullImageLoadednull116     fun onIconUpdated_unsupportedType_fullImageLoaded() =
117         testScope.runTest {
118             // WHEN update with an unsupported icon
119             iconManager.updateIcon(mockConsumer, unsupportedIcon).run()
120 
121             // THEN consumer is updated with the full image
122             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
123             assertIsFullImage(drawableCaptor.value)
124             assertSize(drawableCaptor.value)
125         }
126 
127     @Test
onIconUpdated_withNull_drawableIsNullnull128     fun onIconUpdated_withNull_drawableIsNull() =
129         testScope.runTest {
130             // WHEN update with null
131             iconManager.updateIcon(mockConsumer, null).run()
132 
133             // THEN consumer is updated with null
134             verify(mockConsumer).setImageDrawable(null)
135         }
136 
137     @Test
onIconUpdated_invalidIcon_drawableIsNullnull138     fun onIconUpdated_invalidIcon_drawableIsNull() =
139         testScope.runTest {
140             // WHEN update with an invalid icon
141             iconManager.updateIcon(mockConsumer, invalidIcon).run()
142 
143             // THEN consumer is updated with null
144             verify(mockConsumer).setImageDrawable(null)
145         }
146 
147     @Test
onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithPlaceholdernull148     fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithPlaceholder() =
149         testScope.runTest {
150             // GIVEN a consumer is set
151             iconManager.updateIcon(mockConsumer, supportedIcon).run()
152             clearInvocations(mockConsumer)
153 
154             // WHEN a new consumer is set
155             val newConsumer: NotificationDrawableConsumer = mock()
156             iconManager.updateIcon(newConsumer, supportedIcon).run()
157 
158             // THEN the new consumer is updated
159             verify(newConsumer).setImageDrawable(drawableCaptor.capture())
160             assertIsPlaceHolder(drawableCaptor.value)
161             assertSize(drawableCaptor.value)
162             // AND nothing happens on the old consumer
163             verifyZeroInteractions(mockConsumer)
164         }
165 
166     @Test
onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithFullImagenull167     fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithFullImage() =
168         testScope.runTest {
169             // GIVEN a consumer is set
170             iconManager.updateIcon(mockConsumer, supportedIcon).run()
171             // AND an icon is loaded
172             iconManager.onViewShown(true)
173             runCurrent()
174             clearInvocations(mockConsumer)
175 
176             // WHEN a new consumer is set
177             val newConsumer: NotificationDrawableConsumer = mock()
178             iconManager.updateIcon(newConsumer, supportedIcon).run()
179 
180             // THEN the new consumer is updated
181             verify(newConsumer).setImageDrawable(drawableCaptor.capture())
182             assertIsFullImage(drawableCaptor.value)
183             assertSize(drawableCaptor.value)
184             // AND nothing happens on the old consumer
185             verifyZeroInteractions(mockConsumer)
186         }
187 
188     @Test
onIconUpdated_iconAlreadySet_loadsNewIconnull189     fun onIconUpdated_iconAlreadySet_loadsNewIcon() =
190         testScope.runTest {
191             // GIVEN an icon is set
192             iconManager.updateIcon(mockConsumer, supportedIcon).run()
193             clearInvocations(mockConsumer)
194 
195             // WHEN a new icon is set
196             iconManager.updateIcon(mockConsumer, unsupportedIcon).run()
197 
198             // THEN consumer is updated with the new image
199             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
200             assertIsFullImage(drawableCaptor.value)
201             assertSize(drawableCaptor.value)
202         }
203 
204     @Test
onIconUpdated_iconAlreadySetForTheSameIcon_loadsIconAgainnull205     fun onIconUpdated_iconAlreadySetForTheSameIcon_loadsIconAgain() =
206         testScope.runTest {
207             // GIVEN an icon is set
208             iconManager.updateIcon(mockConsumer, supportedIcon).run()
209             // AND the view is shown
210             iconManager.onViewShown(true)
211             runCurrent()
212             clearInvocations(mockConsumer)
213             // WHEN the icon is set again
214             iconManager.updateIcon(mockConsumer, supportedIcon).run()
215 
216             // THEN consumer is updated with the new image
217             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
218             assertIsFullImage(drawableCaptor.value)
219             assertSize(drawableCaptor.value)
220         }
221 
222     @Test
onIconUpdated_iconAlreadySetForUnsupportedIcon_loadsNewIconnull223     fun onIconUpdated_iconAlreadySetForUnsupportedIcon_loadsNewIcon() =
224         testScope.runTest {
225             // GIVEN an unsupported icon is set
226             iconManager.updateIcon(mockConsumer, unsupportedIcon).run()
227             // AND the view is shown
228             iconManager.onViewShown(true)
229             runCurrent()
230             clearInvocations(mockConsumer)
231 
232             // WHEN a new icon is set
233             iconManager.updateIcon(mockConsumer, supportedIcon).run()
234 
235             // THEN consumer is updated with the new image
236             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
237             assertIsFullImage(drawableCaptor.value)
238             assertSize(drawableCaptor.value)
239         }
240 
241     @Test
onIconUpdated_supportedTypeButTooWide_resizedPlaceholderLoadednull242     fun onIconUpdated_supportedTypeButTooWide_resizedPlaceholderLoaded() =
243         testScope.runTest {
244             // GIVEN the max width is smaller than our image
245             testableResources.addOverride(
246                 com.android.internal.R.dimen.notification_big_picture_max_width,
247                 20
248             )
249             iconManager.updateMaxImageSizes()
250 
251             // WHEN update with a supported icon
252             iconManager.updateIcon(mockConsumer, supportedIcon).run()
253 
254             // THEN consumer is updated with the resized placeholder
255             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
256             assertIsPlaceHolder(drawableCaptor.value)
257             assertSize(drawableCaptor.value, expectedWidth = 20, expectedHeight = 20)
258         }
259 
260     @Test
onIconUpdated_supportedTypeButTooHigh_resizedPlaceholderLoadednull261     fun onIconUpdated_supportedTypeButTooHigh_resizedPlaceholderLoaded() =
262         testScope.runTest {
263             // GIVEN the max height is smaller than our image
264             testableResources.addOverride(
265                 com.android.internal.R.dimen.notification_big_picture_max_height,
266                 20
267             )
268             iconManager.updateMaxImageSizes()
269 
270             // WHEN update with a supported icon
271             iconManager.updateIcon(mockConsumer, supportedIcon).run()
272 
273             // THEN consumer is updated with the resized placeholder
274             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
275             assertIsPlaceHolder(drawableCaptor.value)
276             assertSize(drawableCaptor.value, expectedWidth = 20, expectedHeight = 20)
277         }
278 
279     @Test
onViewShown_placeholderShowing_fullImageLoadednull280     fun onViewShown_placeholderShowing_fullImageLoaded() =
281         testScope.runTest {
282             // GIVEN placeholder is showing
283             iconManager.updateIcon(mockConsumer, supportedIcon).run()
284             clearInvocations(mockConsumer)
285 
286             // WHEN the view is shown
287             iconManager.onViewShown(true)
288             runCurrent()
289 
290             // THEN full image is set
291             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
292             assertIsFullImage(drawableCaptor.value)
293             assertSize(drawableCaptor.value)
294         }
295 
296     @Test
onViewHidden_fullImageShowing_placeHolderSetnull297     fun onViewHidden_fullImageShowing_placeHolderSet() =
298         testScope.runTest {
299             // GIVEN full image is showing and the view is shown
300             iconManager.updateIcon(mockConsumer, supportedIcon).run()
301             iconManager.onViewShown(true)
302             runCurrent()
303             clearInvocations(mockConsumer)
304 
305             // WHEN the view goes off the screen
306             iconManager.onViewShown(false)
307             // AND we wait a bit
308             advanceTimeBy(FREE_IMAGE_DELAY_MS)
309             runCurrent()
310 
311             // THEN placeholder is set
312             verify(mockConsumer).setImageDrawable(drawableCaptor.capture())
313             assertIsPlaceHolder(drawableCaptor.value)
314             assertSize(drawableCaptor.value)
315         }
316 
317     @Test
onViewShownToggled_viewShown_nothingHappensnull318     fun onViewShownToggled_viewShown_nothingHappens() =
319         testScope.runTest {
320             // GIVEN full image is showing and the view is shown
321             iconManager.updateIcon(mockConsumer, supportedIcon).run()
322             iconManager.onViewShown(true)
323             runCurrent()
324             clearInvocations(mockConsumer)
325 
326             // WHEN the onViewShown is toggled
327             iconManager.onViewShown(false)
328             runCurrent()
329             iconManager.onViewShown(true)
330             // AND we wait a bit
331             advanceTimeBy(FREE_IMAGE_DELAY_MS)
332             runCurrent()
333 
334             // THEN nothing happens
335             verifyZeroInteractions(mockConsumer)
336         }
337 
338     @Test
onViewShown_unsupportedIconLoaded_nothingHappensnull339     fun onViewShown_unsupportedIconLoaded_nothingHappens() =
340         testScope.runTest {
341             // GIVEN full image is showing for an unsupported icon
342             iconManager.updateIcon(mockConsumer, unsupportedIcon).run()
343             clearInvocations(mockConsumer)
344 
345             // WHEN the view is shown
346             iconManager.onViewShown(true)
347             runCurrent()
348 
349             // THEN nothing happens
350             verifyZeroInteractions(mockConsumer)
351         }
352 
353     @Test
onViewShown_nullSetForIcon_nothingHappensnull354     fun onViewShown_nullSetForIcon_nothingHappens() =
355         testScope.runTest {
356             // GIVEN null is set for the icon
357             iconManager.updateIcon(mockConsumer, null).run()
358             clearInvocations(mockConsumer)
359 
360             // WHEN the view is shown
361             iconManager.onViewShown(true)
362             runCurrent()
363 
364             // THEN nothing happens
365             verifyZeroInteractions(mockConsumer)
366         }
367 
368     @Test
onViewHidden_unsupportedIconLoadedAndViewIsShown_nothingHappensnull369     fun onViewHidden_unsupportedIconLoadedAndViewIsShown_nothingHappens() =
370         testScope.runTest {
371             // GIVEN full image is showing for an unsupported icon
372             iconManager.updateIcon(mockConsumer, unsupportedIcon).run()
373             // AND the view is shown
374             iconManager.onViewShown(true)
375             runCurrent()
376             clearInvocations(mockConsumer)
377 
378             // WHEN the view goes off the screen
379             iconManager.onViewShown(false)
380             // AND we wait a bit
381             advanceTimeBy(FREE_IMAGE_DELAY_MS)
382             runCurrent()
383 
384             // THEN nothing happens
385             verifyZeroInteractions(mockConsumer)
386         }
387 
388     @Test
onViewHidden_placeholderShowing_nothingHappensnull389     fun onViewHidden_placeholderShowing_nothingHappens() =
390         testScope.runTest {
391             // GIVEN placeholder image is showing
392             iconManager.updateIcon(mockConsumer, supportedIcon).run()
393             clearInvocations(mockConsumer)
394 
395             // WHEN the view is hidden
396             iconManager.onViewShown(false)
397             // AND we wait a bit
398             advanceTimeBy(FREE_IMAGE_DELAY_MS)
399             runCurrent()
400 
401             // THEN nothing happens
402             verifyZeroInteractions(mockConsumer)
403         }
404 
405     @Test
onViewShown_alreadyShowing_nothingHappensnull406     fun onViewShown_alreadyShowing_nothingHappens() =
407         testScope.runTest {
408             // GIVEN full image is showing and the view is shown
409             iconManager.updateIcon(mockConsumer, supportedIcon).run()
410             iconManager.onViewShown(true)
411             runCurrent()
412             clearInvocations(mockConsumer)
413 
414             // WHEN view shown called again
415             iconManager.onViewShown(true)
416             runCurrent()
417 
418             // THEN nothing happens
419             verifyZeroInteractions(mockConsumer)
420         }
421 
422     @Test
onViewHidden_alreadyHidden_nothingHappensnull423     fun onViewHidden_alreadyHidden_nothingHappens() =
424         testScope.runTest {
425             // GIVEN placeholder image is showing and the view is hidden
426             iconManager.updateIcon(mockConsumer, supportedIcon).run()
427             iconManager.onViewShown(false)
428             advanceTimeBy(FREE_IMAGE_DELAY_MS)
429             runCurrent()
430             clearInvocations(mockConsumer)
431 
432             // WHEN the view is hidden again
433             iconManager.onViewShown(false)
434             // AND we wait a bit
435             advanceTimeBy(FREE_IMAGE_DELAY_MS)
436             runCurrent()
437 
438             // THEN nothing happens
439             verifyZeroInteractions(mockConsumer)
440         }
441 
442     @Test
cancelJobs_freeImageJobRunning_jobCancellednull443     fun cancelJobs_freeImageJobRunning_jobCancelled() =
444         testScope.runTest {
445             // GIVEN full image is showing
446             iconManager.updateIcon(mockConsumer, supportedIcon).run()
447             iconManager.onViewShown(true)
448             runCurrent()
449             clearInvocations(mockConsumer)
450             // AND the view has just gone off the screen
451             iconManager.onViewShown(false)
452 
453             // WHEN cancelJobs is called
454             iconManager.cancelJobs()
455             // AND we wait a bit
456             advanceTimeBy(FREE_IMAGE_DELAY_MS)
457             runCurrent()
458 
459             // THEN no more updates are happening
460             verifyZeroInteractions(mockConsumer)
461         }
462 
overrideMaxImageSizesnull463     private fun overrideMaxImageSizes() {
464         testableResources.addOverride(
465             com.android.internal.R.dimen.notification_big_picture_max_width,
466             MAX_IMAGE_SIZE
467         )
468         testableResources.addOverride(
469             com.android.internal.R.dimen.notification_big_picture_max_height,
470             MAX_IMAGE_SIZE
471         )
472     }
473 
assertIsPlaceHoldernull474     private fun assertIsPlaceHolder(drawable: Drawable) {
475         assertThat(drawable).isInstanceOf(PlaceHolderDrawable::class.java)
476     }
477 
assertIsFullImagenull478     private fun assertIsFullImage(drawable: Drawable) {
479         assertThat(drawable).isInstanceOf(BitmapDrawable::class.java)
480     }
481 
assertSizenull482     private fun assertSize(
483         drawable: Drawable,
484         expectedWidth: Int = expectedDrawable.intrinsicWidth,
485         expectedHeight: Int = expectedDrawable.intrinsicHeight
486     ) {
487         assertThat(drawable.intrinsicWidth).isEqualTo(expectedWidth)
488         assertThat(drawable.intrinsicHeight).isEqualTo(expectedHeight)
489     }
490 }
491