blob: b5954e5e3ddb6effa5edfe0ca556b28186b47287 [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.customization.picker.quickaffordance.ui.viewmodel
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import androidx.annotation.DrawableRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
import com.android.systemui.shared.customization.data.content.CustomizationProviderContract
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
import com.android.wallpaper.R
import com.android.wallpaper.module.CurrentWallpaperInfoFactory
import com.android.wallpaper.picker.common.button.ui.viewmodel.ButtonStyle
import com.android.wallpaper.picker.common.button.ui.viewmodel.ButtonViewModel
import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel
import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel
import com.android.wallpaper.picker.undo.domain.interactor.UndoInteractor
import com.android.wallpaper.picker.undo.ui.viewmodel.UndoViewModel
import com.android.wallpaper.util.PreviewUtils
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
/** Models UI state for a lock screen quick affordance picker experience. */
@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardQuickAffordancePickerViewModel
private constructor(
context: Context,
private val quickAffordanceInteractor: KeyguardQuickAffordancePickerInteractor,
undoInteractor: UndoInteractor,
private val wallpaperInfoFactory: CurrentWallpaperInfoFactory,
activityStarter: (Intent) -> Unit,
) : ViewModel() {
@SuppressLint("StaticFieldLeak") private val applicationContext = context.applicationContext
val preview =
ScreenPreviewViewModel(
previewUtils =
PreviewUtils(
context = applicationContext,
authority =
applicationContext.getString(
R.string.lock_screen_preview_provider_authority,
),
),
initialExtrasProvider = {
Bundle().apply {
putString(
KeyguardQuickAffordancePreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
selectedSlotId.value,
)
}
},
wallpaperInfoProvider = {
suspendCancellableCoroutine { continuation ->
wallpaperInfoFactory.createCurrentWallpaperInfos(
{ homeWallpaper, lockWallpaper, _ ->
continuation.resume(lockWallpaper ?: homeWallpaper, null)
},
/* forceRefresh= */ true,
)
}
},
)
val undo: UndoViewModel =
UndoViewModel(
interactor = undoInteractor,
)
private val _selectedSlotId = MutableStateFlow<String?>(null)
val selectedSlotId: StateFlow<String?> = _selectedSlotId.asStateFlow()
/** View-models for each slot, keyed by slot ID. */
val slots: Flow<Map<String, KeyguardQuickAffordanceSlotViewModel>> =
combine(
quickAffordanceInteractor.slots,
quickAffordanceInteractor.affordances,
quickAffordanceInteractor.selections,
selectedSlotId,
) { slots, affordances, selections, selectedSlotIdOrNull ->
slots
.mapIndexed { index, slot ->
val selectedAffordanceIds =
selections
.filter { selection -> selection.slotId == slot.id }
.map { selection -> selection.affordanceId }
.toSet()
val selectedAffordances =
affordances.filter { affordance ->
selectedAffordanceIds.contains(affordance.id)
}
val isSelected =
(selectedSlotIdOrNull == null && index == 0) ||
selectedSlotIdOrNull == slot.id
slot.id to
KeyguardQuickAffordanceSlotViewModel(
name = getSlotName(slot.id),
isSelected = isSelected,
selectedQuickAffordances =
selectedAffordances.map { affordanceModel ->
KeyguardQuickAffordanceViewModel(
icon = getAffordanceIcon(affordanceModel.iconResourceId),
contentDescription = affordanceModel.name,
isSelected = true,
onClicked = null,
onLongClicked = null,
isEnabled = affordanceModel.isEnabled,
)
},
maxSelectedQuickAffordances = slot.maxSelectedQuickAffordances,
onClicked =
if (isSelected) {
null
} else {
{ _selectedSlotId.tryEmit(slot.id) }
},
)
}
.toMap()
}
/** The list of all available quick affordances for the selected slot. */
val quickAffordances: Flow<List<KeyguardQuickAffordanceViewModel>> =
combine(
quickAffordanceInteractor.slots,
quickAffordanceInteractor.affordances,
quickAffordanceInteractor.selections,
selectedSlotId,
) { slots, affordances, selections, selectedSlotIdOrNull ->
val selectedSlot =
selectedSlotIdOrNull?.let { slots.find { slot -> slot.id == it } } ?: slots.first()
val selectedAffordanceIds =
selections
.filter { selection -> selection.slotId == selectedSlot.id }
.map { selection -> selection.affordanceId }
.toSet()
listOf(
none(
slotId = selectedSlot.id,
isSelected = selectedAffordanceIds.isEmpty(),
)
) +
affordances.map { affordance ->
val isSelected = selectedAffordanceIds.contains(affordance.id)
val affordanceIcon = getAffordanceIcon(affordance.iconResourceId)
KeyguardQuickAffordanceViewModel(
icon = affordanceIcon,
contentDescription = affordance.name,
isSelected = isSelected,
onClicked =
if (affordance.isEnabled) {
{
viewModelScope.launch {
if (isSelected) {
quickAffordanceInteractor.unselect(
slotId = selectedSlot.id,
affordanceId = affordance.id,
)
} else {
quickAffordanceInteractor.select(
slotId = selectedSlot.id,
affordanceId = affordance.id,
)
}
}
}
} else {
{
showEnablementDialog(
icon = affordanceIcon,
name = affordance.name,
instructions = affordance.enablementInstructions,
actionText = affordance.enablementActionText,
actionComponentName =
affordance.enablementActionComponentName,
)
}
},
onLongClicked =
if (affordance.configureIntent != null) {
{ activityStarter(affordance.configureIntent) }
} else {
null
},
isEnabled = affordance.isEnabled,
)
}
}
@SuppressLint("UseCompatLoadingForDrawables")
val summary: Flow<KeyguardQuickAffordanceSummaryViewModel> =
slots.map { slots ->
val icon2 =
slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]
?.selectedQuickAffordances
?.firstOrNull()
?.icon
val icon1 =
slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]
?.selectedQuickAffordances
?.firstOrNull()
?.icon
KeyguardQuickAffordanceSummaryViewModel(
description = toDescriptionText(context, slots),
icon1 = icon1
?: if (icon2 == null) {
context.getDrawable(R.drawable.link_off)
} else {
null
},
icon2 = icon2,
)
}
private val _dialog = MutableStateFlow<DialogViewModel?>(null)
/**
* The current dialog to show. If `null`, no dialog should be shown.
*
* When the dialog is dismissed, [onDialogDismissed] must be called.
*/
val dialog: Flow<DialogViewModel?> = _dialog.asStateFlow()
/** Notifies that the dialog has been dismissed in the UI. */
fun onDialogDismissed() {
_dialog.value = null
}
private fun showEnablementDialog(
icon: Drawable,
name: String,
instructions: List<String>,
actionText: String?,
actionComponentName: String?,
) {
_dialog.value =
DialogViewModel(
icon =
Icon.Loaded(
drawable = icon,
contentDescription = null,
),
title = Text.Loaded(name),
message =
Text.Loaded(
buildString {
instructions.forEachIndexed { index, instruction ->
if (index > 0) {
append('\n')
}
append(instruction)
}
}
),
buttons =
listOf(
ButtonViewModel(
text = actionText?.let { Text.Loaded(actionText) }
?: Text.Resource(
R.string
.keyguard_affordance_enablement_dialog_dismiss_button,
),
style = ButtonStyle.Primary,
onClicked = {
actionComponentName.toIntent()?.let { intent ->
applicationContext.startActivity(intent)
}
}
),
),
)
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun none(
slotId: String,
isSelected: Boolean,
): KeyguardQuickAffordanceViewModel {
return KeyguardQuickAffordanceViewModel.none(
context = applicationContext,
isSelected = isSelected,
onSelected = {
viewModelScope.launch { quickAffordanceInteractor.unselectAll(slotId) }
},
)
}
private fun getSlotName(slotId: String): String {
return applicationContext.getString(
when (slotId) {
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START ->
R.string.keyguard_slot_name_bottom_start
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END ->
R.string.keyguard_slot_name_bottom_end
else -> error("No name for slot with ID of \"$slotId\"!")
}
)
}
private suspend fun getAffordanceIcon(@DrawableRes iconResourceId: Int): Drawable {
return quickAffordanceInteractor.getAffordanceIcon(iconResourceId)
}
private fun String?.toIntent(): Intent? {
if (isNullOrEmpty()) {
return null
}
val splitUp =
split(
CustomizationProviderContract.LockScreenQuickAffordances.AffordanceTable
.COMPONENT_NAME_SEPARATOR
)
check(splitUp.size == 1 || splitUp.size == 2) {
"Illegal component name \"$this\". Must be either just an action or a package and an" +
" action separated by a" +
" \"${CustomizationProviderContract.LockScreenQuickAffordances.AffordanceTable.COMPONENT_NAME_SEPARATOR}\"!"
}
return Intent(splitUp.last()).apply {
if (splitUp.size > 1) {
setPackage(splitUp[0])
}
}
}
private fun toDescriptionText(
context: Context,
slots: Map<String, KeyguardQuickAffordanceSlotViewModel>,
): String {
val bottomStartAffordanceName =
slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]
?.selectedQuickAffordances
?.firstOrNull()
?.contentDescription
val bottomEndAffordanceName =
slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]
?.selectedQuickAffordances
?.firstOrNull()
?.contentDescription
return when {
!bottomStartAffordanceName.isNullOrEmpty() &&
!bottomEndAffordanceName.isNullOrEmpty() -> {
context.getString(
R.string.keyguard_quick_affordance_two_selected_template,
bottomStartAffordanceName,
bottomEndAffordanceName,
)
}
!bottomStartAffordanceName.isNullOrEmpty() -> bottomStartAffordanceName
!bottomEndAffordanceName.isNullOrEmpty() -> bottomEndAffordanceName
else -> context.getString(R.string.keyguard_quick_affordance_none_selected)
}
}
class Factory(
private val context: Context,
private val quickAffordanceInteractor: KeyguardQuickAffordancePickerInteractor,
private val undoInteractor: UndoInteractor,
private val wallpaperInfoFactory: CurrentWallpaperInfoFactory,
private val activityStarter: (Intent) -> Unit,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return KeyguardQuickAffordancePickerViewModel(
context = context,
quickAffordanceInteractor = quickAffordanceInteractor,
undoInteractor = undoInteractor,
wallpaperInfoFactory = wallpaperInfoFactory,
activityStarter = activityStarter,
)
as T
}
}
}