1 /*
2  * Copyright (C) 2021 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.qs.external
18 
19 import android.app.IUriGrantsManager
20 import android.graphics.Bitmap
21 import android.graphics.Canvas
22 import android.graphics.Color
23 import android.graphics.drawable.Drawable
24 import android.graphics.drawable.Icon
25 import android.testing.TestableLooper
26 import android.view.ViewGroup
27 import android.widget.ImageView
28 import android.widget.TextView
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.SmallTest
31 import com.android.systemui.SysuiTestCase
32 import com.android.systemui.plugins.qs.QSTileView
33 import com.android.systemui.res.R
34 import com.android.systemui.util.mockito.any
35 import com.android.systemui.util.mockito.eq
36 import com.android.systemui.util.mockito.whenever
37 import com.google.common.truth.Truth.assertThat
38 import java.util.Arrays
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.ArgumentMatchers.anyInt
44 import org.mockito.ArgumentMatchers.anyString
45 import org.mockito.Mock
46 import org.mockito.Mockito
47 import org.mockito.Mockito.verify
48 import org.mockito.MockitoAnnotations
49 
50 @SmallTest
51 @RunWith(AndroidJUnit4::class)
52 @TestableLooper.RunWithLooper
53 class TileRequestDialogTest : SysuiTestCase() {
54 
55     companion object {
56         private const val APP_NAME = "App name"
57         private const val LABEL = "Label"
58         private const val PACKAGE = "package"
59         private const val UID = 12345
60         private val DEFAULT_ICON = R.drawable.android
61     }
62 
63     private lateinit var dialog: TileRequestDialog
64 
65     @Mock private lateinit var ugm: IUriGrantsManager
66 
67     @Before
setUpnull68     fun setUp() {
69         MockitoAnnotations.initMocks(this)
70         // Create in looper so we can make sure that the tile is fully updated
71         TestableLooper.get(this).runWithLooper { dialog = TileRequestDialog(mContext) }
72     }
73 
74     @After
teardownnull75     fun teardown() {
76         if (this::dialog.isInitialized) {
77             dialog.dismiss()
78         }
79     }
80 
81     @Test
setTileData_hasCorrectViewsnull82     fun setTileData_hasCorrectViews() {
83         val icon = Icon.createWithResource(mContext, R.drawable.cloud)
84         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
85 
86         dialog.setTileData(tileData, ugm)
87         dialog.show()
88 
89         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
90 
91         assertThat(content.childCount).isEqualTo(2)
92         assertThat(content.getChildAt(0)).isInstanceOf(TextView::class.java)
93         assertThat(content.getChildAt(1)).isInstanceOf(QSTileView::class.java)
94     }
95 
96     @Test
setTileData_hasCorrectAppNamenull97     fun setTileData_hasCorrectAppName() {
98         val icon = Icon.createWithResource(mContext, R.drawable.cloud)
99         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
100 
101         dialog.setTileData(tileData, ugm)
102         dialog.show()
103 
104         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
105         val text = content.getChildAt(0) as TextView
106         assertThat(text.text.toString()).contains(APP_NAME)
107     }
108 
109     @Test
setTileData_hasCorrectLabelnull110     fun setTileData_hasCorrectLabel() {
111         val icon = Icon.createWithResource(mContext, R.drawable.cloud)
112         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
113 
114         dialog.setTileData(tileData, ugm)
115         dialog.show()
116 
117         TestableLooper.get(this).processAllMessages()
118 
119         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
120         val tile = content.getChildAt(1) as QSTileView
121         assertThat((tile.label as TextView).text.toString()).isEqualTo(LABEL)
122     }
123 
124     @Test
setTileData_hasIconnull125     fun setTileData_hasIcon() {
126         val icon = Icon.createWithResource(mContext, R.drawable.cloud)
127         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
128 
129         dialog.setTileData(tileData, ugm)
130         dialog.show()
131 
132         TestableLooper.get(this).processAllMessages()
133 
134         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
135         val tile = content.getChildAt(1) as QSTileView
136         assertThat((tile.icon.iconView as ImageView).drawable).isNotNull()
137     }
138 
139     @Test
setTileData_nullIcon_hasIconnull140     fun setTileData_nullIcon_hasIcon() {
141         val tileData = TileData(UID, APP_NAME, LABEL, null, PACKAGE)
142 
143         dialog.setTileData(tileData, ugm)
144         dialog.show()
145 
146         TestableLooper.get(this).processAllMessages()
147 
148         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
149         val tile = content.getChildAt(1) as QSTileView
150         assertThat((tile.icon.iconView as ImageView).drawable).isNotNull()
151     }
152 
153     @Test
setTileData_hasNoStateDescriptionnull154     fun setTileData_hasNoStateDescription() {
155         val icon = Icon.createWithResource(mContext, R.drawable.cloud)
156         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
157 
158         dialog.setTileData(tileData, ugm)
159         dialog.show()
160 
161         TestableLooper.get(this).processAllMessages()
162 
163         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
164         val tile = content.getChildAt(1) as QSTileView
165 
166         assertThat(tile.stateDescription).isEqualTo("")
167     }
168 
169     @Test
setTileData_tileNotClickablenull170     fun setTileData_tileNotClickable() {
171         val icon = Icon.createWithResource(mContext, R.drawable.cloud)
172         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
173 
174         dialog.setTileData(tileData, ugm)
175         dialog.show()
176 
177         TestableLooper.get(this).processAllMessages()
178 
179         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
180         val tile = content.getChildAt(1) as QSTileView
181 
182         assertThat(tile.isClickable).isFalse()
183         assertThat(tile.isLongClickable).isFalse()
184     }
185 
186     @Test
setTileData_tileHasCorrectContentDescriptionnull187     fun setTileData_tileHasCorrectContentDescription() {
188         val icon = Icon.createWithResource(mContext, R.drawable.cloud)
189         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
190 
191         dialog.setTileData(tileData, ugm)
192         dialog.show()
193 
194         TestableLooper.get(this).processAllMessages()
195 
196         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
197         val tile = content.getChildAt(1) as QSTileView
198 
199         assertThat(tile.contentDescription).isEqualTo(LABEL)
200     }
201 
202     @Test
uriIconLoadSuccess_correctIconnull203     fun uriIconLoadSuccess_correctIcon() {
204         val tintColor = Color.BLACK
205         val icon = Mockito.mock(Icon::class.java)
206         val drawable = context.getDrawable(R.drawable.cloud)!!.apply { setTint(tintColor) }
207         whenever(icon.loadDrawable(any())).thenReturn(drawable)
208         whenever(icon.loadDrawableCheckingUriGrant(any(), eq(ugm), anyInt(), anyString()))
209             .thenReturn(drawable)
210 
211         val size = 100
212 
213         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
214 
215         dialog.setTileData(tileData, ugm)
216         dialog.show()
217 
218         TestableLooper.get(this).processAllMessages()
219 
220         verify(icon).loadDrawableCheckingUriGrant(any(), eq(ugm), eq(UID), eq(PACKAGE))
221 
222         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
223         val tile = content.getChildAt(1) as QSTileView
224 
225         val iconDrawable = (tile.icon.iconView as ImageView).drawable.apply { setTint(tintColor) }
226 
227         assertThat(areDrawablesEqual(iconDrawable, drawable, size)).isTrue()
228     }
229 
230     @Test
uriIconLoadFail_defaultIconnull231     fun uriIconLoadFail_defaultIcon() {
232         val tintColor = Color.BLACK
233         val icon = Mockito.mock(Icon::class.java)
234         val drawable = context.getDrawable(R.drawable.cloud)!!.apply { setTint(tintColor) }
235         whenever(icon.loadDrawable(any())).thenReturn(drawable)
236         whenever(icon.loadDrawableCheckingUriGrant(any(), eq(ugm), anyInt(), anyString()))
237             .thenReturn(null)
238 
239         val size = 100
240 
241         val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)
242 
243         dialog.setTileData(tileData, ugm)
244         dialog.show()
245 
246         TestableLooper.get(this).processAllMessages()
247 
248         verify(icon).loadDrawableCheckingUriGrant(any(), eq(ugm), eq(UID), eq(PACKAGE))
249 
250         val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
251         val tile = content.getChildAt(1) as QSTileView
252 
253         val iconDrawable = (tile.icon.iconView as ImageView).drawable.apply { setTint(tintColor) }
254 
255         val defaultIcon = context.getDrawable(DEFAULT_ICON)!!.apply { setTint(tintColor) }
256 
257         assertThat(areDrawablesEqual(iconDrawable, defaultIcon, size)).isTrue()
258     }
259 }
260 
areDrawablesEqualnull261 private fun areDrawablesEqual(drawable1: Drawable, drawable2: Drawable, size: Int = 24): Boolean {
262     val bm1 = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
263     val bm2 = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
264 
265     val canvas1 = Canvas(bm1)
266     val canvas2 = Canvas(bm2)
267 
268     drawable1.setBounds(0, 0, size, size)
269     drawable2.setBounds(0, 0, size, size)
270 
271     drawable1.draw(canvas1)
272     drawable2.draw(canvas2)
273 
274     return equalBitmaps(bm1, bm2).also {
275         bm1.recycle()
276         bm2.recycle()
277     }
278 }
279 
equalBitmapsnull280 private fun equalBitmaps(a: Bitmap, b: Bitmap): Boolean {
281     if (a.width != b.width || a.height != b.height) return false
282     val w = a.width
283     val h = a.height
284     val aPix = IntArray(w * h)
285     val bPix = IntArray(w * h)
286     a.getPixels(aPix, 0, w, 0, 0, w, h)
287     b.getPixels(bPix, 0, w, 0, 0, w, h)
288     return Arrays.equals(aPix, bPix)
289 }
290