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.app.StatusBarManager
21 import android.content.ComponentName
22 import android.content.DialogInterface
23 import android.graphics.drawable.Icon
24 import android.os.RemoteException
25 import androidx.test.ext.junit.runners.AndroidJUnit4
26 import androidx.test.filters.SmallTest
27 import com.android.internal.logging.InstanceId
28 import com.android.internal.statusbar.IAddTileResultCallback
29 import com.android.systemui.InstanceIdSequenceFake
30 import com.android.systemui.SysuiTestCase
31 import com.android.systemui.qs.QSHost
32 import com.android.systemui.statusbar.CommandQueue
33 import com.android.systemui.statusbar.commandline.CommandRegistry
34 import com.android.systemui.util.mockito.any
35 import com.android.systemui.util.mockito.capture
36 import com.android.systemui.util.mockito.eq
37 import com.google.common.truth.Truth.assertThat
38 import java.util.function.Consumer
39 import org.junit.Before
40 import org.junit.Test
41 import org.junit.runner.RunWith
42 import org.mockito.ArgumentCaptor
43 import org.mockito.Mock
44 import org.mockito.Mockito.anyBoolean
45 import org.mockito.Mockito.anyInt
46 import org.mockito.Mockito.anyString
47 import org.mockito.Mockito.atLeastOnce
48 import org.mockito.Mockito.never
49 import org.mockito.Mockito.verify
50 import org.mockito.Mockito.`when`
51 import org.mockito.MockitoAnnotations
52 
53 @SmallTest
54 @RunWith(AndroidJUnit4::class)
55 class TileServiceRequestControllerTest : SysuiTestCase() {
56 
57     companion object {
58         private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls")
59         private const val TEST_APP_NAME = "App"
60         private const val TEST_LABEL = "Label"
61         private const val TEST_UID = 12345
62     }
63 
64     @Mock
65     private lateinit var tileRequestDialog: TileRequestDialog
66     @Mock
67     private lateinit var qsHost: QSHost
68     @Mock
69     private lateinit var commandRegistry: CommandRegistry
70     @Mock
71     private lateinit var commandQueue: CommandQueue
72     @Mock
73     private lateinit var logger: TileRequestDialogEventLogger
74     @Mock
75     private lateinit var icon: Icon
76     @Mock
77     private lateinit var ugm: IUriGrantsManager
78 
79     private val instanceIdSequence = InstanceIdSequenceFake(1_000)
80     private lateinit var controller: TileServiceRequestController
81 
82     @Before
setUpnull83     fun setUp() {
84         MockitoAnnotations.initMocks(this)
85 
86         `when`(logger.newInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
87 
88         // Tile not present by default
89         `when`(qsHost.indexOf(anyString())).thenReturn(-1)
90 
91         controller = TileServiceRequestController(
92                 qsHost,
93                 commandQueue,
94                 commandRegistry,
95                 logger,
96                 ugm,
97         ) {
98             tileRequestDialog
99         }
100 
101         controller.init()
102     }
103 
104     @Test
requestTileAdd_dataIsPassedToDialognull105     fun requestTileAdd_dataIsPassedToDialog() {
106         controller.requestTileAdd(
107                 TEST_UID,
108                 TEST_COMPONENT,
109                 TEST_APP_NAME,
110                 TEST_LABEL,
111                 icon,
112                 Callback(),
113         )
114 
115         verify(tileRequestDialog).setTileData(
116                 TileRequestDialog.TileData(
117                         TEST_UID,
118                         TEST_APP_NAME,
119                         TEST_LABEL,
120                         icon,
121                         TEST_COMPONENT.packageName,
122                 ),
123                 ugm,
124         )
125     }
126 
127     @Test
tileAlreadyAdded_correctResultnull128     fun tileAlreadyAdded_correctResult() {
129         `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
130 
131         val callback = Callback()
132         controller.requestTileAdd(
133                 TEST_UID,
134                 TEST_COMPONENT,
135                 TEST_APP_NAME,
136                 TEST_LABEL,
137                 icon,
138                 callback,
139         )
140 
141         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.TILE_ALREADY_ADDED)
142         verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
143     }
144 
145     @Test
tileAlreadyAdded_loggednull146     fun tileAlreadyAdded_logged() {
147         `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
148 
149         controller.requestTileAdd(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
150 
151         verify(logger).logTileAlreadyAdded(eq<String>(TEST_COMPONENT.packageName), any())
152         verify(logger, never()).logDialogShown(anyString(), any())
153         verify(logger, never()).logUserResponse(anyInt(), anyString(), any())
154     }
155 
156     @Test
showAllUsers_setnull157     fun showAllUsers_set() {
158         controller.requestTileAdd(
159                 TEST_UID,
160                 TEST_COMPONENT,
161                 TEST_APP_NAME,
162                 TEST_LABEL,
163                 icon,
164                 Callback(),
165         )
166         verify(tileRequestDialog).setShowForAllUsers(true)
167     }
168 
169     @Test
cancelOnTouchOutside_setnull170     fun cancelOnTouchOutside_set() {
171         controller.requestTileAdd(
172                 TEST_UID,
173                 TEST_COMPONENT,
174                 TEST_APP_NAME,
175                 TEST_LABEL,
176                 icon,
177                 Callback(),
178         )
179         verify(tileRequestDialog).setCanceledOnTouchOutside(true)
180     }
181 
182     @Test
dialogShown_loggednull183     fun dialogShown_logged() {
184         controller.requestTileAdd(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
185 
186         verify(logger).logDialogShown(eq<String>(TEST_COMPONENT.packageName), any())
187     }
188 
189     @Test
cancelListener_dismissResultnull190     fun cancelListener_dismissResult() {
191         val cancelListenerCaptor =
192                 ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
193 
194         val callback = Callback()
195         controller.requestTileAdd(
196                 TEST_UID,
197                 TEST_COMPONENT,
198                 TEST_APP_NAME,
199                 TEST_LABEL,
200                 icon,
201                 callback,
202         )
203         verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
204 
205         cancelListenerCaptor.value.onCancel(tileRequestDialog)
206         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DISMISSED)
207         verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
208     }
209 
210     @Test
dialogCancelled_loggednull211     fun dialogCancelled_logged() {
212         val cancelListenerCaptor =
213                 ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
214 
215         controller.requestTileAdd(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
216         val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
217 
218         verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
219         verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
220 
221         cancelListenerCaptor.value.onCancel(tileRequestDialog)
222         verify(logger).logUserResponse(
223                 StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED,
224                 TEST_COMPONENT.packageName,
225                 instanceId
226         )
227     }
228 
229     @Test
positiveActionListener_tileAddedResultnull230     fun positiveActionListener_tileAddedResult() {
231         val clickListenerCaptor =
232                 ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
233 
234         val callback = Callback()
235         controller.requestTileAdd(
236                 TEST_UID,
237                 TEST_COMPONENT,
238                 TEST_APP_NAME,
239                 TEST_LABEL,
240                 icon,
241                 callback,
242         )
243         verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor))
244 
245         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
246 
247         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.ADD_TILE)
248         verify(qsHost).addTile(TEST_COMPONENT, /* end */ true)
249     }
250 
251     @Test
tileAdded_loggednull252     fun tileAdded_logged() {
253         val clickListenerCaptor =
254                 ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
255 
256         controller.requestTileAdd(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
257         val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
258 
259         verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor))
260         verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
261 
262         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
263         verify(logger).logUserResponse(
264                 StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED,
265                 TEST_COMPONENT.packageName,
266                 instanceId
267         )
268     }
269 
270     @Test
negativeActionListener_tileNotAddedResultnull271     fun negativeActionListener_tileNotAddedResult() {
272         val clickListenerCaptor =
273                 ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
274 
275         val callback = Callback()
276         controller.requestTileAdd(
277                 TEST_UID,
278                 TEST_COMPONENT,
279                 TEST_APP_NAME,
280                 TEST_LABEL,
281                 icon,
282                 callback,
283         )
284         verify(tileRequestDialog).setNegativeButton(anyInt(), capture(clickListenerCaptor))
285 
286         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_NEGATIVE)
287 
288         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DONT_ADD_TILE)
289         verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
290     }
291 
292     @Test
tileNotAdded_loggednull293     fun tileNotAdded_logged() {
294         val clickListenerCaptor =
295                 ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
296 
297         controller.requestTileAdd(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
298         val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
299 
300         verify(tileRequestDialog).setNegativeButton(anyInt(), capture(clickListenerCaptor))
301         verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
302 
303         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_NEGATIVE)
304         verify(logger).logUserResponse(
305                 StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
306                 TEST_COMPONENT.packageName,
307                 instanceId
308         )
309     }
310 
311     @Test
commandQueueCallback_registerednull312     fun commandQueueCallback_registered() {
313         verify(commandQueue).addCallback(any())
314     }
315 
316     @Test
commandQueueCallback_dataPassedToDialognull317     fun commandQueueCallback_dataPassedToDialog() {
318         val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
319         verify(commandQueue, atLeastOnce()).addCallback(capture(captor))
320 
321         captor.value.requestAddTile(
322                 TEST_UID,
323                 TEST_COMPONENT,
324                 TEST_APP_NAME,
325                 TEST_LABEL,
326                 icon,
327                 Callback(),
328         )
329 
330         verify(tileRequestDialog).setTileData(
331                 TileRequestDialog.TileData(
332                         TEST_UID,
333                         TEST_APP_NAME,
334                         TEST_LABEL,
335                         icon,
336                         TEST_COMPONENT.packageName,
337                 ),
338                 ugm,
339         )
340     }
341 
342     @Test
commandQueueCallback_callbackCallednull343     fun commandQueueCallback_callbackCalled() {
344         `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
345         val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
346         verify(commandQueue, atLeastOnce()).addCallback(capture(captor))
347         val c = Callback()
348 
349         captor.value.requestAddTile(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, c)
350 
351         assertThat(c.lastAccepted).isEqualTo(TileServiceRequestController.TILE_ALREADY_ADDED)
352     }
353 
354     @Test
interfaceThrowsRemoteException_doesntCrashnull355     fun interfaceThrowsRemoteException_doesntCrash() {
356         val cancelListenerCaptor =
357                 ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
358         val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
359         verify(commandQueue, atLeastOnce()).addCallback(capture(captor))
360 
361         val callback = object : IAddTileResultCallback.Stub() {
362             override fun onTileRequest(p0: Int) {
363                 throw RemoteException()
364             }
365         }
366         captor.value.requestAddTile(
367                 TEST_UID,
368                 TEST_COMPONENT,
369                 TEST_APP_NAME,
370                 TEST_LABEL,
371                 icon,
372                 callback,
373         )
374         verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
375 
376         cancelListenerCaptor.value.onCancel(tileRequestDialog)
377     }
378 
379     @Test
testDismissDialogResponsenull380     fun testDismissDialogResponse() {
381         val dismissListenerCaptor =
382             ArgumentCaptor.forClass(DialogInterface.OnDismissListener::class.java)
383 
384         val callback = Callback()
385         controller.requestTileAdd(
386                 TEST_UID,
387                 TEST_COMPONENT,
388                 TEST_APP_NAME,
389                 TEST_LABEL,
390                 icon,
391                 callback,
392         )
393         verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor))
394 
395         dismissListenerCaptor.value.onDismiss(tileRequestDialog)
396         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DISMISSED)
397     }
398 
399     @Test
addTileAndThenDismissSendsOnlyAddTilenull400     fun addTileAndThenDismissSendsOnlyAddTile() {
401         // After clicking, the dialog is dismissed. This tests that only one response
402         // is sent (the first one)
403         val dismissListenerCaptor =
404             ArgumentCaptor.forClass(DialogInterface.OnDismissListener::class.java)
405         val clickListenerCaptor =
406             ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
407 
408         val callback = Callback()
409         controller.requestTileAdd(
410                 TEST_UID,
411                 TEST_COMPONENT,
412                 TEST_APP_NAME,
413                 TEST_LABEL,
414                 icon,
415                 callback,
416         )
417         verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor))
418         verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor))
419 
420         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
421         dismissListenerCaptor.value.onDismiss(tileRequestDialog)
422 
423         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.ADD_TILE)
424         assertThat(callback.timesCalled).isEqualTo(1)
425     }
426 
427     @Test
cancelAndThenDismissSendsOnlyOncenull428     fun cancelAndThenDismissSendsOnlyOnce() {
429         // After cancelling, the dialog is dismissed. This tests that only one response
430         // is sent.
431         val dismissListenerCaptor =
432             ArgumentCaptor.forClass(DialogInterface.OnDismissListener::class.java)
433         val cancelListenerCaptor =
434             ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
435 
436         val callback = Callback()
437         controller.requestTileAdd(
438                 TEST_UID,
439                 TEST_COMPONENT,
440                 TEST_APP_NAME,
441                 TEST_LABEL,
442                 icon,
443                 callback,
444         )
445         verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
446         verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor))
447 
448         cancelListenerCaptor.value.onCancel(tileRequestDialog)
449         dismissListenerCaptor.value.onDismiss(tileRequestDialog)
450 
451         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DISMISSED)
452         assertThat(callback.timesCalled).isEqualTo(1)
453     }
454 
455     private class Callback : IAddTileResultCallback.Stub(), Consumer<Int> {
456         var lastAccepted: Int? = null
457             private set
458 
459         var timesCalled = 0
460             private set
461 
acceptnull462         override fun accept(t: Int) {
463             lastAccepted = t
464             timesCalled++
465         }
466 
onTileRequestnull467         override fun onTileRequest(r: Int) {
468             accept(r)
469         }
470     }
471 }
472