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.StatusBarManager
20 import android.content.ComponentName
21 import android.content.DialogInterface
22 import android.graphics.drawable.Icon
23 import android.os.RemoteException
24 import android.testing.AndroidTestingRunner
25 import androidx.test.filters.SmallTest
26 import com.android.internal.logging.InstanceId
27 import com.android.internal.statusbar.IAddTileResultCallback
28 import com.android.systemui.InstanceIdSequenceFake
29 import com.android.systemui.SysuiTestCase
30 import com.android.systemui.qs.QSHost
31 import com.android.systemui.statusbar.CommandQueue
32 import com.android.systemui.statusbar.commandline.CommandRegistry
33 import com.android.systemui.util.mockito.any
34 import com.android.systemui.util.mockito.capture
35 import com.android.systemui.util.mockito.eq
36 import com.google.common.truth.Truth.assertThat
37 import java.util.function.Consumer
38 import org.junit.Before
39 import org.junit.Test
40 import org.junit.runner.RunWith
41 import org.mockito.ArgumentCaptor
42 import org.mockito.Mock
43 import org.mockito.Mockito.anyBoolean
44 import org.mockito.Mockito.anyInt
45 import org.mockito.Mockito.anyString
46 import org.mockito.Mockito.atLeastOnce
47 import org.mockito.Mockito.never
48 import org.mockito.Mockito.verify
49 import org.mockito.Mockito.`when`
50 import org.mockito.MockitoAnnotations
51 
52 @SmallTest
53 @RunWith(AndroidTestingRunner::class)
54 class TileServiceRequestControllerTest : SysuiTestCase() {
55 
56     companion object {
57         private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls")
58         private const val TEST_APP_NAME = "App"
59         private const val TEST_LABEL = "Label"
60     }
61 
62     @Mock
63     private lateinit var tileRequestDialog: TileRequestDialog
64     @Mock
65     private lateinit var qsHost: QSHost
66     @Mock
67     private lateinit var commandRegistry: CommandRegistry
68     @Mock
69     private lateinit var commandQueue: CommandQueue
70     @Mock
71     private lateinit var logger: TileRequestDialogEventLogger
72     @Mock
73     private lateinit var icon: Icon
74 
75     private val instanceIdSequence = InstanceIdSequenceFake(1_000)
76     private lateinit var controller: TileServiceRequestController
77 
78     @Before
79     fun setUp() {
80         MockitoAnnotations.initMocks(this)
81 
82         `when`(logger.newInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
83 
84         // Tile not present by default
85         `when`(qsHost.indexOf(anyString())).thenReturn(-1)
86 
87         controller = TileServiceRequestController(
88                 qsHost,
89                 commandQueue,
90                 commandRegistry,
91                 logger
92         ) {
93             tileRequestDialog
94         }
95 
96         controller.init()
97     }
98 
99     @Test
100     fun requestTileAdd_dataIsPassedToDialog() {
101         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback())
102 
103         verify(tileRequestDialog).setTileData(
104                 TileRequestDialog.TileData(TEST_APP_NAME, TEST_LABEL, icon)
105         )
106     }
107 
108     @Test
109     fun tileAlreadyAdded_correctResult() {
110         `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
111 
112         val callback = Callback()
113         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
114 
115         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.TILE_ALREADY_ADDED)
116         verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
117     }
118 
119     @Test
120     fun tileAlreadyAdded_logged() {
121         `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
122 
123         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
124 
125         verify(logger).logTileAlreadyAdded(eq<String>(TEST_COMPONENT.packageName), any())
126         verify(logger, never()).logDialogShown(anyString(), any())
127         verify(logger, never()).logUserResponse(anyInt(), anyString(), any())
128     }
129 
130     @Test
131     fun showAllUsers_set() {
132         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback())
133         verify(tileRequestDialog).setShowForAllUsers(true)
134     }
135 
136     @Test
137     fun cancelOnTouchOutside_set() {
138         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback())
139         verify(tileRequestDialog).setCanceledOnTouchOutside(true)
140     }
141 
142     @Test
143     fun dialogShown_logged() {
144         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
145 
146         verify(logger).logDialogShown(eq<String>(TEST_COMPONENT.packageName), any())
147     }
148 
149     @Test
150     fun cancelListener_dismissResult() {
151         val cancelListenerCaptor =
152                 ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
153 
154         val callback = Callback()
155         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
156         verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
157 
158         cancelListenerCaptor.value.onCancel(tileRequestDialog)
159         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DISMISSED)
160         verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
161     }
162 
163     @Test
164     fun dialogCancelled_logged() {
165         val cancelListenerCaptor =
166                 ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
167 
168         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
169         val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
170 
171         verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
172         verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
173 
174         cancelListenerCaptor.value.onCancel(tileRequestDialog)
175         verify(logger).logUserResponse(
176                 StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED,
177                 TEST_COMPONENT.packageName,
178                 instanceId
179         )
180     }
181 
182     @Test
183     fun positiveActionListener_tileAddedResult() {
184         val clickListenerCaptor =
185                 ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
186 
187         val callback = Callback()
188         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
189         verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor))
190 
191         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
192 
193         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.ADD_TILE)
194         verify(qsHost).addTile(TEST_COMPONENT, /* end */ true)
195     }
196 
197     @Test
198     fun tileAdded_logged() {
199         val clickListenerCaptor =
200                 ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
201 
202         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
203         val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
204 
205         verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor))
206         verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
207 
208         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
209         verify(logger).logUserResponse(
210                 StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED,
211                 TEST_COMPONENT.packageName,
212                 instanceId
213         )
214     }
215 
216     @Test
217     fun negativeActionListener_tileNotAddedResult() {
218         val clickListenerCaptor =
219                 ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
220 
221         val callback = Callback()
222         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
223         verify(tileRequestDialog).setNegativeButton(anyInt(), capture(clickListenerCaptor))
224 
225         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_NEGATIVE)
226 
227         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DONT_ADD_TILE)
228         verify(qsHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
229     }
230 
231     @Test
232     fun tileNotAdded_logged() {
233         val clickListenerCaptor =
234                 ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
235 
236         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
237         val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
238 
239         verify(tileRequestDialog).setNegativeButton(anyInt(), capture(clickListenerCaptor))
240         verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
241 
242         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_NEGATIVE)
243         verify(logger).logUserResponse(
244                 StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
245                 TEST_COMPONENT.packageName,
246                 instanceId
247         )
248     }
249 
250     @Test
251     fun commandQueueCallback_registered() {
252         verify(commandQueue).addCallback(any())
253     }
254 
255     @Test
256     fun commandQueueCallback_dataPassedToDialog() {
257         val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
258         verify(commandQueue, atLeastOnce()).addCallback(capture(captor))
259 
260         captor.value.requestAddTile(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback())
261 
262         verify(tileRequestDialog).setTileData(
263                 TileRequestDialog.TileData(TEST_APP_NAME, TEST_LABEL, icon)
264         )
265     }
266 
267     @Test
268     fun commandQueueCallback_callbackCalled() {
269         `when`(qsHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
270         val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
271         verify(commandQueue, atLeastOnce()).addCallback(capture(captor))
272         val c = Callback()
273 
274         captor.value.requestAddTile(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, c)
275 
276         assertThat(c.lastAccepted).isEqualTo(TileServiceRequestController.TILE_ALREADY_ADDED)
277     }
278 
279     @Test
280     fun interfaceThrowsRemoteException_doesntCrash() {
281         val cancelListenerCaptor =
282                 ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
283         val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
284         verify(commandQueue, atLeastOnce()).addCallback(capture(captor))
285 
286         val callback = object : IAddTileResultCallback.Stub() {
287             override fun onTileRequest(p0: Int) {
288                 throw RemoteException()
289             }
290         }
291         captor.value.requestAddTile(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
292         verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
293 
294         cancelListenerCaptor.value.onCancel(tileRequestDialog)
295     }
296 
297     @Test
298     fun testDismissDialogResponse() {
299         val dismissListenerCaptor =
300             ArgumentCaptor.forClass(DialogInterface.OnDismissListener::class.java)
301 
302         val callback = Callback()
303         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
304         verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor))
305 
306         dismissListenerCaptor.value.onDismiss(tileRequestDialog)
307         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DISMISSED)
308     }
309 
310     @Test
311     fun addTileAndThenDismissSendsOnlyAddTile() {
312         // After clicking, the dialog is dismissed. This tests that only one response
313         // is sent (the first one)
314         val dismissListenerCaptor =
315             ArgumentCaptor.forClass(DialogInterface.OnDismissListener::class.java)
316         val clickListenerCaptor =
317             ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
318 
319         val callback = Callback()
320         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
321         verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor))
322         verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor))
323 
324         clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
325         dismissListenerCaptor.value.onDismiss(tileRequestDialog)
326 
327         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.ADD_TILE)
328         assertThat(callback.timesCalled).isEqualTo(1)
329     }
330 
331     @Test
332     fun cancelAndThenDismissSendsOnlyOnce() {
333         // After cancelling, the dialog is dismissed. This tests that only one response
334         // is sent.
335         val dismissListenerCaptor =
336             ArgumentCaptor.forClass(DialogInterface.OnDismissListener::class.java)
337         val cancelListenerCaptor =
338             ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
339 
340         val callback = Callback()
341         controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, callback)
342         verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
343         verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor))
344 
345         cancelListenerCaptor.value.onCancel(tileRequestDialog)
346         dismissListenerCaptor.value.onDismiss(tileRequestDialog)
347 
348         assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.DISMISSED)
349         assertThat(callback.timesCalled).isEqualTo(1)
350     }
351 
352     private class Callback : IAddTileResultCallback.Stub(), Consumer<Int> {
353         var lastAccepted: Int? = null
354             private set
355 
356         var timesCalled = 0
357             private set
358 
359         override fun accept(t: Int) {
360             lastAccepted = t
361             timesCalled++
362         }
363 
364         override fun onTileRequest(r: Int) {
365             accept(r)
366         }
367     }
368 }
369