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