1 /*
2  * Copyright (C) 2022 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 
18 package com.android.systemui.keyguard
19 
20 import android.app.admin.DevicePolicyManager
21 import android.content.ContentValues
22 import android.content.pm.PackageManager
23 import android.content.pm.ProviderInfo
24 import android.os.Bundle
25 import android.os.Handler
26 import android.os.IBinder
27 import android.os.UserHandle
28 import android.testing.AndroidTestingRunner
29 import android.testing.TestableLooper
30 import android.view.SurfaceControlViewHost
31 import androidx.test.filters.SmallTest
32 import com.android.internal.widget.LockPatternUtils
33 import com.android.systemui.R
34 import com.android.systemui.SystemUIAppComponentFactoryBase
35 import com.android.systemui.SysuiTestCase
36 import com.android.systemui.animation.DialogLaunchAnimator
37 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
38 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
39 import com.android.systemui.dock.DockManagerFake
40 import com.android.systemui.flags.FakeFeatureFlags
41 import com.android.systemui.flags.Flags
42 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
43 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
44 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
45 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
46 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
47 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
48 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
49 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
50 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
51 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
52 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger
53 import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer
54 import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRendererFactory
55 import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager
56 import com.android.systemui.plugins.ActivityStarter
57 import com.android.systemui.settings.UserFileManager
58 import com.android.systemui.settings.UserTracker
59 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
60 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
61 import com.android.systemui.statusbar.CommandQueue
62 import com.android.systemui.statusbar.policy.KeyguardStateController
63 import com.android.systemui.util.FakeSharedPreferences
64 import com.android.systemui.util.mockito.any
65 import com.android.systemui.util.mockito.mock
66 import com.android.systemui.util.mockito.whenever
67 import com.android.systemui.util.settings.FakeSettings
68 import com.google.common.truth.Truth.assertThat
69 import kotlinx.coroutines.ExperimentalCoroutinesApi
70 import kotlinx.coroutines.test.TestScope
71 import kotlinx.coroutines.test.UnconfinedTestDispatcher
72 import kotlinx.coroutines.test.runTest
73 import org.junit.After
74 import org.junit.Before
75 import org.junit.Test
76 import org.junit.runner.RunWith
77 import org.mockito.ArgumentMatchers.anyInt
78 import org.mockito.ArgumentMatchers.anyString
79 import org.mockito.Mock
80 import org.mockito.Mockito.verify
81 import org.mockito.MockitoAnnotations
82 
83 @OptIn(ExperimentalCoroutinesApi::class)
84 @SmallTest
85 @RunWith(AndroidTestingRunner::class)
86 @TestableLooper.RunWithLooper(setAsMainLooper = true)
87 class CustomizationProviderTest : SysuiTestCase() {
88 
89     @Mock private lateinit var lockPatternUtils: LockPatternUtils
90     @Mock private lateinit var keyguardStateController: KeyguardStateController
91     @Mock private lateinit var userTracker: UserTracker
92     @Mock private lateinit var activityStarter: ActivityStarter
93     @Mock private lateinit var previewRendererFactory: KeyguardPreviewRendererFactory
94     @Mock private lateinit var previewRenderer: KeyguardPreviewRenderer
95     @Mock private lateinit var backgroundHandler: Handler
96     @Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage
97     @Mock private lateinit var launchAnimator: DialogLaunchAnimator
98     @Mock private lateinit var commandQueue: CommandQueue
99     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
100     @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger
101 
102     private lateinit var dockManager: DockManagerFake
103     private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
104 
105     private lateinit var underTest: CustomizationProvider
106     private lateinit var testScope: TestScope
107 
108     @Before
109     fun setUp() {
110         MockitoAnnotations.initMocks(this)
111         overrideResource(R.bool.custom_lockscreen_shortcuts_enabled, true)
112         whenever(previewRenderer.surfacePackage).thenReturn(previewSurfacePackage)
113         whenever(previewRendererFactory.create(any())).thenReturn(previewRenderer)
114         whenever(backgroundHandler.looper).thenReturn(TestableLooper.get(this).looper)
115 
116         dockManager = DockManagerFake()
117         biometricSettingsRepository = FakeBiometricSettingsRepository()
118 
119         underTest = CustomizationProvider()
120         val testDispatcher = UnconfinedTestDispatcher()
121         testScope = TestScope(testDispatcher)
122         val localUserSelectionManager =
123             KeyguardQuickAffordanceLocalUserSelectionManager(
124                 context = context,
125                 userFileManager =
126                     mock<UserFileManager>().apply {
127                         whenever(
128                                 getSharedPreferences(
129                                     anyString(),
130                                     anyInt(),
131                                     anyInt(),
132                                 )
133                             )
134                             .thenReturn(FakeSharedPreferences())
135                     },
136                 userTracker = userTracker,
137                 broadcastDispatcher = fakeBroadcastDispatcher,
138             )
139         val remoteUserSelectionManager =
140             KeyguardQuickAffordanceRemoteUserSelectionManager(
141                 scope = testScope.backgroundScope,
142                 userTracker = userTracker,
143                 clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
144                 userHandle = UserHandle.SYSTEM,
145             )
146         val quickAffordanceRepository =
147             KeyguardQuickAffordanceRepository(
148                 appContext = context,
149                 scope = testScope.backgroundScope,
150                 localUserSelectionManager = localUserSelectionManager,
151                 remoteUserSelectionManager = remoteUserSelectionManager,
152                 userTracker = userTracker,
153                 configs =
154                     setOf(
155                         FakeKeyguardQuickAffordanceConfig(
156                             key = AFFORDANCE_1,
157                             pickerName = AFFORDANCE_1_NAME,
158                             pickerIconResourceId = 1,
159                         ),
160                         FakeKeyguardQuickAffordanceConfig(
161                             key = AFFORDANCE_2,
162                             pickerName = AFFORDANCE_2_NAME,
163                             pickerIconResourceId = 2,
164                         ),
165                     ),
166                 legacySettingSyncer =
167                     KeyguardQuickAffordanceLegacySettingSyncer(
168                         scope = testScope.backgroundScope,
169                         backgroundDispatcher = testDispatcher,
170                         secureSettings = FakeSettings(),
171                         selectionsManager = localUserSelectionManager,
172                     ),
173                 dumpManager = mock(),
174                 userHandle = UserHandle.SYSTEM,
175             )
176         val featureFlags =
177             FakeFeatureFlags().apply {
178                 set(Flags.LOCKSCREEN_CUSTOM_CLOCKS, true)
179                 set(Flags.REVAMPED_WALLPAPER_UI, true)
180                 set(Flags.WALLPAPER_FULLSCREEN_PREVIEW, true)
181                 set(Flags.FACE_AUTH_REFACTOR, true)
182             }
183         underTest.interactor =
184             KeyguardQuickAffordanceInteractor(
185                 keyguardInteractor =
186                     KeyguardInteractor(
187                         repository = FakeKeyguardRepository(),
188                         commandQueue = commandQueue,
189                         featureFlags = featureFlags,
190                         bouncerRepository = FakeKeyguardBouncerRepository(),
191                         configurationRepository = FakeConfigurationRepository(),
192                     ),
193                 lockPatternUtils = lockPatternUtils,
194                 keyguardStateController = keyguardStateController,
195                 userTracker = userTracker,
196                 activityStarter = activityStarter,
197                 featureFlags = featureFlags,
198                 repository = { quickAffordanceRepository },
199                 launchAnimator = launchAnimator,
200                 logger = logger,
201                 devicePolicyManager = devicePolicyManager,
202                 dockManager = dockManager,
203                 biometricSettingsRepository = biometricSettingsRepository,
204                 backgroundDispatcher = testDispatcher,
205                 appContext = mContext,
206             )
207         underTest.previewManager =
208             KeyguardRemotePreviewManager(
209                 applicationScope = testScope.backgroundScope,
210                 previewRendererFactory = previewRendererFactory,
211                 mainDispatcher = testDispatcher,
212                 backgroundHandler = backgroundHandler,
213             )
214         underTest.mainDispatcher = UnconfinedTestDispatcher()
215 
216         underTest.attachInfoForTesting(
217             context,
218             ProviderInfo().apply { authority = Contract.AUTHORITY },
219         )
220         context.contentResolver.addProvider(Contract.AUTHORITY, underTest)
221         context.testablePermissions.setPermission(
222             Contract.PERMISSION,
223             PackageManager.PERMISSION_GRANTED,
224         )
225     }
226 
227     @After
228     fun tearDown() {
229         mContext
230             .getOrCreateTestableResources()
231             .removeOverride(R.bool.custom_lockscreen_shortcuts_enabled)
232     }
233 
234     @Test
235     fun onAttachInfo_reportsContext() {
236         val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock()
237         underTest.setContextAvailableCallback(callback)
238 
239         underTest.attachInfo(context, null)
240 
241         verify(callback).onContextAvailable(context)
242     }
243 
244     @Test
245     fun getType() {
246         assertThat(underTest.getType(Contract.LockScreenQuickAffordances.AffordanceTable.URI))
247             .isEqualTo(
248                 "vnd.android.cursor.dir/vnd." +
249                     "${Contract.AUTHORITY}." +
250                     Contract.LockScreenQuickAffordances.qualifiedTablePath(
251                         Contract.LockScreenQuickAffordances.AffordanceTable.TABLE_NAME
252                     )
253             )
254         assertThat(underTest.getType(Contract.LockScreenQuickAffordances.SlotTable.URI))
255             .isEqualTo(
256                 "vnd.android.cursor.dir/vnd.${Contract.AUTHORITY}." +
257                     Contract.LockScreenQuickAffordances.qualifiedTablePath(
258                         Contract.LockScreenQuickAffordances.SlotTable.TABLE_NAME
259                     )
260             )
261         assertThat(underTest.getType(Contract.LockScreenQuickAffordances.SelectionTable.URI))
262             .isEqualTo(
263                 "vnd.android.cursor.dir/vnd." +
264                     "${Contract.AUTHORITY}." +
265                     Contract.LockScreenQuickAffordances.qualifiedTablePath(
266                         Contract.LockScreenQuickAffordances.SelectionTable.TABLE_NAME
267                     )
268             )
269         assertThat(underTest.getType(Contract.FlagsTable.URI))
270             .isEqualTo(
271                 "vnd.android.cursor.dir/vnd." +
272                     "${Contract.AUTHORITY}." +
273                     Contract.FlagsTable.TABLE_NAME
274             )
275     }
276 
277     @Test
278     fun insertAndQuerySelection() =
279         testScope.runTest {
280             val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
281             val affordanceId = AFFORDANCE_2
282             val affordanceName = AFFORDANCE_2_NAME
283 
284             insertSelection(
285                 slotId = slotId,
286                 affordanceId = affordanceId,
287             )
288 
289             assertThat(querySelections())
290                 .isEqualTo(
291                     listOf(
292                         Selection(
293                             slotId = slotId,
294                             affordanceId = affordanceId,
295                             affordanceName = affordanceName,
296                         )
297                     )
298                 )
299         }
300 
301     @Test
302     fun querySlotsProvidesTwoSlots() =
303         testScope.runTest {
304             assertThat(querySlots())
305                 .isEqualTo(
306                     listOf(
307                         Slot(
308                             id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
309                             capacity = 1,
310                         ),
311                         Slot(
312                             id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
313                             capacity = 1,
314                         ),
315                     )
316                 )
317         }
318 
319     @Test
320     fun queryAffordancesProvidesTwoAffordances() =
321         testScope.runTest {
322             assertThat(queryAffordances())
323                 .isEqualTo(
324                     listOf(
325                         Affordance(
326                             id = AFFORDANCE_1,
327                             name = AFFORDANCE_1_NAME,
328                             iconResourceId = 1,
329                         ),
330                         Affordance(
331                             id = AFFORDANCE_2,
332                             name = AFFORDANCE_2_NAME,
333                             iconResourceId = 2,
334                         ),
335                     )
336                 )
337         }
338 
339     @Test
340     fun deleteAndQuerySelection() =
341         testScope.runTest {
342             insertSelection(
343                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
344                 affordanceId = AFFORDANCE_1,
345             )
346             insertSelection(
347                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
348                 affordanceId = AFFORDANCE_2,
349             )
350 
351             context.contentResolver.delete(
352                 Contract.LockScreenQuickAffordances.SelectionTable.URI,
353                 "${Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID} = ? AND" +
354                     " ${Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID}" +
355                     " = ?",
356                 arrayOf(
357                     KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
358                     AFFORDANCE_2,
359                 ),
360             )
361 
362             assertThat(querySelections())
363                 .isEqualTo(
364                     listOf(
365                         Selection(
366                             slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
367                             affordanceId = AFFORDANCE_1,
368                             affordanceName = AFFORDANCE_1_NAME,
369                         )
370                     )
371                 )
372         }
373 
374     @Test
375     fun deleteAllSelectionsInAslot() =
376         testScope.runTest {
377             insertSelection(
378                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
379                 affordanceId = AFFORDANCE_1,
380             )
381             insertSelection(
382                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
383                 affordanceId = AFFORDANCE_2,
384             )
385 
386             context.contentResolver.delete(
387                 Contract.LockScreenQuickAffordances.SelectionTable.URI,
388                 Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID,
389                 arrayOf(
390                     KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
391                 ),
392             )
393 
394             assertThat(querySelections())
395                 .isEqualTo(
396                     listOf(
397                         Selection(
398                             slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
399                             affordanceId = AFFORDANCE_1,
400                             affordanceName = AFFORDANCE_1_NAME,
401                         )
402                     )
403                 )
404         }
405 
406     @Test
407     fun preview() =
408         testScope.runTest {
409             val hostToken: IBinder = mock()
410             whenever(previewRenderer.hostToken).thenReturn(hostToken)
411             val extras = Bundle()
412 
413             val result = underTest.call("whatever", "anything", extras)
414 
415             verify(previewRenderer).render()
416             verify(hostToken).linkToDeath(any(), anyInt())
417             assertThat(result!!).isNotNull()
418             assertThat(result.get(KeyguardRemotePreviewManager.KEY_PREVIEW_SURFACE_PACKAGE))
419                 .isEqualTo(previewSurfacePackage)
420             assertThat(result.containsKey(KeyguardRemotePreviewManager.KEY_PREVIEW_CALLBACK))
421         }
422 
423     private fun insertSelection(
424         slotId: String,
425         affordanceId: String,
426     ) {
427         context.contentResolver.insert(
428             Contract.LockScreenQuickAffordances.SelectionTable.URI,
429             ContentValues().apply {
430                 put(Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID, slotId)
431                 put(
432                     Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID,
433                     affordanceId
434                 )
435             }
436         )
437     }
438 
439     private fun querySelections(): List<Selection> {
440         return context.contentResolver
441             .query(
442                 Contract.LockScreenQuickAffordances.SelectionTable.URI,
443                 null,
444                 null,
445                 null,
446                 null,
447             )
448             ?.use { cursor ->
449                 buildList {
450                     val slotIdColumnIndex =
451                         cursor.getColumnIndex(
452                             Contract.LockScreenQuickAffordances.SelectionTable.Columns.SLOT_ID
453                         )
454                     val affordanceIdColumnIndex =
455                         cursor.getColumnIndex(
456                             Contract.LockScreenQuickAffordances.SelectionTable.Columns.AFFORDANCE_ID
457                         )
458                     val affordanceNameColumnIndex =
459                         cursor.getColumnIndex(
460                             Contract.LockScreenQuickAffordances.SelectionTable.Columns
461                                 .AFFORDANCE_NAME
462                         )
463                     if (
464                         slotIdColumnIndex == -1 ||
465                             affordanceIdColumnIndex == -1 ||
466                             affordanceNameColumnIndex == -1
467                     ) {
468                         return@buildList
469                     }
470 
471                     while (cursor.moveToNext()) {
472                         add(
473                             Selection(
474                                 slotId = cursor.getString(slotIdColumnIndex),
475                                 affordanceId = cursor.getString(affordanceIdColumnIndex),
476                                 affordanceName = cursor.getString(affordanceNameColumnIndex),
477                             )
478                         )
479                     }
480                 }
481             }
482             ?: emptyList()
483     }
484 
485     private fun querySlots(): List<Slot> {
486         return context.contentResolver
487             .query(
488                 Contract.LockScreenQuickAffordances.SlotTable.URI,
489                 null,
490                 null,
491                 null,
492                 null,
493             )
494             ?.use { cursor ->
495                 buildList {
496                     val idColumnIndex =
497                         cursor.getColumnIndex(
498                             Contract.LockScreenQuickAffordances.SlotTable.Columns.ID
499                         )
500                     val capacityColumnIndex =
501                         cursor.getColumnIndex(
502                             Contract.LockScreenQuickAffordances.SlotTable.Columns.CAPACITY
503                         )
504                     if (idColumnIndex == -1 || capacityColumnIndex == -1) {
505                         return@buildList
506                     }
507 
508                     while (cursor.moveToNext()) {
509                         add(
510                             Slot(
511                                 id = cursor.getString(idColumnIndex),
512                                 capacity = cursor.getInt(capacityColumnIndex),
513                             )
514                         )
515                     }
516                 }
517             }
518             ?: emptyList()
519     }
520 
521     private fun queryAffordances(): List<Affordance> {
522         return context.contentResolver
523             .query(
524                 Contract.LockScreenQuickAffordances.AffordanceTable.URI,
525                 null,
526                 null,
527                 null,
528                 null,
529             )
530             ?.use { cursor ->
531                 buildList {
532                     val idColumnIndex =
533                         cursor.getColumnIndex(
534                             Contract.LockScreenQuickAffordances.AffordanceTable.Columns.ID
535                         )
536                     val nameColumnIndex =
537                         cursor.getColumnIndex(
538                             Contract.LockScreenQuickAffordances.AffordanceTable.Columns.NAME
539                         )
540                     val iconColumnIndex =
541                         cursor.getColumnIndex(
542                             Contract.LockScreenQuickAffordances.AffordanceTable.Columns.ICON
543                         )
544                     if (idColumnIndex == -1 || nameColumnIndex == -1 || iconColumnIndex == -1) {
545                         return@buildList
546                     }
547 
548                     while (cursor.moveToNext()) {
549                         add(
550                             Affordance(
551                                 id = cursor.getString(idColumnIndex),
552                                 name = cursor.getString(nameColumnIndex),
553                                 iconResourceId = cursor.getInt(iconColumnIndex),
554                             )
555                         )
556                     }
557                 }
558             }
559             ?: emptyList()
560     }
561 
562     data class Slot(
563         val id: String,
564         val capacity: Int,
565     )
566 
567     data class Affordance(
568         val id: String,
569         val name: String,
570         val iconResourceId: Int,
571     )
572 
573     data class Selection(
574         val slotId: String,
575         val affordanceId: String,
576         val affordanceName: String,
577     )
578 
579     companion object {
580         private const val AFFORDANCE_1 = "affordance_1"
581         private const val AFFORDANCE_2 = "affordance_2"
582         private const val AFFORDANCE_1_NAME = "affordance_1_name"
583         private const val AFFORDANCE_2_NAME = "affordance_2_name"
584     }
585 }
586