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.statusbar.lockscreen 18 19 import android.app.smartspace.SmartspaceManager 20 import android.app.smartspace.SmartspaceSession 21 import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener 22 import android.app.smartspace.SmartspaceTarget 23 import android.content.ComponentName 24 import android.content.ContentResolver 25 import android.content.pm.UserInfo 26 import android.database.ContentObserver 27 import android.graphics.drawable.Drawable 28 import android.net.Uri 29 import android.os.Handler 30 import android.os.UserHandle 31 import android.provider.Settings 32 import android.view.View 33 import android.widget.FrameLayout 34 import androidx.test.filters.SmallTest 35 import com.android.systemui.SysuiTestCase 36 import com.android.systemui.plugins.ActivityStarter 37 import com.android.systemui.plugins.BcSmartspaceDataPlugin 38 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceTargetListener 39 import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView 40 import com.android.systemui.plugins.FalsingManager 41 import com.android.systemui.plugins.statusbar.StatusBarStateController 42 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener 43 import com.android.systemui.settings.UserTracker 44 import com.android.systemui.flags.FeatureFlags 45 import com.android.systemui.statusbar.policy.ConfigurationController 46 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener 47 import com.android.systemui.statusbar.policy.DeviceProvisionedController 48 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener 49 import com.android.systemui.util.concurrency.FakeExecution 50 import com.android.systemui.util.concurrency.FakeExecutor 51 import com.android.systemui.util.mockito.any 52 import com.android.systemui.util.mockito.capture 53 import com.android.systemui.util.mockito.eq 54 import com.android.systemui.util.settings.SecureSettings 55 import com.android.systemui.util.time.FakeSystemClock 56 import org.junit.Before 57 import org.junit.Test 58 import org.mockito.ArgumentCaptor 59 import org.mockito.Captor 60 import org.mockito.Mock 61 import org.mockito.Mockito.`when` 62 import org.mockito.Mockito.anyInt 63 import org.mockito.Mockito.clearInvocations 64 import org.mockito.Mockito.mock 65 import org.mockito.Mockito.never 66 import org.mockito.Mockito.spy 67 import org.mockito.Mockito.verify 68 import org.mockito.MockitoAnnotations 69 import java.util.Optional 70 71 @SmallTest 72 class LockscreenSmartspaceControllerTest : SysuiTestCase() { 73 @Mock 74 private lateinit var featureFlags: FeatureFlags 75 @Mock 76 private lateinit var smartspaceManager: SmartspaceManager 77 @Mock 78 private lateinit var smartspaceSession: SmartspaceSession 79 @Mock 80 private lateinit var activityStarter: ActivityStarter 81 @Mock 82 private lateinit var falsingManager: FalsingManager 83 @Mock 84 private lateinit var secureSettings: SecureSettings 85 @Mock 86 private lateinit var userTracker: UserTracker 87 @Mock 88 private lateinit var contentResolver: ContentResolver 89 @Mock 90 private lateinit var configurationController: ConfigurationController 91 @Mock 92 private lateinit var statusBarStateController: StatusBarStateController 93 @Mock 94 private lateinit var deviceProvisionedController: DeviceProvisionedController 95 @Mock 96 private lateinit var handler: Handler 97 98 @Mock 99 private lateinit var plugin: BcSmartspaceDataPlugin 100 @Mock 101 private lateinit var controllerListener: SmartspaceTargetListener 102 103 @Captor 104 private lateinit var sessionListenerCaptor: ArgumentCaptor<OnTargetsAvailableListener> 105 @Captor 106 private lateinit var userTrackerCaptor: ArgumentCaptor<UserTracker.Callback> 107 @Captor 108 private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver> 109 @Captor 110 private lateinit var configChangeListenerCaptor: ArgumentCaptor<ConfigurationListener> 111 @Captor 112 private lateinit var statusBarStateListenerCaptor: ArgumentCaptor<StateListener> 113 @Captor 114 private lateinit var deviceProvisionedCaptor: ArgumentCaptor<DeviceProvisionedListener> 115 116 private lateinit var sessionListener: OnTargetsAvailableListener 117 private lateinit var userListener: UserTracker.Callback 118 private lateinit var settingsObserver: ContentObserver 119 private lateinit var configChangeListener: ConfigurationListener 120 private lateinit var statusBarStateListener: StateListener 121 private lateinit var deviceProvisionedListener: DeviceProvisionedListener 122 123 private lateinit var smartspaceView: SmartspaceView 124 125 private val clock = FakeSystemClock() 126 private val executor = FakeExecutor(clock) 127 private val execution = FakeExecution() 128 private val fakeParent = FrameLayout(context) 129 private val fakePrivateLockscreenSettingUri = Uri.Builder().appendPath("test").build() 130 131 private val userHandlePrimary: UserHandle = UserHandle(0) 132 private val userHandleManaged: UserHandle = UserHandle(2) 133 private val userHandleSecondary: UserHandle = UserHandle(3) 134 135 private val userList = listOf( 136 mockUserInfo(userHandlePrimary, isManagedProfile = false), 137 mockUserInfo(userHandleManaged, isManagedProfile = true), 138 mockUserInfo(userHandleSecondary, isManagedProfile = false) 139 ) 140 141 private lateinit var controller: LockscreenSmartspaceController 142 143 @Before 144 fun setUp() { 145 MockitoAnnotations.initMocks(this) 146 147 `when`(featureFlags.isSmartspaceEnabled).thenReturn(true) 148 149 `when`(secureSettings.getUriFor(PRIVATE_LOCKSCREEN_SETTING)) 150 .thenReturn(fakePrivateLockscreenSettingUri) 151 `when`(smartspaceManager.createSmartspaceSession(any())).thenReturn(smartspaceSession) 152 `when`(plugin.getView(any())).thenReturn(createSmartspaceView(), createSmartspaceView()) 153 `when`(userTracker.userProfiles).thenReturn(userList) 154 `when`(statusBarStateController.dozeAmount).thenReturn(0.5f) 155 `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true) 156 `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true) 157 158 setActiveUser(userHandlePrimary) 159 setAllowPrivateNotifications(userHandlePrimary, true) 160 setAllowPrivateNotifications(userHandleManaged, true) 161 setAllowPrivateNotifications(userHandleSecondary, true) 162 163 controller = LockscreenSmartspaceController( 164 context, 165 featureFlags, 166 smartspaceManager, 167 activityStarter, 168 falsingManager, 169 secureSettings, 170 userTracker, 171 contentResolver, 172 configurationController, 173 statusBarStateController, 174 deviceProvisionedController, 175 execution, 176 executor, 177 handler, 178 Optional.of(plugin) 179 ) 180 181 verify(deviceProvisionedController).addCallback(capture(deviceProvisionedCaptor)) 182 deviceProvisionedListener = deviceProvisionedCaptor.value 183 } 184 185 @Test(expected = RuntimeException::class) 186 fun testThrowsIfFlagIsDisabled() { 187 // GIVEN the feature flag is disabled 188 `when`(featureFlags.isSmartspaceEnabled).thenReturn(false) 189 190 // WHEN we try to build the view 191 controller.buildAndConnectView(fakeParent) 192 193 // THEN an exception is thrown 194 } 195 196 @Test 197 fun connectOnlyAfterDeviceIsProvisioned() { 198 // GIVEN an unprovisioned device and an attempt to connect 199 `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(false) 200 `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(false) 201 202 // WHEN a connection attempt is made and view is attached 203 val view = controller.buildAndConnectView(fakeParent) 204 controller.stateChangeListener.onViewAttachedToWindow(view) 205 206 // THEN no session is created 207 verify(smartspaceManager, never()).createSmartspaceSession(any()) 208 209 // WHEN it does become provisioned 210 `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true) 211 `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true) 212 deviceProvisionedListener.onUserSetupChanged() 213 214 // THEN the session is created 215 verify(smartspaceManager).createSmartspaceSession(any()) 216 // THEN an event notifier is registered 217 verify(plugin).registerSmartspaceEventNotifier(any()) 218 } 219 220 @Test 221 fun testListenersAreRegistered() { 222 // GIVEN a listener is added after a session is created 223 connectSession() 224 225 // WHEN a listener is registered 226 controller.addListener(controllerListener) 227 228 // THEN the listener is registered to the underlying plugin 229 verify(plugin).registerListener(controllerListener) 230 } 231 232 @Test 233 fun testEarlyRegisteredListenersAreAttachedAfterConnected() { 234 // GIVEN a listener that is registered before the session is created 235 controller.addListener(controllerListener) 236 237 // WHEN the session is created 238 connectSession() 239 240 // THEN the listener is subsequently registered 241 verify(plugin).registerListener(controllerListener) 242 } 243 244 @Test 245 fun testEmptyListIsEmittedAndNotifierRemovedAfterDisconnect() { 246 // GIVEN a registered listener on an active session 247 connectSession() 248 clearInvocations(plugin) 249 250 // WHEN the session is closed 251 controller.stateChangeListener.onViewDetachedFromWindow(smartspaceView as View) 252 controller.disconnect() 253 254 // THEN the listener receives an empty list of targets and unregisters the notifier 255 verify(plugin).onTargetsAvailable(emptyList()) 256 verify(plugin).registerSmartspaceEventNotifier(null) 257 } 258 259 @Test 260 fun testUserChangeReloadsSmartspace() { 261 // GIVEN a connected smartspace session 262 connectSession() 263 264 // WHEN the active user changes 265 userListener.onUserChanged(-1, context) 266 267 // THEN we request a new smartspace update 268 verify(smartspaceSession).requestSmartspaceUpdate() 269 } 270 271 @Test 272 fun testSettingsChangeReloadsSmartspace() { 273 // GIVEN a connected smartspace session 274 connectSession() 275 276 // WHEN the lockscreen privacy setting changes 277 settingsObserver.onChange(true, null) 278 279 // THEN we request a new smartspace update 280 verify(smartspaceSession).requestSmartspaceUpdate() 281 } 282 283 @Test 284 fun testThemeChangeUpdatesTextColor() { 285 // GIVEN a connected smartspace session 286 connectSession() 287 288 // WHEN the theme changes 289 configChangeListener.onThemeChanged() 290 291 // We update the new text color to match the wallpaper color 292 verify(smartspaceView).setPrimaryTextColor(anyInt()) 293 } 294 295 @Test 296 fun testDozeAmountChangeUpdatesView() { 297 // GIVEN a connected smartspace session 298 connectSession() 299 300 // WHEN the doze amount changes 301 statusBarStateListener.onDozeAmountChanged(0.1f, 0.7f) 302 303 // We pass that along to the view 304 verify(smartspaceView).setDozeAmount(0.7f) 305 } 306 307 @Test 308 fun testSensitiveTargetsAreNotFilteredIfAllowed() { 309 // GIVEN the active and managed users allow sensitive content 310 connectSession() 311 312 // WHEN we receive a list of targets 313 val targets = listOf( 314 makeTarget(1, userHandlePrimary, isSensitive = true), 315 makeTarget(2, userHandleManaged, isSensitive = true), 316 makeTarget(3, userHandlePrimary, isSensitive = true) 317 ) 318 sessionListener.onTargetsAvailable(targets) 319 320 // THEN all sensitive content is still shown 321 verify(plugin).onTargetsAvailable(eq(targets)) 322 } 323 324 @Test 325 fun testNonSensitiveTargetsAreNeverFiltered() { 326 // GIVEN the active user doesn't allow sensitive lockscreen content 327 setAllowPrivateNotifications(userHandlePrimary, false) 328 connectSession() 329 330 // WHEN we receive a list of targets 331 val targets = listOf( 332 makeTarget(1, userHandlePrimary), 333 makeTarget(2, userHandlePrimary), 334 makeTarget(3, userHandlePrimary) 335 ) 336 sessionListener.onTargetsAvailable(targets) 337 338 // THEN all non-sensitive content is still shown 339 verify(plugin).onTargetsAvailable(eq(targets)) 340 } 341 342 @Test 343 fun testSensitiveTargetsAreFilteredOutForAppropriateUsers() { 344 // GIVEN the active and managed users don't allow sensitive lockscreen content 345 setAllowPrivateNotifications(userHandlePrimary, false) 346 setAllowPrivateNotifications(userHandleManaged, false) 347 connectSession() 348 349 // WHEN we receive a list of targets 350 val targets = listOf( 351 makeTarget(0, userHandlePrimary), 352 makeTarget(1, userHandlePrimary, isSensitive = true), 353 makeTarget(2, userHandleManaged, isSensitive = true), 354 makeTarget(3, userHandleManaged), 355 makeTarget(4, userHandlePrimary, isSensitive = true), 356 makeTarget(5, userHandlePrimary), 357 makeTarget(6, userHandleSecondary, isSensitive = true) 358 ) 359 sessionListener.onTargetsAvailable(targets) 360 361 // THEN only non-sensitive content from those accounts is shown 362 verify(plugin).onTargetsAvailable(eq(listOf( 363 targets[0], 364 targets[3], 365 targets[5] 366 ))) 367 } 368 369 @Test 370 fun testSettingsAreReloaded() { 371 // GIVEN a connected session where the privacy settings later flip to false 372 connectSession() 373 setAllowPrivateNotifications(userHandlePrimary, false) 374 setAllowPrivateNotifications(userHandleManaged, false) 375 settingsObserver.onChange(true, fakePrivateLockscreenSettingUri) 376 377 // WHEN we receive a new list of targets 378 val targets = listOf( 379 makeTarget(1, userHandlePrimary, isSensitive = true), 380 makeTarget(2, userHandleManaged, isSensitive = true), 381 makeTarget(4, userHandlePrimary, isSensitive = true) 382 ) 383 sessionListener.onTargetsAvailable(targets) 384 385 // THEN we filter based on the new settings values 386 verify(plugin).onTargetsAvailable(emptyList()) 387 } 388 389 @Test 390 fun testRecognizeSwitchToSecondaryUser() { 391 // GIVEN an inactive secondary user that doesn't allow sensitive content 392 setAllowPrivateNotifications(userHandleSecondary, false) 393 connectSession() 394 395 // WHEN the secondary user becomes the active user 396 setActiveUser(userHandleSecondary) 397 userListener.onUserChanged(userHandleSecondary.identifier, context) 398 399 // WHEN we receive a new list of targets 400 val targets = listOf( 401 makeTarget(0, userHandlePrimary), 402 makeTarget(1, userHandleSecondary), 403 makeTarget(2, userHandleSecondary, isSensitive = true), 404 makeTarget(3, userHandleManaged), 405 makeTarget(4, userHandleSecondary), 406 makeTarget(5, userHandleManaged), 407 makeTarget(6, userHandlePrimary) 408 ) 409 sessionListener.onTargetsAvailable(targets) 410 411 // THEN only non-sensitive content from the secondary user is shown 412 verify(plugin).onTargetsAvailable(listOf( 413 targets[1], 414 targets[4] 415 )) 416 } 417 418 @Test 419 fun testUnregisterListenersOnCleanup() { 420 // GIVEN a connected session 421 connectSession() 422 423 // WHEN we are told to cleanup 424 controller.stateChangeListener.onViewDetachedFromWindow(smartspaceView as View) 425 controller.disconnect() 426 427 // THEN we disconnect from the session and unregister any listeners 428 verify(smartspaceSession).removeOnTargetsAvailableListener(sessionListener) 429 verify(smartspaceSession).close() 430 verify(userTracker).removeCallback(userListener) 431 verify(contentResolver).unregisterContentObserver(settingsObserver) 432 verify(configurationController).removeCallback(configChangeListener) 433 verify(statusBarStateController).removeCallback(statusBarStateListener) 434 } 435 436 @Test 437 fun testMultipleViewsUseSameSession() { 438 // GIVEN a connected session 439 connectSession() 440 clearInvocations(smartspaceManager) 441 clearInvocations(plugin) 442 443 // WHEN we're asked to connect a second time and add to a parent. If the same view 444 // was created the ViewGroup will throw an exception 445 val view = controller.buildAndConnectView(fakeParent) 446 fakeParent.addView(view) 447 val smartspaceView2 = view as SmartspaceView 448 449 // THEN the existing session is reused and views are registered 450 verify(smartspaceManager, never()).createSmartspaceSession(any()) 451 verify(smartspaceView2).registerDataProvider(plugin) 452 } 453 454 @Test 455 fun testConnectAttemptBeforeInitializationShouldNotCreateSession() { 456 // GIVEN an uninitalized smartspaceView 457 // WHEN the device is provisioned 458 `when`(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true) 459 `when`(deviceProvisionedController.isCurrentUserSetup()).thenReturn(true) 460 deviceProvisionedListener.onDeviceProvisionedChanged() 461 462 // THEN no calls to createSmartspaceSession should occur 463 verify(smartspaceManager, never()).createSmartspaceSession(any()) 464 // THEN no listeners should be registered 465 verify(configurationController, never()).addCallback(any()) 466 } 467 468 private fun connectSession() { 469 val view = controller.buildAndConnectView(fakeParent) 470 smartspaceView = view as SmartspaceView 471 472 controller.stateChangeListener.onViewAttachedToWindow(view) 473 474 verify(smartspaceView).registerDataProvider(plugin) 475 verify(smartspaceSession) 476 .addOnTargetsAvailableListener(any(), capture(sessionListenerCaptor)) 477 sessionListener = sessionListenerCaptor.value 478 479 verify(userTracker).addCallback(capture(userTrackerCaptor), any()) 480 userListener = userTrackerCaptor.value 481 482 verify(contentResolver).registerContentObserver( 483 eq(fakePrivateLockscreenSettingUri), 484 eq(true), 485 capture(settingsObserverCaptor), 486 eq(UserHandle.USER_ALL)) 487 settingsObserver = settingsObserverCaptor.value 488 489 verify(configurationController).addCallback(configChangeListenerCaptor.capture()) 490 configChangeListener = configChangeListenerCaptor.value 491 492 verify(statusBarStateController).addCallback(statusBarStateListenerCaptor.capture()) 493 statusBarStateListener = statusBarStateListenerCaptor.value 494 495 verify(smartspaceSession).requestSmartspaceUpdate() 496 clearInvocations(smartspaceSession) 497 498 verify(smartspaceView).setPrimaryTextColor(anyInt()) 499 verify(smartspaceView).setDozeAmount(0.5f) 500 clearInvocations(view) 501 502 fakeParent.addView(view) 503 } 504 505 private fun setActiveUser(userHandle: UserHandle) { 506 `when`(userTracker.userId).thenReturn(userHandle.identifier) 507 `when`(userTracker.userHandle).thenReturn(userHandle) 508 } 509 510 private fun mockUserInfo(userHandle: UserHandle, isManagedProfile: Boolean): UserInfo { 511 val userInfo = mock(UserInfo::class.java) 512 `when`(userInfo.userHandle).thenReturn(userHandle) 513 `when`(userInfo.isManagedProfile).thenReturn(isManagedProfile) 514 return userInfo 515 } 516 517 fun makeTarget( 518 id: Int, 519 userHandle: UserHandle, 520 isSensitive: Boolean = false 521 ): SmartspaceTarget { 522 return SmartspaceTarget.Builder( 523 "target$id", 524 ComponentName("testpackage", "testclass$id"), 525 userHandle) 526 .setSensitive(isSensitive) 527 .build() 528 } 529 530 private fun setAllowPrivateNotifications(user: UserHandle, value: Boolean) { 531 `when`(secureSettings.getIntForUser( 532 eq(PRIVATE_LOCKSCREEN_SETTING), 533 anyInt(), 534 eq(user.identifier)) 535 ).thenReturn(if (value) 1 else 0) 536 } 537 538 private fun createSmartspaceView(): SmartspaceView { 539 return spy(object : View(context), SmartspaceView { 540 override fun registerDataProvider(plugin: BcSmartspaceDataPlugin?) { 541 } 542 543 override fun setPrimaryTextColor(color: Int) { 544 } 545 546 override fun setDozeAmount(amount: Float) { 547 } 548 549 override fun setIntentStarter(intentStarter: BcSmartspaceDataPlugin.IntentStarter?) { 550 } 551 552 override fun setFalsingManager(falsingManager: FalsingManager?) { 553 } 554 555 override fun setDnd(image: Drawable?, description: String?) { 556 } 557 558 override fun setNextAlarm(image: Drawable?, description: String?) { 559 } 560 561 override fun setMediaTarget(target: SmartspaceTarget?) { 562 } 563 }) 564 } 565 } 566 567 private const val PRIVATE_LOCKSCREEN_SETTING = 568 Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS 569