1 /* 2 * Copyright (C) 2019 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.PendingIntent 20 import android.content.ComponentName 21 import android.content.Context 22 import android.content.pm.ApplicationInfo 23 import android.content.pm.PackageManager 24 import android.content.pm.ServiceInfo 25 import android.graphics.drawable.Drawable 26 import android.graphics.drawable.Icon 27 import android.os.Handler 28 import android.service.quicksettings.IQSTileService 29 import android.service.quicksettings.Tile 30 import android.test.suitebuilder.annotation.SmallTest 31 import android.testing.AndroidTestingRunner 32 import android.testing.TestableLooper 33 import android.view.IWindowManager 34 import android.view.View 35 import com.android.internal.logging.MetricsLogger 36 import com.android.systemui.SysuiTestCase 37 import com.android.systemui.animation.ActivityLaunchAnimator 38 import com.android.systemui.animation.view.LaunchableFrameLayout 39 import com.android.systemui.classifier.FalsingManagerFake 40 import com.android.systemui.plugins.ActivityStarter 41 import com.android.systemui.plugins.qs.QSTile 42 import com.android.systemui.plugins.statusbar.StatusBarStateController 43 import com.android.systemui.qs.QSHost 44 import com.android.systemui.qs.QsEventLogger 45 import com.android.systemui.qs.logging.QSLogger 46 import com.android.systemui.settings.FakeDisplayTracker 47 import com.android.systemui.util.mockito.any 48 import com.android.systemui.util.mockito.eq 49 import com.android.systemui.util.mockito.nullable 50 import org.junit.Assert.assertEquals 51 import org.junit.Assert.assertFalse 52 import org.junit.Assert.assertThrows 53 import org.junit.Assert.assertTrue 54 import org.junit.Before 55 import org.junit.Test 56 import org.junit.runner.RunWith 57 import org.mockito.ArgumentMatchers.anyInt 58 import org.mockito.ArgumentMatchers.anyString 59 import org.mockito.Mock 60 import org.mockito.Mockito.clearInvocations 61 import org.mockito.Mockito.mock 62 import org.mockito.Mockito.never 63 import org.mockito.Mockito.reset 64 import org.mockito.Mockito.verify 65 import org.mockito.Mockito.`when` 66 import org.mockito.MockitoAnnotations 67 68 @SmallTest 69 @RunWith(AndroidTestingRunner::class) 70 @TestableLooper.RunWithLooper(setAsMainLooper = true) 71 class CustomTileTest : SysuiTestCase() { 72 73 companion object { 74 const val packageName = "test_package" 75 const val className = "test_class" 76 val componentName = ComponentName(packageName, className) 77 val TILE_SPEC = CustomTile.toSpec(componentName) 78 } 79 80 @Mock private lateinit var tileHost: QSHost 81 @Mock private lateinit var metricsLogger: MetricsLogger 82 @Mock private lateinit var statusBarStateController: StatusBarStateController 83 @Mock private lateinit var activityStarter: ActivityStarter 84 @Mock private lateinit var qsLogger: QSLogger 85 @Mock private lateinit var tileService: IQSTileService 86 @Mock private lateinit var tileServices: TileServices 87 @Mock private lateinit var tileServiceManager: TileServiceManager 88 @Mock private lateinit var windowService: IWindowManager 89 @Mock private lateinit var packageManager: PackageManager 90 @Mock private lateinit var applicationInfo: ApplicationInfo 91 @Mock private lateinit var serviceInfo: ServiceInfo 92 @Mock private lateinit var customTileStatePersister: CustomTileStatePersister 93 @Mock private lateinit var uiEventLogger: QsEventLogger 94 95 private var displayTracker = FakeDisplayTracker(mContext) 96 private lateinit var customTile: CustomTile 97 private lateinit var testableLooper: TestableLooper 98 private lateinit var customTileBuilder: CustomTile.Builder 99 100 @Before 101 fun setUp() { 102 MockitoAnnotations.initMocks(this) 103 testableLooper = TestableLooper.get(this) 104 105 mContext.addMockSystemService("window", windowService) 106 mContext.setMockPackageManager(packageManager) 107 `when`(tileHost.context).thenReturn(mContext) 108 `when`(tileServices.getTileWrapper(any(CustomTile::class.java))) 109 .thenReturn(tileServiceManager) 110 `when`(tileServiceManager.tileService).thenReturn(tileService) 111 `when`(packageManager.getApplicationInfo(anyString(), anyInt())) 112 .thenReturn(applicationInfo) 113 114 `when`(packageManager.getServiceInfo(any(ComponentName::class.java), anyInt())) 115 .thenReturn(serviceInfo) 116 serviceInfo.applicationInfo = applicationInfo 117 118 customTileBuilder = CustomTile.Builder( 119 { tileHost }, 120 uiEventLogger, 121 testableLooper.looper, 122 Handler(testableLooper.looper), 123 FalsingManagerFake(), 124 metricsLogger, 125 statusBarStateController, 126 activityStarter, 127 qsLogger, 128 customTileStatePersister, 129 tileServices, 130 displayTracker 131 ) 132 133 customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 134 customTile.initialize() 135 testableLooper.processAllMessages() 136 } 137 138 @Test 139 fun testCorrectUser() { 140 assertEquals(0, customTile.user) 141 142 val userContext = mock(Context::class.java) 143 `when`(userContext.packageManager).thenReturn(packageManager) 144 `when`(userContext.userId).thenReturn(10) 145 146 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, userContext) 147 tile.initialize() 148 testableLooper.processAllMessages() 149 150 assertEquals(10, tile.user) 151 } 152 153 @Test 154 fun testToggleableTileHasBooleanState() { 155 `when`(tileServiceManager.isToggleableTile).thenReturn(true) 156 customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 157 customTile.initialize() 158 testableLooper.processAllMessages() 159 160 assertTrue(customTile.state is QSTile.BooleanState) 161 assertTrue(customTile.newTileState() is QSTile.BooleanState) 162 } 163 164 @Test 165 fun testRegularTileHasNotBooleanState() { 166 assertFalse(customTile.state is QSTile.BooleanState) 167 assertFalse(customTile.newTileState() is QSTile.BooleanState) 168 } 169 170 @Test 171 fun testValueUpdatedInBooleanTile() { 172 `when`(tileServiceManager.isToggleableTile).thenReturn(true) 173 customTile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 174 customTile.initialize() 175 testableLooper.processAllMessages() 176 177 customTile.qsTile.icon = mock(Icon::class.java) 178 `when`(customTile.qsTile.icon.loadDrawable(any(Context::class.java))) 179 .thenReturn(mock(Drawable::class.java)) 180 181 val state = customTile.newTileState() 182 assertTrue(state is QSTile.BooleanState) 183 184 customTile.qsTile.state = Tile.STATE_INACTIVE 185 customTile.handleUpdateState(state, null) 186 assertFalse((state as QSTile.BooleanState).value) 187 188 customTile.qsTile.state = Tile.STATE_ACTIVE 189 customTile.handleUpdateState(state, null) 190 assertTrue(state.value) 191 192 customTile.qsTile.state = Tile.STATE_UNAVAILABLE 193 customTile.handleUpdateState(state, null) 194 assertFalse(state.value) 195 } 196 197 @Test 198 fun testNoCrashOnNullDrawable() { 199 customTile.qsTile.icon = mock(Icon::class.java) 200 `when`(customTile.qsTile.icon.loadDrawable(any(Context::class.java))) 201 .thenReturn(null) 202 customTile.handleUpdateState(customTile.newTileState(), null) 203 } 204 205 @Test 206 fun testNoLoadStateTileNotActive() { 207 // Not active by default 208 testableLooper.processAllMessages() 209 210 verify(customTileStatePersister, never()).readState(any()) 211 } 212 213 @Test 214 fun testNoPersistedStateTileNotActive() { 215 // Not active by default 216 val t = Tile().apply { 217 state = Tile.STATE_INACTIVE 218 } 219 customTile.updateTileState(t) 220 testableLooper.processAllMessages() 221 222 verify(customTileStatePersister, never()).persistState(any(), any()) 223 } 224 225 @Test 226 fun testPersistedStateRetrieved() { 227 val state = Tile.STATE_INACTIVE 228 val label = "test_label" 229 val subtitle = "test_subtitle" 230 val contentDescription = "test_content_description" 231 val stateDescription = "test_state_description" 232 233 val t = Tile().apply { 234 this.state = state 235 this.label = label 236 this.subtitle = subtitle 237 this.contentDescription = contentDescription 238 this.stateDescription = stateDescription 239 } 240 `when`(tileServiceManager.isActiveTile).thenReturn(true) 241 `when`(customTileStatePersister 242 .readState(TileServiceKey(componentName, customTile.user))).thenReturn(t) 243 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 244 tile.initialize() 245 testableLooper.processAllMessages() 246 247 // Make sure we have an icon in the tile because we don't have a default icon 248 // This should not be overridden by the retrieved tile that has null icon. 249 tile.qsTile.icon = mock(Icon::class.java) 250 `when`(tile.qsTile.icon.loadDrawable(any(Context::class.java))) 251 .thenReturn(mock(Drawable::class.java)) 252 253 val pi = mock(PendingIntent::class.java) 254 `when`(pi.isActivity).thenReturn(true) 255 tile.qsTile.activityLaunchForClick = pi 256 257 tile.refreshState() 258 259 testableLooper.processAllMessages() 260 261 val tileState = tile.state 262 263 assertEquals(state, tileState.state) 264 assertEquals(label, tileState.label) 265 assertEquals(subtitle, tileState.secondaryLabel) 266 assertEquals(contentDescription, tileState.contentDescription) 267 assertEquals(stateDescription, tileState.stateDescription) 268 } 269 270 @Test 271 fun testStoreStateOnChange() { 272 val t = Tile().apply { 273 state = Tile.STATE_INACTIVE 274 label = "test_label" 275 subtitle = "test_subtitle" 276 contentDescription = "test_content_description" 277 stateDescription = "test_state_description" 278 } 279 `when`(tileServiceManager.isActiveTile).thenReturn(true) 280 281 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 282 tile.initialize() 283 testableLooper.processAllMessages() 284 285 tile.updateTileState(t) 286 287 testableLooper.processAllMessages() 288 289 verify(customTileStatePersister) 290 .persistState(TileServiceKey(componentName, customTile.user), t) 291 } 292 293 @Test 294 fun testAvailableBeforeInitialization() { 295 `when`(packageManager.getApplicationInfo(anyString(), anyInt())) 296 .thenThrow(PackageManager.NameNotFoundException()) 297 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 298 assertTrue(tile.isAvailable) 299 } 300 301 @Test 302 fun testNotAvailableAfterInitializationWithoutIcon() { 303 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 304 reset(tileHost) 305 tile.initialize() 306 testableLooper.processAllMessages() 307 assertFalse(tile.isAvailable) 308 verify(tileHost).removeTile(tile.tileSpec) 309 } 310 311 @Test 312 fun testInvalidPendingIntentDoesNotStartActivity() { 313 val pi = mock(PendingIntent::class.java) 314 `when`(pi.isActivity).thenReturn(false) 315 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 316 317 assertThrows(IllegalArgumentException::class.java) { 318 tile.qsTile.activityLaunchForClick = pi 319 } 320 321 tile.handleClick(mock(View::class.java)) 322 testableLooper.processAllMessages() 323 324 verify(activityStarter, never()) 325 .startPendingIntentDismissingKeyguard( 326 any(), any(), any(ActivityLaunchAnimator.Controller::class.java)) 327 } 328 329 @Test 330 fun testValidPendingIntentWithNoClickDoesNotStartActivity() { 331 val pi = mock(PendingIntent::class.java) 332 `when`(pi.isActivity).thenReturn(true) 333 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 334 tile.qsTile.activityLaunchForClick = pi 335 336 testableLooper.processAllMessages() 337 338 verify(activityStarter, never()) 339 .startPendingIntentDismissingKeyguard( 340 any(), any(), any(ActivityLaunchAnimator.Controller::class.java)) 341 } 342 343 @Test 344 fun testValidPendingIntentStartsActivity() { 345 val pi = mock(PendingIntent::class.java) 346 `when`(pi.isActivity).thenReturn(true) 347 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 348 tile.qsTile.activityLaunchForClick = pi 349 350 tile.handleClick(mock(LaunchableFrameLayout::class.java)) 351 352 testableLooper.processAllMessages() 353 354 verify(activityStarter) 355 .startPendingIntentDismissingKeyguard( 356 eq(pi), nullable(), nullable<ActivityLaunchAnimator.Controller>()) 357 } 358 359 @Test 360 fun testActiveTileListensOnceAfterCreated() { 361 `when`(tileServiceManager.isActiveTile).thenReturn(true) 362 363 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 364 tile.initialize() 365 tile.postStale() 366 testableLooper.processAllMessages() 367 368 verify(tileServiceManager).setBindRequested(true) 369 verify(tileService).onStartListening() 370 } 371 372 @Test 373 fun testActiveTileDoesntListenAfterFirstTime() { 374 `when`(tileServiceManager.isActiveTile).thenReturn(true) 375 376 val tile = CustomTile.create(customTileBuilder, TILE_SPEC, mContext) 377 tile.initialize() 378 // Make sure we have an icon in the tile because we don't have a default icon 379 // This should not be overridden by the retrieved tile that has null icon. 380 tile.qsTile.icon = mock(Icon::class.java) 381 `when`(tile.qsTile.icon.loadDrawable(any(Context::class.java))) 382 .thenReturn(mock(Drawable::class.java)) 383 384 tile.postStale() 385 testableLooper.processAllMessages() 386 387 // postStale will set it to not listening after it's done 388 verify(tileService).onStopListening() 389 390 clearInvocations(tileServiceManager, tileService) 391 392 tile.setListening(Any(), true) 393 testableLooper.processAllMessages() 394 395 verify(tileServiceManager, never()).setBindRequested(true) 396 verify(tileService, never()).onStartListening() 397 } 398 }