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