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