• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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