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