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