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.domain.interactor 19 20 import android.app.AlertDialog 21 import android.app.admin.DevicePolicyManager 22 import android.content.Context 23 import android.content.Intent 24 import android.util.Log 25 import com.android.app.tracing.coroutines.withContext 26 import com.android.compose.animation.scene.ObservableTransitionState 27 import com.android.internal.widget.LockPatternUtils 28 import com.android.systemui.animation.DialogTransitionAnimator 29 import com.android.systemui.animation.Expandable 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dagger.qualifiers.Application 32 import com.android.systemui.dagger.qualifiers.Background 33 import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled 34 import com.android.systemui.dock.DockManager 35 import com.android.systemui.dock.retrieveIsDocked 36 import com.android.systemui.flags.FeatureFlags 37 import com.android.systemui.flags.Flags 38 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig 39 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository 40 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository 41 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel 42 import com.android.systemui.keyguard.shared.model.KeyguardPickerFlag 43 import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation 44 import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation 45 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition 46 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger 47 import com.android.systemui.plugins.ActivityStarter 48 import com.android.systemui.res.R 49 import com.android.systemui.scene.domain.interactor.SceneInteractor 50 import com.android.systemui.scene.shared.flag.SceneContainerFlag 51 import com.android.systemui.scene.shared.model.Scenes 52 import com.android.systemui.settings.UserTracker 53 import com.android.systemui.shade.domain.interactor.ShadeInteractor 54 import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract 55 import com.android.systemui.statusbar.phone.SystemUIDialog 56 import com.android.systemui.statusbar.policy.KeyguardStateController 57 import dagger.Lazy 58 import javax.inject.Inject 59 import kotlinx.coroutines.CoroutineDispatcher 60 import kotlinx.coroutines.ExperimentalCoroutinesApi 61 import kotlinx.coroutines.flow.Flow 62 import kotlinx.coroutines.flow.combine 63 import kotlinx.coroutines.flow.distinctUntilChanged 64 import kotlinx.coroutines.flow.flatMapLatest 65 import kotlinx.coroutines.flow.flowOf 66 import kotlinx.coroutines.flow.map 67 import kotlinx.coroutines.flow.onStart 68 69 @OptIn(ExperimentalCoroutinesApi::class) 70 @SysUISingleton 71 class KeyguardQuickAffordanceInteractor 72 @Inject 73 constructor( 74 private val keyguardInteractor: KeyguardInteractor, 75 private val shadeInteractor: ShadeInteractor, 76 private val lockPatternUtils: LockPatternUtils, 77 private val keyguardStateController: KeyguardStateController, 78 private val userTracker: UserTracker, 79 private val activityStarter: ActivityStarter, 80 private val featureFlags: FeatureFlags, 81 private val repository: Lazy<KeyguardQuickAffordanceRepository>, 82 private val launchAnimator: DialogTransitionAnimator, 83 private val logger: KeyguardQuickAffordancesMetricsLogger, 84 private val devicePolicyManager: DevicePolicyManager, 85 private val dockManager: DockManager, 86 private val biometricSettingsRepository: BiometricSettingsRepository, 87 @Background private val backgroundDispatcher: CoroutineDispatcher, 88 @Application private val appContext: Context, 89 private val sceneInteractor: Lazy<SceneInteractor>, 90 ) { 91 92 /** 93 * Whether the UI should use the long press gesture to activate quick affordances. 94 * 95 * If `false`, the UI goes back to using single taps. 96 */ 97 fun useLongPress(): Flow<Boolean> = dockManager.retrieveIsDocked().map { !it } 98 99 /** Returns an observable for the quick affordance at the given position. */ 100 suspend fun quickAffordance( 101 position: KeyguardQuickAffordancePosition 102 ): Flow<KeyguardQuickAffordanceModel> { 103 if (isFeatureDisabledByDevicePolicy()) { 104 return flowOf(KeyguardQuickAffordanceModel.Hidden) 105 } 106 107 return combine( 108 quickAffordanceAlwaysVisible(position), 109 keyguardInteractor.isDozing, 110 if (SceneContainerFlag.isEnabled) { 111 sceneInteractor 112 .get() 113 .transitionState 114 .map { 115 when (it) { 116 is ObservableTransitionState.Idle -> 117 it.currentScene == Scenes.Lockscreen 118 is ObservableTransitionState.Transition -> 119 it.fromScene == Scenes.Lockscreen || it.toScene == Scenes.Lockscreen 120 } 121 } 122 .distinctUntilChanged() 123 } else { 124 keyguardInteractor.isKeyguardShowing 125 }, 126 shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(), 127 biometricSettingsRepository.isCurrentUserInLockdown, 128 ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown -> 129 if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) { 130 affordance 131 } else { 132 KeyguardQuickAffordanceModel.Hidden 133 } 134 } 135 } 136 137 /** 138 * Returns an observable for the quick affordance at the given position but always visible, 139 * regardless of lock screen state. 140 * 141 * This is useful for experiences like the lock screen preview mode, where the affordances must 142 * always be visible. 143 */ 144 suspend fun quickAffordanceAlwaysVisible( 145 position: KeyguardQuickAffordancePosition, 146 ): Flow<KeyguardQuickAffordanceModel> { 147 return if (isFeatureDisabledByDevicePolicy()) { 148 flowOf(KeyguardQuickAffordanceModel.Hidden) 149 } else { 150 quickAffordanceInternal(position) 151 } 152 } 153 154 /** 155 * Notifies that a quick affordance has been "triggered" (clicked) by the user. 156 * 157 * @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of 158 * the affordance that was clicked 159 * @param expandable An optional [Expandable] for the activity- or dialog-launch animation 160 * @param slotId The id of the lockscreen slot that the affordance is in 161 */ 162 fun onQuickAffordanceTriggered( 163 configKey: String, 164 expandable: Expandable?, 165 slotId: String, 166 ) { 167 val (decodedSlotId, decodedConfigKey) = configKey.decode() 168 val config = 169 repository.get().selections.value[decodedSlotId]?.find { it.key == decodedConfigKey } 170 if (config == null) { 171 Log.e(TAG, "Affordance config with key of \"$configKey\" not found!") 172 return 173 } 174 logger.logOnShortcutTriggered(slotId, configKey) 175 176 when (val result = config.onTriggered(expandable)) { 177 is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> 178 launchQuickAffordance( 179 intent = result.intent, 180 canShowWhileLocked = result.canShowWhileLocked, 181 expandable = expandable, 182 ) 183 is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> Unit 184 is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog -> 185 showDialog( 186 result.dialog, 187 result.expandable, 188 ) 189 } 190 } 191 192 /** 193 * Selects an affordance with the given ID on the slot with the given ID. 194 * 195 * @return `true` if the affordance was selected successfully; `false` otherwise. 196 */ 197 suspend fun select(slotId: String, affordanceId: String): Boolean { 198 if (isFeatureDisabledByDevicePolicy()) { 199 return false 200 } 201 202 val slots = repository.get().getSlotPickerRepresentations() 203 val slot = slots.find { it.id == slotId } ?: return false 204 val selections = 205 repository 206 .get() 207 .getCurrentSelections() 208 .getOrDefault(slotId, emptyList()) 209 .toMutableList() 210 val alreadySelected = selections.remove(affordanceId) 211 if (!alreadySelected) { 212 while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) { 213 selections.removeAt(0) 214 } 215 } 216 217 selections.add(affordanceId) 218 219 repository 220 .get() 221 .setSelections( 222 slotId = slotId, 223 affordanceIds = selections, 224 ) 225 226 logger.logOnShortcutSelected(slotId, affordanceId) 227 return true 228 } 229 230 /** 231 * Unselects one or all affordances from the slot with the given ID. 232 * 233 * @param slotId The ID of the slot. 234 * @param affordanceId The ID of the affordance to remove; if `null`, removes all affordances 235 * from the slot. 236 * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if 237 * the affordance was not on the slot to begin with). 238 */ 239 suspend fun unselect(slotId: String, affordanceId: String?): Boolean { 240 if (isFeatureDisabledByDevicePolicy()) { 241 return false 242 } 243 244 val slots = repository.get().getSlotPickerRepresentations() 245 if (slots.find { it.id == slotId } == null) { 246 return false 247 } 248 249 if (affordanceId.isNullOrEmpty()) { 250 return if ( 251 repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).isEmpty() 252 ) { 253 false 254 } else { 255 repository.get().setSelections(slotId = slotId, affordanceIds = emptyList()) 256 true 257 } 258 } 259 260 val selections = 261 repository 262 .get() 263 .getCurrentSelections() 264 .getOrDefault(slotId, emptyList()) 265 .toMutableList() 266 return if (selections.remove(affordanceId)) { 267 repository 268 .get() 269 .setSelections( 270 slotId = slotId, 271 affordanceIds = selections, 272 ) 273 true 274 } else { 275 false 276 } 277 } 278 279 /** Returns affordance IDs indexed by slot ID, for all known slots. */ 280 suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> { 281 if (isFeatureDisabledByDevicePolicy()) { 282 return emptyMap() 283 } 284 285 val slots = repository.get().getSlotPickerRepresentations() 286 val selections = repository.get().getCurrentSelections() 287 val affordanceById = 288 getAffordancePickerRepresentations().associateBy { affordance -> affordance.id } 289 return slots.associate { slot -> 290 slot.id to 291 (selections[slot.id] ?: emptyList()).mapNotNull { affordanceId -> 292 affordanceById[affordanceId] 293 } 294 } 295 } 296 297 private fun quickAffordanceInternal( 298 position: KeyguardQuickAffordancePosition 299 ): Flow<KeyguardQuickAffordanceModel> = 300 repository 301 .get() 302 .selections 303 .map { it[position.toSlotId()] ?: emptyList() } 304 .flatMapLatest { configs -> combinedConfigs(position, configs) } 305 306 private fun combinedConfigs( 307 position: KeyguardQuickAffordancePosition, 308 configs: List<KeyguardQuickAffordanceConfig>, 309 ): Flow<KeyguardQuickAffordanceModel> { 310 if (configs.isEmpty()) { 311 return flowOf(KeyguardQuickAffordanceModel.Hidden) 312 } 313 314 return combine( 315 configs.map { config -> 316 // We emit an initial "Hidden" value to make sure that there's always an 317 // initial value and avoid subtle bugs where the downstream isn't receiving 318 // any values because one config implementation is not emitting an initial 319 // value. For example, see b/244296596. 320 config.lockScreenState.onStart { 321 emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden) 322 } 323 } 324 ) { states -> 325 val index = 326 states.indexOfFirst { state -> 327 state is KeyguardQuickAffordanceConfig.LockScreenState.Visible 328 } 329 if (index != -1) { 330 val visibleState = 331 states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible 332 val configKey = configs[index].key 333 KeyguardQuickAffordanceModel.Visible( 334 configKey = configKey.encode(position.toSlotId()), 335 icon = visibleState.icon, 336 activationState = visibleState.activationState, 337 ) 338 } else { 339 KeyguardQuickAffordanceModel.Hidden 340 } 341 } 342 } 343 344 private fun showDialog(dialog: AlertDialog, expandable: Expandable?) { 345 expandable?.dialogTransitionController()?.let { controller -> 346 SystemUIDialog.applyFlags(dialog) 347 SystemUIDialog.setShowForAllUsers(dialog, true) 348 SystemUIDialog.registerDismissListener(dialog) 349 SystemUIDialog.setDialogSize(dialog) 350 launchAnimator.show(dialog, controller) 351 } 352 } 353 354 private fun launchQuickAffordance( 355 intent: Intent, 356 canShowWhileLocked: Boolean, 357 expandable: Expandable?, 358 ) { 359 @LockPatternUtils.StrongAuthTracker.StrongAuthFlags 360 val strongAuthFlags = 361 lockPatternUtils.getStrongAuthForUser(userTracker.userHandle.identifier) 362 val needsToUnlockFirst = 363 when { 364 strongAuthFlags == 365 LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT -> true 366 !canShowWhileLocked && !keyguardStateController.isUnlocked -> true 367 else -> false 368 } 369 if (needsToUnlockFirst) { 370 activityStarter.postStartActivityDismissingKeyguard( 371 intent, 372 0 /* delay */, 373 expandable?.activityTransitionController(), 374 ) 375 } else { 376 activityStarter.startActivity( 377 intent, 378 true /* dismissShade */, 379 expandable?.activityTransitionController(), 380 true /* showOverLockscreenWhenLocked */, 381 ) 382 } 383 } 384 385 private fun String.encode(slotId: String): String { 386 return "$slotId$DELIMITER$this" 387 } 388 389 private fun String.decode(): Pair<String, String> { 390 val splitUp = this.split(DELIMITER) 391 return Pair(splitUp[0], splitUp[1]) 392 } 393 394 suspend fun getAffordancePickerRepresentations(): 395 List<KeyguardQuickAffordancePickerRepresentation> { 396 return repository.get().getAffordancePickerRepresentations() 397 } 398 399 suspend fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> { 400 if (isFeatureDisabledByDevicePolicy()) { 401 return emptyList() 402 } 403 404 return repository.get().getSlotPickerRepresentations() 405 } 406 407 suspend fun getPickerFlags(): List<KeyguardPickerFlag> { 408 return listOf( 409 KeyguardPickerFlag( 410 name = Contract.FlagsTable.FLAG_NAME_CUSTOM_LOCK_SCREEN_QUICK_AFFORDANCES_ENABLED, 411 value = 412 !isFeatureDisabledByDevicePolicy() && 413 appContext.resources.getBoolean(R.bool.custom_lockscreen_shortcuts_enabled), 414 ), 415 KeyguardPickerFlag( 416 name = Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED, 417 value = featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), 418 ), 419 KeyguardPickerFlag( 420 name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW, 421 value = featureFlags.isEnabled(Flags.WALLPAPER_FULLSCREEN_PREVIEW), 422 ), 423 KeyguardPickerFlag( 424 name = Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME, 425 value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME) 426 ), 427 KeyguardPickerFlag( 428 name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_UI_FOR_AIWP, 429 value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_UI_FOR_AIWP) 430 ), 431 KeyguardPickerFlag( 432 name = Contract.FlagsTable.FLAG_NAME_TRANSIT_CLOCK, 433 value = featureFlags.isEnabled(Flags.TRANSIT_CLOCK) 434 ), 435 KeyguardPickerFlag( 436 name = Contract.FlagsTable.FLAG_NAME_PAGE_TRANSITIONS, 437 value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS) 438 ), 439 KeyguardPickerFlag( 440 name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_PREVIEW_ANIMATION, 441 value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PREVIEW_ANIMATION) 442 ), 443 ) 444 } 445 446 private suspend fun isFeatureDisabledByDevicePolicy(): Boolean = 447 withContext("$TAG#isFeatureDisabledByDevicePolicy", backgroundDispatcher) { 448 devicePolicyManager.areKeyguardShortcutsDisabled(userId = userTracker.userId) 449 } 450 451 companion object { 452 private const val TAG = "KeyguardQuickAffordanceInteractor" 453 private const val DELIMITER = "::" 454 } 455 } 456