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