1 package com.android.systemui.graphics
2 
3 import android.content.res.Resources
4 import android.graphics.Bitmap
5 import android.graphics.BitmapFactory
6 import android.graphics.ImageDecoder
7 import android.graphics.drawable.BitmapDrawable
8 import android.graphics.drawable.Drawable
9 import android.graphics.drawable.Icon
10 import android.graphics.drawable.VectorDrawable
11 import android.net.Uri
12 import androidx.test.ext.junit.runners.AndroidJUnit4
13 import androidx.test.filters.SmallTest
14 import com.android.systemui.R
15 import com.android.systemui.SysuiTestCase
16 import com.google.common.truth.Truth.assertThat
17 import java.io.ByteArrayInputStream
18 import java.io.ByteArrayOutputStream
19 import java.io.File
20 import java.io.FileInputStream
21 import java.io.FileOutputStream
22 import kotlinx.coroutines.test.TestScope
23 import kotlinx.coroutines.test.UnconfinedTestDispatcher
24 import kotlinx.coroutines.test.runTest
25 import org.junit.After
26 import org.junit.Before
27 import org.junit.Test
28 import org.junit.runner.RunWith
29 
30 @SmallTest
31 @kotlinx.coroutines.ExperimentalCoroutinesApi
32 @RunWith(AndroidJUnit4::class)
33 class ImageLoaderTest : SysuiTestCase() {
34 
35     private val testDispatcher = UnconfinedTestDispatcher()
36     private val testScope = TestScope(testDispatcher)
37     private val imageLoader = ImageLoader(context, testDispatcher)
38 
39     private lateinit var imgFile: File
40 
41     @Before
42     fun setUp() {
43         val context = context.createPackageContext("com.android.systemui.tests", 0)
44         val bitmap =
45             BitmapFactory.decodeResource(
46                 context.resources,
47                 com.android.systemui.tests.R.drawable.romainguy_rockaway
48             )
49 
50         imgFile = File.createTempFile("image", ".png", context.cacheDir)
51         imgFile.deleteOnExit()
52         bitmap.compress(Bitmap.CompressFormat.PNG, 100, FileOutputStream(imgFile))
53     }
54 
55     @After
56     fun tearDown() {
57         imgFile.delete()
58     }
59 
60     @Test
61     fun invalidResource_drawable_returnsNull() =
62         testScope.runTest { assertThat(imageLoader.loadDrawable(ImageLoader.Res(-1))).isNull() }
63 
64     @Test
65     fun invalidResource_bitmap_returnsNull() =
66         testScope.runTest { assertThat(imageLoader.loadBitmap(ImageLoader.Res(-1))).isNull() }
67 
68     @Test
69     fun invalidUri_returnsNull() =
70         testScope.runTest {
71             assertThat(imageLoader.loadBitmap(ImageLoader.Uri("this.is/bogus"))).isNull()
72         }
73 
74     @Test
75     fun invalidFile_returnsNull() =
76         testScope.runTest {
77             assertThat(imageLoader.loadBitmap(ImageLoader.File("this is broken!"))).isNull()
78         }
79 
80     @Test
81     fun invalidIcon_returnsNull() =
82         testScope.runTest {
83             assertThat(imageLoader.loadDrawable(Icon.createWithFilePath("this is broken"))).isNull()
84         }
85 
86     @Test
87     fun invalidIS_returnsNull() =
88         testScope.runTest {
89             assertThat(
90                     imageLoader.loadDrawable(
91                         ImageLoader.InputStream(ByteArrayInputStream(ByteArray(0)))
92                     )
93                 )
94                 .isNull()
95         }
96 
97     @Test
98     fun validBitmapResource_loadDrawable_returnsBitmapDrawable() =
99         testScope.runTest {
100             val context = context.createPackageContext("com.android.systemui.tests", 0)
101             val bitmap =
102                 BitmapFactory.decodeResource(
103                     context.resources,
104                     com.android.systemui.tests.R.drawable.romainguy_rockaway
105                 )
106             assertThat(bitmap).isNotNull()
107             val loadedDrawable =
108                 imageLoader.loadDrawable(
109                     ImageLoader.Res(
110                         com.android.systemui.tests.R.drawable.romainguy_rockaway,
111                         context
112                     )
113                 )
114             assertBitmapEqualToDrawable(loadedDrawable, bitmap)
115         }
116 
117     @Test
118     fun validBitmapResource_loadBitmap_returnsBitmapDrawable() =
119         testScope.runTest {
120             val bitmap =
121                 BitmapFactory.decodeResource(
122                     context.resources,
123                     R.drawable.dessert_zombiegingerbread
124                 )
125             val loadedBitmap =
126                 imageLoader.loadBitmap(ImageLoader.Res(R.drawable.dessert_zombiegingerbread))
127             assertBitmapEqualToBitmap(loadedBitmap, bitmap)
128         }
129 
130     @Test
131     fun validBitmapUri_returnsBitmapDrawable() =
132         testScope.runTest {
133             val bitmap =
134                 BitmapFactory.decodeResource(
135                     context.resources,
136                     R.drawable.dessert_zombiegingerbread
137                 )
138 
139             val uri =
140                 "android.resource://${context.packageName}/${R.drawable.dessert_zombiegingerbread}"
141             val loadedBitmap = imageLoader.loadBitmap(ImageLoader.Uri(uri))
142             assertBitmapEqualToBitmap(loadedBitmap, bitmap)
143         }
144 
145     @Test
146     fun validBitmapFile_returnsBitmapDrawable() =
147         testScope.runTest {
148             val bitmap = BitmapFactory.decodeFile(imgFile.absolutePath)
149             val loadedBitmap = imageLoader.loadBitmap(ImageLoader.File(imgFile))
150             assertBitmapEqualToBitmap(loadedBitmap, bitmap)
151         }
152 
153     @Test
154     fun validInputStream_returnsBitmapDrawable() =
155         testScope.runTest {
156             val bitmap = BitmapFactory.decodeFile(imgFile.absolutePath)
157             val loadedBitmap =
158                 imageLoader.loadBitmap(ImageLoader.InputStream(FileInputStream(imgFile)))
159             assertBitmapEqualToBitmap(loadedBitmap, bitmap)
160         }
161 
162     @Test
163     fun validBitmapIcon_returnsBitmapDrawable() =
164         testScope.runTest {
165             val bitmap =
166                 BitmapFactory.decodeResource(
167                     context.resources,
168                     R.drawable.dessert_zombiegingerbread
169                 )
170             val loadedDrawable = imageLoader.loadDrawable(Icon.createWithBitmap(bitmap))
171             assertBitmapEqualToDrawable(loadedDrawable, bitmap)
172         }
173 
174     @Test
175     fun validUriIcon_returnsBitmapDrawable() =
176         testScope.runTest {
177             val bitmap =
178                 BitmapFactory.decodeResource(
179                     context.resources,
180                     R.drawable.dessert_zombiegingerbread
181                 )
182             val uri =
183                 "android.resource://${context.packageName}/${R.drawable.dessert_zombiegingerbread}"
184             val loadedDrawable = imageLoader.loadDrawable(Icon.createWithContentUri(Uri.parse(uri)))
185             assertBitmapEqualToDrawable(loadedDrawable, bitmap)
186         }
187 
188     @Test
189     fun validDataIcon_returnsBitmapDrawable() =
190         testScope.runTest {
191             val bitmap =
192                 BitmapFactory.decodeResource(
193                     context.resources,
194                     R.drawable.dessert_zombiegingerbread
195                 )
196             val bos =
197                 ByteArrayOutputStream(
198                     bitmap.byteCount * 2
199                 ) // Compressed bitmap should be smaller than its source.
200             bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos)
201 
202             val array = bos.toByteArray()
203             val loadedDrawable = imageLoader.loadDrawable(Icon.createWithData(array, 0, array.size))
204             assertBitmapEqualToDrawable(loadedDrawable, bitmap)
205         }
206 
207     @Test
208     fun validSystemResourceIcon_returnsBitmapDrawable() =
209         testScope.runTest {
210             val bitmap =
211                 Resources.getSystem().getDrawable(android.R.drawable.ic_dialog_alert, context.theme)
212             val loadedDrawable =
213                 imageLoader.loadDrawable(
214                     Icon.createWithResource("android", android.R.drawable.ic_dialog_alert)
215                 )
216             assertBitmapEqualToDrawable(loadedDrawable, (bitmap as BitmapDrawable).bitmap)
217         }
218 
219     @Test
220     fun invalidDifferentPackageResourceIcon_returnsNull() =
221         testScope.runTest {
222             val loadedDrawable =
223                 imageLoader.loadDrawable(
224                     Icon.createWithResource(
225                         "noooope.wrong.package",
226                         R.drawable.dessert_zombiegingerbread
227                     )
228                 )
229             assertThat(loadedDrawable).isNull()
230         }
231 
232     @Test
233     fun validBitmapResource_widthMoreRestricted_downsizesKeepingAspectRatio() =
234         testScope.runTest {
235             val loadedDrawable =
236                 imageLoader.loadDrawable(ImageLoader.File(imgFile), maxWidth = 160, maxHeight = 160)
237             val loadedBitmap = assertBitmapInDrawable(loadedDrawable)
238             assertThat(loadedBitmap.width).isEqualTo(160)
239             assertThat(loadedBitmap.height).isEqualTo(106)
240         }
241 
242     @Test
243     fun validBitmapResource_heightMoreRestricted_downsizesKeepingAspectRatio() =
244         testScope.runTest {
245             val loadedDrawable =
246                 imageLoader.loadDrawable(ImageLoader.File(imgFile), maxWidth = 160, maxHeight = 50)
247             val loadedBitmap = assertBitmapInDrawable(loadedDrawable)
248             assertThat(loadedBitmap.width).isEqualTo(74)
249             assertThat(loadedBitmap.height).isEqualTo(50)
250         }
251 
252     @Test
253     fun validBitmapResource_onlyWidthRestricted_downsizesKeepingAspectRatio() =
254         testScope.runTest {
255             val loadedDrawable =
256                 imageLoader.loadDrawable(
257                     ImageLoader.File(imgFile),
258                     maxWidth = 160,
259                     maxHeight = ImageLoader.DO_NOT_RESIZE
260                 )
261             val loadedBitmap = assertBitmapInDrawable(loadedDrawable)
262             assertThat(loadedBitmap.width).isEqualTo(160)
263             assertThat(loadedBitmap.height).isEqualTo(106)
264         }
265 
266     @Test
267     fun validBitmapResource_onlyHeightRestricted_downsizesKeepingAspectRatio() =
268         testScope.runTest {
269             val loadedDrawable =
270                 imageLoader.loadDrawable(
271                     ImageLoader.Res(R.drawable.bubble_thumbnail),
272                     maxWidth = ImageLoader.DO_NOT_RESIZE,
273                     maxHeight = 120
274                 )
275             val loadedBitmap = assertBitmapInDrawable(loadedDrawable)
276             assertThat(loadedBitmap.width).isEqualTo(123)
277             assertThat(loadedBitmap.height).isEqualTo(120)
278         }
279 
280     @Test
281     fun validVectorDrawable_loadDrawable_successfullyLoaded() =
282         testScope.runTest {
283             val loadedDrawable = imageLoader.loadDrawable(ImageLoader.Res(R.drawable.ic_settings))
284             assertThat(loadedDrawable).isNotNull()
285             assertThat(loadedDrawable).isInstanceOf(VectorDrawable::class.java)
286         }
287 
288     @Test
289     fun validVectorDrawable_loadBitmap_returnsNull() =
290         testScope.runTest {
291             val loadedBitmap = imageLoader.loadBitmap(ImageLoader.Res(R.drawable.ic_settings))
292             assertThat(loadedBitmap).isNull()
293         }
294 
295     @Test
296     fun validVectorDrawableIcon_loadDrawable_successfullyLoaded() =
297         testScope.runTest {
298             val loadedDrawable =
299                 imageLoader.loadDrawable(Icon.createWithResource(context, R.drawable.ic_settings))
300             assertThat(loadedDrawable).isNotNull()
301             assertThat(loadedDrawable).isInstanceOf(VectorDrawable::class.java)
302         }
303 
304     @Test
305     fun hardwareAllocator_returnsHardwareBitmap() =
306         testScope.runTest {
307             val loadedDrawable =
308                 imageLoader.loadDrawable(
309                     ImageLoader.File(imgFile),
310                     allocator = ImageDecoder.ALLOCATOR_HARDWARE
311                 )
312             assertThat(loadedDrawable).isNotNull()
313             assertThat((loadedDrawable as BitmapDrawable).bitmap.config)
314                 .isEqualTo(Bitmap.Config.HARDWARE)
315         }
316 
317     @Test
318     fun softwareAllocator_returnsSoftwareBitmap() =
319         testScope.runTest {
320             val loadedDrawable =
321                 imageLoader.loadDrawable(
322                     ImageLoader.File(imgFile),
323                     allocator = ImageDecoder.ALLOCATOR_SOFTWARE
324                 )
325             assertThat(loadedDrawable).isNotNull()
326             assertThat((loadedDrawable as BitmapDrawable).bitmap.config)
327                 .isNotEqualTo(Bitmap.Config.HARDWARE)
328         }
329 
330     private fun assertBitmapInDrawable(drawable: Drawable?): Bitmap {
331         assertThat(drawable).isNotNull()
332         assertThat(drawable).isInstanceOf(BitmapDrawable::class.java)
333         return (drawable as BitmapDrawable).bitmap
334     }
335 
336     private fun assertBitmapEqualToDrawable(actual: Drawable?, expected: Bitmap) {
337         val actualBitmap = assertBitmapInDrawable(actual)
338         assertBitmapEqualToBitmap(actualBitmap, expected)
339     }
340 
341     private fun assertBitmapEqualToBitmap(actual: Bitmap?, expected: Bitmap) {
342         assertThat(actual).isNotNull()
343         assertThat(actual?.width).isEqualTo(expected.width)
344         assertThat(actual?.height).isEqualTo(expected.height)
345     }
346 }
347