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