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