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.tiles 18 19 import android.content.ComponentName 20 import android.content.Context 21 import android.content.Intent 22 import android.os.Handler 23 import android.provider.Settings 24 import android.service.quicksettings.Tile 25 import android.testing.TestableLooper 26 import androidx.lifecycle.LifecycleOwner 27 import androidx.test.ext.junit.runners.AndroidJUnit4 28 import androidx.test.filters.SmallTest 29 import com.android.internal.logging.MetricsLogger 30 import com.android.systemui.res.R 31 import com.android.systemui.SysuiTestCase 32 import com.android.systemui.animation.ActivityTransitionAnimator 33 import com.android.systemui.classifier.FalsingManagerFake 34 import com.android.systemui.controls.ControlsServiceInfo 35 import com.android.systemui.controls.controller.ControlInfo 36 import com.android.systemui.controls.controller.ControlsController 37 import com.android.systemui.controls.controller.StructureInfo 38 import com.android.systemui.controls.dagger.ControlsComponent 39 import com.android.systemui.controls.management.ControlsListingController 40 import com.android.systemui.controls.ui.ControlsActivity 41 import com.android.systemui.controls.ui.ControlsUiController 42 import com.android.systemui.controls.ui.SelectedItem 43 import com.android.systemui.plugins.ActivityStarter 44 import com.android.systemui.plugins.statusbar.StatusBarStateController 45 import com.android.systemui.qs.QSHost 46 import com.android.systemui.qs.QsEventLogger 47 import com.android.systemui.qs.logging.QSLogger 48 import com.android.systemui.qs.tileimpl.QSTileImpl 49 import com.android.systemui.util.mockito.any 50 import com.android.systemui.util.mockito.capture 51 import com.android.systemui.util.mockito.eq 52 import com.android.systemui.util.settings.FakeSettings 53 import com.android.systemui.util.settings.SecureSettings 54 import com.google.common.truth.Truth.assertThat 55 import org.junit.After 56 import org.junit.Before 57 import org.junit.Test 58 import org.junit.runner.RunWith 59 import org.mockito.ArgumentCaptor 60 import org.mockito.ArgumentMatchers.anyBoolean 61 import org.mockito.Captor 62 import org.mockito.Mock 63 import org.mockito.Mockito.doNothing 64 import org.mockito.Mockito.nullable 65 import org.mockito.Mockito.spy 66 import org.mockito.Mockito.verify 67 import org.mockito.Mockito.verifyZeroInteractions 68 import org.mockito.Mockito.`when` 69 import org.mockito.MockitoAnnotations 70 import java.util.Optional 71 72 @SmallTest 73 @RunWith(AndroidJUnit4::class) 74 @TestableLooper.RunWithLooper(setAsMainLooper = true) 75 class DeviceControlsTileTest : SysuiTestCase() { 76 77 @Mock 78 private lateinit var qsHost: QSHost 79 @Mock 80 private lateinit var metricsLogger: MetricsLogger 81 @Mock 82 private lateinit var statusBarStateController: StatusBarStateController 83 @Mock 84 private lateinit var activityStarter: ActivityStarter 85 @Mock 86 private lateinit var qsLogger: QSLogger 87 @Mock 88 private lateinit var controlsComponent: ControlsComponent 89 @Mock 90 private lateinit var controlsUiController: ControlsUiController 91 @Mock 92 private lateinit var controlsListingController: ControlsListingController 93 @Mock 94 private lateinit var controlsController: ControlsController 95 @Mock 96 private lateinit var serviceInfo: ControlsServiceInfo 97 @Mock 98 private lateinit var uiEventLogger: QsEventLogger 99 @Captor 100 private lateinit var listingCallbackCaptor: 101 ArgumentCaptor<ControlsListingController.ControlsListingCallback> 102 @Captor 103 private lateinit var intentCaptor: ArgumentCaptor<Intent> 104 105 private lateinit var testableLooper: TestableLooper 106 private lateinit var tile: DeviceControlsTile 107 108 private lateinit var secureSettings: SecureSettings 109 private lateinit var spiedContext: Context 110 private var featureEnabled = true 111 112 @Before setUpnull113 fun setUp() { 114 MockitoAnnotations.initMocks(this) 115 testableLooper = TestableLooper.get(this) 116 secureSettings = FakeSettings() 117 118 spiedContext = spy(mContext) 119 doNothing().`when`(spiedContext).startActivity(any(Intent::class.java)) 120 `when`(qsHost.context).thenReturn(spiedContext) 121 `when`(controlsComponent.isEnabled()).thenReturn(true) 122 `when`(controlsController.getPreferredSelection()) 123 .thenReturn(SelectedItem.StructureItem( 124 StructureInfo(ComponentName("pkg", "cls"), "structure", listOf()))) 125 secureSettings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 1) 126 127 setupControlsComponent() 128 129 tile = createTile() 130 } 131 132 @After tearDownnull133 fun tearDown() { 134 tile.destroy() 135 testableLooper.processAllMessages() 136 } 137 setupControlsComponentnull138 private fun setupControlsComponent() { 139 `when`(controlsComponent.getControlsController()).thenAnswer { 140 if (featureEnabled) { 141 Optional.of(controlsController) 142 } else { 143 Optional.empty() 144 } 145 } 146 147 `when`(controlsComponent.getControlsListingController()).thenAnswer { 148 if (featureEnabled) { 149 Optional.of(controlsListingController) 150 } else { 151 Optional.empty() 152 } 153 } 154 155 `when`(controlsComponent.getControlsUiController()).thenAnswer { 156 if (featureEnabled) { 157 Optional.of(controlsUiController) 158 } else { 159 Optional.empty() 160 } 161 } 162 163 `when`(controlsComponent.getTileTitleId()).thenReturn(R.string.quick_controls_title) 164 `when`(controlsComponent.getTileTitleId()).thenReturn(R.drawable.controls_icon) 165 } 166 167 @Test testAvailablenull168 fun testAvailable() { 169 assertThat(tile.isAvailable).isTrue() 170 } 171 172 @Test testNotAvailableControlsnull173 fun testNotAvailableControls() { 174 featureEnabled = false 175 176 // Destroy previous tile 177 tile.destroy() 178 tile = createTile() 179 180 assertThat(tile.isAvailable).isFalse() 181 } 182 183 @Test testObservingCallbacknull184 fun testObservingCallback() { 185 verify(controlsListingController).observe( 186 any(LifecycleOwner::class.java), 187 any(ControlsListingController.ControlsListingCallback::class.java) 188 ) 189 } 190 191 @Test testLongClickIntentnull192 fun testLongClickIntent() { 193 assertThat(tile.longClickIntent).isNull() 194 } 195 196 @Test testDoesNotHandleLongClicknull197 fun testDoesNotHandleLongClick() { 198 assertThat(tile.state.handlesLongClick).isFalse() 199 } 200 201 @Test testUnavailableByDefaultnull202 fun testUnavailableByDefault() { 203 assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) 204 } 205 206 @Test testStateUnavailableIfNoListingsnull207 fun testStateUnavailableIfNoListings() { 208 verify(controlsListingController).observe( 209 any(LifecycleOwner::class.java), 210 capture(listingCallbackCaptor) 211 ) 212 213 listingCallbackCaptor.value.onServicesUpdated(emptyList()) 214 testableLooper.processAllMessages() 215 216 assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) 217 } 218 219 @Test testStateUnavailableIfNotEnablednull220 fun testStateUnavailableIfNotEnabled() { 221 verify(controlsListingController).observe( 222 any(LifecycleOwner::class.java), 223 capture(listingCallbackCaptor) 224 ) 225 `when`(controlsComponent.isEnabled()).thenReturn(false) 226 227 listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) 228 testableLooper.processAllMessages() 229 230 assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) 231 } 232 233 @Test testStateActiveIfListingsHasControlsFavoritednull234 fun testStateActiveIfListingsHasControlsFavorited() { 235 verify(controlsListingController).observe( 236 any(LifecycleOwner::class.java), 237 capture(listingCallbackCaptor) 238 ) 239 `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) 240 `when`(controlsController.getPreferredSelection()).thenReturn( 241 SelectedItem.StructureItem(StructureInfo( 242 ComponentName("pkg", "cls"), 243 "structure", 244 listOf(ControlInfo("id", "title", "subtitle", 1)) 245 )) 246 ) 247 248 listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) 249 testableLooper.processAllMessages() 250 251 assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE) 252 } 253 254 @Test testStateInactiveIfListingsHasNoControlsFavoritednull255 fun testStateInactiveIfListingsHasNoControlsFavorited() { 256 verify(controlsListingController).observe( 257 any(LifecycleOwner::class.java), 258 capture(listingCallbackCaptor) 259 ) 260 `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) 261 `when`(controlsController.getPreferredSelection()) 262 .thenReturn(SelectedItem.StructureItem( 263 StructureInfo(ComponentName("pkg", "cls"), "structure", listOf()))) 264 265 listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) 266 testableLooper.processAllMessages() 267 268 assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE) 269 } 270 271 @Test testStateActiveIfPreferredIsPanelnull272 fun testStateActiveIfPreferredIsPanel() { 273 verify(controlsListingController).observe( 274 any(LifecycleOwner::class.java), 275 capture(listingCallbackCaptor) 276 ) 277 `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) 278 `when`(controlsController.getPreferredSelection()) 279 .thenReturn(SelectedItem.PanelItem("appName", ComponentName("pkg", "cls"))) 280 281 listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) 282 testableLooper.processAllMessages() 283 284 assertThat(tile.state.state).isEqualTo(Tile.STATE_ACTIVE) 285 } 286 287 @Test testStateInactiveIfLockednull288 fun testStateInactiveIfLocked() { 289 verify(controlsListingController).observe( 290 any(LifecycleOwner::class.java), 291 capture(listingCallbackCaptor) 292 ) 293 `when`(controlsComponent.getVisibility()) 294 .thenReturn(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK) 295 296 listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) 297 testableLooper.processAllMessages() 298 299 assertThat(tile.state.state).isEqualTo(Tile.STATE_INACTIVE) 300 } 301 302 @Test testMoveBetweenStatesnull303 fun testMoveBetweenStates() { 304 verify(controlsListingController).observe( 305 any(LifecycleOwner::class.java), 306 capture(listingCallbackCaptor) 307 ) 308 309 listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) 310 testableLooper.processAllMessages() 311 312 listingCallbackCaptor.value.onServicesUpdated(emptyList()) 313 testableLooper.processAllMessages() 314 315 assertThat(tile.state.state).isEqualTo(Tile.STATE_UNAVAILABLE) 316 } 317 318 @Test handleClick_unavailable_noActivityStartednull319 fun handleClick_unavailable_noActivityStarted() { 320 tile.click(null /* view */) 321 testableLooper.processAllMessages() 322 323 verifyZeroInteractions(activityStarter) 324 } 325 326 @Test handleClick_available_shownOverLockscreenWhenLockednull327 fun handleClick_available_shownOverLockscreenWhenLocked() { 328 verify(controlsListingController).observe( 329 any(LifecycleOwner::class.java), 330 capture(listingCallbackCaptor) 331 ) 332 `when`(controlsComponent.getVisibility()).thenReturn(ControlsComponent.Visibility.AVAILABLE) 333 `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java) 334 `when`(controlsController.getPreferredSelection()).thenReturn( 335 SelectedItem.StructureItem(StructureInfo( 336 ComponentName("pkg", "cls"), 337 "structure", 338 listOf(ControlInfo("id", "title", "subtitle", 1)) 339 )) 340 ) 341 342 listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) 343 testableLooper.processAllMessages() 344 345 tile.click(null /* view */) 346 testableLooper.processAllMessages() 347 348 verify(activityStarter).startActivity( 349 intentCaptor.capture(), 350 eq(true) /* dismissShade */, 351 nullable(ActivityTransitionAnimator.Controller::class.java), 352 eq(true) /* showOverLockscreenWhenLocked */) 353 assertThat(intentCaptor.value.component?.className).isEqualTo(CONTROLS_ACTIVITY_CLASS_NAME) 354 } 355 356 @Test handleClick_availableAfterUnlock_notShownOverLockscreenWhenLockednull357 fun handleClick_availableAfterUnlock_notShownOverLockscreenWhenLocked() { 358 verify(controlsListingController).observe( 359 any(LifecycleOwner::class.java), 360 capture(listingCallbackCaptor) 361 ) 362 `when`(controlsComponent.getVisibility()) 363 .thenReturn(ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK) 364 `when`(controlsUiController.resolveActivity()).thenReturn(ControlsActivity::class.java) 365 `when`(controlsController.getPreferredSelection()).thenReturn( 366 SelectedItem.StructureItem(StructureInfo( 367 ComponentName("pkg", "cls"), 368 "structure", 369 listOf(ControlInfo("id", "title", "subtitle", 1)) 370 )) 371 ) 372 373 listingCallbackCaptor.value.onServicesUpdated(listOf(serviceInfo)) 374 testableLooper.processAllMessages() 375 376 tile.click(null /* view */) 377 testableLooper.processAllMessages() 378 379 verify(activityStarter).startActivity( 380 intentCaptor.capture(), 381 anyBoolean() /* dismissShade */, 382 nullable(ActivityTransitionAnimator.Controller::class.java), 383 eq(false) /* showOverLockscreenWhenLocked */) 384 assertThat(intentCaptor.value.component?.className).isEqualTo(CONTROLS_ACTIVITY_CLASS_NAME) 385 } 386 387 @Test verifyTileEqualsResourceFromComponentnull388 fun verifyTileEqualsResourceFromComponent() { 389 assertThat(tile.tileLabel) 390 .isEqualTo( 391 context.getText( 392 controlsComponent.getTileTitleId())) 393 } 394 395 @Test verifyTileImageEqualsResourceFromComponentnull396 fun verifyTileImageEqualsResourceFromComponent() { 397 assertThat(tile.icon) 398 .isEqualTo(QSTileImpl.ResourceIcon.get(controlsComponent.getTileImageId())) 399 } 400 createTilenull401 private fun createTile(): DeviceControlsTile { 402 return DeviceControlsTile( 403 qsHost, 404 uiEventLogger, 405 testableLooper.looper, 406 Handler(testableLooper.looper), 407 FalsingManagerFake(), 408 metricsLogger, 409 statusBarStateController, 410 activityStarter, 411 qsLogger, 412 controlsComponent 413 ).also { 414 it.initialize() 415 testableLooper.processAllMessages() 416 } 417 } 418 } 419 420 private const val CONTROLS_ACTIVITY_CLASS_NAME = "com.android.systemui.controls.ui.ControlsActivity" 421