blob: 568cc0f426391a8415056967143580d03c9b0824 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.systemui.keyguard.domain.interactor
import android.app.AlertDialog
import android.app.admin.DevicePolicyManager
import android.content.Intent
import android.util.Log
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry
import com.android.systemui.keyguard.shared.model.KeyguardPickerFlag
import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract as Contract
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardQuickAffordanceInteractor
@Inject
constructor(
private val keyguardInteractor: KeyguardInteractor,
private val registry: KeyguardQuickAffordanceRegistry<out KeyguardQuickAffordanceConfig>,
private val lockPatternUtils: LockPatternUtils,
private val keyguardStateController: KeyguardStateController,
private val userTracker: UserTracker,
private val activityStarter: ActivityStarter,
private val featureFlags: FeatureFlags,
private val repository: Lazy<KeyguardQuickAffordanceRepository>,
private val launchAnimator: DialogLaunchAnimator,
private val devicePolicyManager: DevicePolicyManager,
@Background private val backgroundDispatcher: CoroutineDispatcher,
) {
private val isUsingRepository: Boolean
get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
/**
* Whether the UI should use the long press gesture to activate quick affordances.
*
* If `false`, the UI goes back to using single taps.
*/
val useLongPress: Boolean
get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
/** Returns an observable for the quick affordance at the given position. */
suspend fun quickAffordance(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceModel> {
if (isFeatureDisabledByDevicePolicy()) {
return flowOf(KeyguardQuickAffordanceModel.Hidden)
}
return combine(
quickAffordanceAlwaysVisible(position),
keyguardInteractor.isDozing,
keyguardInteractor.isKeyguardShowing,
) { affordance, isDozing, isKeyguardShowing ->
if (!isDozing && isKeyguardShowing) {
affordance
} else {
KeyguardQuickAffordanceModel.Hidden
}
}
}
/**
* Returns an observable for the quick affordance at the given position but always visible,
* regardless of lock screen state.
*
* This is useful for experiences like the lock screen preview mode, where the affordances must
* always be visible.
*/
fun quickAffordanceAlwaysVisible(
position: KeyguardQuickAffordancePosition,
): Flow<KeyguardQuickAffordanceModel> {
return quickAffordanceInternal(position)
}
/**
* Notifies that a quick affordance has been "triggered" (clicked) by the user.
*
* @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of
* the affordance that was clicked
* @param expandable An optional [Expandable] for the activity- or dialog-launch animation
*/
fun onQuickAffordanceTriggered(
configKey: String,
expandable: Expandable?,
) {
@Suppress("UNCHECKED_CAST")
val config =
if (isUsingRepository) {
val (slotId, decodedConfigKey) = configKey.decode()
repository.get().selections.value[slotId]?.find { it.key == decodedConfigKey }
} else {
registry.get(configKey)
}
if (config == null) {
Log.e(TAG, "Affordance config with key of \"$configKey\" not found!")
return
}
when (val result = config.onTriggered(expandable)) {
is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity ->
launchQuickAffordance(
intent = result.intent,
canShowWhileLocked = result.canShowWhileLocked,
expandable = expandable,
)
is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> Unit
is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog ->
showDialog(
result.dialog,
result.expandable,
)
}
}
/**
* Selects an affordance with the given ID on the slot with the given ID.
*
* @return `true` if the affordance was selected successfully; `false` otherwise.
*/
suspend fun select(slotId: String, affordanceId: String): Boolean {
check(isUsingRepository)
if (isFeatureDisabledByDevicePolicy()) {
return false
}
val slots = repository.get().getSlotPickerRepresentations()
val slot = slots.find { it.id == slotId } ?: return false
val selections =
repository
.get()
.getCurrentSelections()
.getOrDefault(slotId, emptyList())
.toMutableList()
val alreadySelected = selections.remove(affordanceId)
if (!alreadySelected) {
while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
selections.removeAt(0)
}
}
selections.add(affordanceId)
repository
.get()
.setSelections(
slotId = slotId,
affordanceIds = selections,
)
return true
}
/**
* Unselects one or all affordances from the slot with the given ID.
*
* @param slotId The ID of the slot.
* @param affordanceId The ID of the affordance to remove; if `null`, removes all affordances
* from the slot.
* @return `true` if the affordance was successfully removed; `false` otherwise (for example, if
* the affordance was not on the slot to begin with).
*/
suspend fun unselect(slotId: String, affordanceId: String?): Boolean {
check(isUsingRepository)
if (isFeatureDisabledByDevicePolicy()) {
return false
}
val slots = repository.get().getSlotPickerRepresentations()
if (slots.find { it.id == slotId } == null) {
return false
}
if (affordanceId.isNullOrEmpty()) {
return if (
repository.get().getCurrentSelections().getOrDefault(slotId, emptyList()).isEmpty()
) {
false
} else {
repository.get().setSelections(slotId = slotId, affordanceIds = emptyList())
true
}
}
val selections =
repository
.get()
.getCurrentSelections()
.getOrDefault(slotId, emptyList())
.toMutableList()
return if (selections.remove(affordanceId)) {
repository
.get()
.setSelections(
slotId = slotId,
affordanceIds = selections,
)
true
} else {
false
}
}
/** Returns affordance IDs indexed by slot ID, for all known slots. */
suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
if (isFeatureDisabledByDevicePolicy()) {
return emptyMap()
}
val slots = repository.get().getSlotPickerRepresentations()
val selections = repository.get().getCurrentSelections()
val affordanceById =
getAffordancePickerRepresentations().associateBy { affordance -> affordance.id }
return slots.associate { slot ->
slot.id to
(selections[slot.id] ?: emptyList()).mapNotNull { affordanceId ->
affordanceById[affordanceId]
}
}
}
private fun quickAffordanceInternal(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceModel> {
return if (isUsingRepository) {
repository
.get()
.selections
.map { it[position.toSlotId()] ?: emptyList() }
.flatMapLatest { configs -> combinedConfigs(position, configs) }
} else {
combinedConfigs(position, registry.getAll(position))
}
}
private fun combinedConfigs(
position: KeyguardQuickAffordancePosition,
configs: List<KeyguardQuickAffordanceConfig>,
): Flow<KeyguardQuickAffordanceModel> {
if (configs.isEmpty()) {
return flowOf(KeyguardQuickAffordanceModel.Hidden)
}
return combine(
configs.map { config ->
// We emit an initial "Hidden" value to make sure that there's always an
// initial value and avoid subtle bugs where the downstream isn't receiving
// any values because one config implementation is not emitting an initial
// value. For example, see b/244296596.
config.lockScreenState.onStart {
emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
}
}
) { states ->
val index =
states.indexOfFirst { state ->
state is KeyguardQuickAffordanceConfig.LockScreenState.Visible
}
if (index != -1) {
val visibleState =
states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible
val configKey = configs[index].key
KeyguardQuickAffordanceModel.Visible(
configKey =
if (isUsingRepository) {
configKey.encode(position.toSlotId())
} else {
configKey
},
icon = visibleState.icon,
activationState = visibleState.activationState,
)
} else {
KeyguardQuickAffordanceModel.Hidden
}
}
}
private fun showDialog(dialog: AlertDialog, expandable: Expandable?) {
expandable?.dialogLaunchController()?.let { controller ->
SystemUIDialog.applyFlags(dialog)
SystemUIDialog.setShowForAllUsers(dialog, true)
SystemUIDialog.registerDismissListener(dialog)
SystemUIDialog.setDialogSize(dialog)
launchAnimator.show(dialog, controller)
}
}
private fun launchQuickAffordance(
intent: Intent,
canShowWhileLocked: Boolean,
expandable: Expandable?,
) {
@LockPatternUtils.StrongAuthTracker.StrongAuthFlags
val strongAuthFlags =
lockPatternUtils.getStrongAuthForUser(userTracker.userHandle.identifier)
val needsToUnlockFirst =
when {
strongAuthFlags ==
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT -> true
!canShowWhileLocked && !keyguardStateController.isUnlocked -> true
else -> false
}
if (needsToUnlockFirst) {
activityStarter.postStartActivityDismissingKeyguard(
intent,
0 /* delay */,
expandable?.activityLaunchController(),
)
} else {
activityStarter.startActivity(
intent,
true /* dismissShade */,
expandable?.activityLaunchController(),
true /* showOverLockscreenWhenLocked */,
)
}
}
private fun String.encode(slotId: String): String {
return "$slotId$DELIMITER$this"
}
private fun String.decode(): Pair<String, String> {
val splitUp = this.split(DELIMITER)
return Pair(splitUp[0], splitUp[1])
}
suspend fun getAffordancePickerRepresentations():
List<KeyguardQuickAffordancePickerRepresentation> {
return repository.get().getAffordancePickerRepresentations()
}
suspend fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
check(isUsingRepository)
if (isFeatureDisabledByDevicePolicy()) {
return emptyList()
}
return repository.get().getSlotPickerRepresentations()
}
suspend fun getPickerFlags(): List<KeyguardPickerFlag> {
return listOf(
KeyguardPickerFlag(
name = Contract.FlagsTable.FLAG_NAME_REVAMPED_WALLPAPER_UI,
value = featureFlags.isEnabled(Flags.REVAMPED_WALLPAPER_UI),
),
KeyguardPickerFlag(
name = Contract.FlagsTable.FLAG_NAME_CUSTOM_LOCK_SCREEN_QUICK_AFFORDANCES_ENABLED,
value =
!isFeatureDisabledByDevicePolicy() &&
featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES),
),
KeyguardPickerFlag(
name = Contract.FlagsTable.FLAG_NAME_CUSTOM_CLOCKS_ENABLED,
value = featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS),
),
KeyguardPickerFlag(
name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_FULLSCREEN_PREVIEW,
value = featureFlags.isEnabled(Flags.WALLPAPER_FULLSCREEN_PREVIEW),
),
KeyguardPickerFlag(
name = Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME,
value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME)
)
)
}
private suspend fun isFeatureDisabledByDevicePolicy(): Boolean {
val flags =
withContext(backgroundDispatcher) {
devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId)
}
val flagsToCheck =
DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL or
DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL
return flagsToCheck and flags != 0
}
companion object {
private const val TAG = "KeyguardQuickAffordanceInteractor"
private const val DELIMITER = "::"
}
}