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