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