blob: b5954e5e3ddb6effa5edfe0ca556b28186b47287 [file] [log] [blame]
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -08001/*
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
Alejandro Nijamkinabda67b2022-11-30 14:34:56 -080018package com.android.customization.picker.quickaffordance.ui.viewmodel
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080019
20import android.annotation.SuppressLint
21import android.content.Context
22import android.content.Intent
23import android.graphics.drawable.Drawable
Alejandro Nijamkinb169b2c2022-12-22 12:25:23 -080024import android.os.Bundle
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080025import androidx.annotation.DrawableRes
26import androidx.lifecycle.ViewModel
27import androidx.lifecycle.ViewModelProvider
28import androidx.lifecycle.viewModelScope
Alejandro Nijamkinabda67b2022-11-30 14:34:56 -080029import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
Alejandro Nijamkin6238c2e2022-12-24 08:11:52 -080030import com.android.systemui.shared.customization.data.content.CustomizationProviderContract
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080031import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
Alejandro Nijamkinb169b2c2022-12-22 12:25:23 -080032import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080033import com.android.wallpaper.R
Alejandro Nijamkin2fe5f2d2022-12-22 15:24:22 -080034import com.android.wallpaper.module.CurrentWallpaperInfoFactory
Alejandro Nijamkind42f5722023-01-17 17:58:41 -080035import com.android.wallpaper.picker.common.button.ui.viewmodel.ButtonStyle
36import com.android.wallpaper.picker.common.button.ui.viewmodel.ButtonViewModel
37import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel
38import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
39import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
Alejandro Nijamkinb169b2c2022-12-22 12:25:23 -080040import com.android.wallpaper.picker.customization.ui.viewmodel.ScreenPreviewViewModel
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -080041import com.android.wallpaper.picker.undo.domain.interactor.UndoInteractor
42import com.android.wallpaper.picker.undo.ui.viewmodel.UndoViewModel
Alejandro Nijamkin2fe5f2d2022-12-22 15:24:22 -080043import com.android.wallpaper.util.PreviewUtils
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080044import kotlinx.coroutines.ExperimentalCoroutinesApi
45import kotlinx.coroutines.flow.Flow
46import kotlinx.coroutines.flow.MutableStateFlow
Alejandro Nijamkin5ec382d2022-12-06 16:43:36 -080047import kotlinx.coroutines.flow.StateFlow
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080048import kotlinx.coroutines.flow.asStateFlow
49import kotlinx.coroutines.flow.combine
Alejandro Nijamkinabda67b2022-11-30 14:34:56 -080050import kotlinx.coroutines.flow.map
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080051import kotlinx.coroutines.launch
Alejandro Nijamkin2fe5f2d2022-12-22 15:24:22 -080052import kotlinx.coroutines.suspendCancellableCoroutine
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080053
54/** Models UI state for a lock screen quick affordance picker experience. */
55@OptIn(ExperimentalCoroutinesApi::class)
56class KeyguardQuickAffordancePickerViewModel
57private constructor(
58 context: Context,
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -080059 private val quickAffordanceInteractor: KeyguardQuickAffordancePickerInteractor,
60 undoInteractor: UndoInteractor,
Alejandro Nijamkin2fe5f2d2022-12-22 15:24:22 -080061 private val wallpaperInfoFactory: CurrentWallpaperInfoFactory,
Alejandro Nijamkin7bda0fd2022-12-28 14:14:20 -080062 activityStarter: (Intent) -> Unit,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -080063) : ViewModel() {
64
65 @SuppressLint("StaticFieldLeak") private val applicationContext = context.applicationContext
66
Alejandro Nijamkin2fe5f2d2022-12-22 15:24:22 -080067 val preview =
68 ScreenPreviewViewModel(
69 previewUtils =
70 PreviewUtils(
71 context = applicationContext,
72 authority =
73 applicationContext.getString(
74 R.string.lock_screen_preview_provider_authority,
75 ),
76 ),
77 initialExtrasProvider = {
78 Bundle().apply {
79 putString(
80 KeyguardQuickAffordancePreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID,
81 selectedSlotId.value,
82 )
83 }
84 },
85 wallpaperInfoProvider = {
86 suspendCancellableCoroutine { continuation ->
87 wallpaperInfoFactory.createCurrentWallpaperInfos(
88 { homeWallpaper, lockWallpaper, _ ->
89 continuation.resume(lockWallpaper ?: homeWallpaper, null)
90 },
91 /* forceRefresh= */ true,
92 )
93 }
94 },
95 )
Alejandro Nijamkinb169b2c2022-12-22 12:25:23 -080096
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -080097 val undo: UndoViewModel =
98 UndoViewModel(
99 interactor = undoInteractor,
100 )
101
Alejandro Nijamkin5ec382d2022-12-06 16:43:36 -0800102 private val _selectedSlotId = MutableStateFlow<String?>(null)
103 val selectedSlotId: StateFlow<String?> = _selectedSlotId.asStateFlow()
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800104
105 /** View-models for each slot, keyed by slot ID. */
106 val slots: Flow<Map<String, KeyguardQuickAffordanceSlotViewModel>> =
107 combine(
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -0800108 quickAffordanceInteractor.slots,
109 quickAffordanceInteractor.affordances,
110 quickAffordanceInteractor.selections,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800111 selectedSlotId,
112 ) { slots, affordances, selections, selectedSlotIdOrNull ->
113 slots
114 .mapIndexed { index, slot ->
115 val selectedAffordanceIds =
116 selections
117 .filter { selection -> selection.slotId == slot.id }
118 .map { selection -> selection.affordanceId }
119 .toSet()
120 val selectedAffordances =
121 affordances.filter { affordance ->
122 selectedAffordanceIds.contains(affordance.id)
123 }
124 val isSelected =
125 (selectedSlotIdOrNull == null && index == 0) ||
126 selectedSlotIdOrNull == slot.id
127 slot.id to
128 KeyguardQuickAffordanceSlotViewModel(
129 name = getSlotName(slot.id),
130 isSelected = isSelected,
131 selectedQuickAffordances =
132 selectedAffordances.map { affordanceModel ->
133 KeyguardQuickAffordanceViewModel(
134 icon = getAffordanceIcon(affordanceModel.iconResourceId),
135 contentDescription = affordanceModel.name,
136 isSelected = true,
137 onClicked = null,
Alejandro Nijamkin7bda0fd2022-12-28 14:14:20 -0800138 onLongClicked = null,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800139 isEnabled = affordanceModel.isEnabled,
140 )
141 },
142 maxSelectedQuickAffordances = slot.maxSelectedQuickAffordances,
143 onClicked =
144 if (isSelected) {
145 null
146 } else {
Alejandro Nijamkin5ec382d2022-12-06 16:43:36 -0800147 { _selectedSlotId.tryEmit(slot.id) }
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800148 },
149 )
150 }
151 .toMap()
152 }
153
154 /** The list of all available quick affordances for the selected slot. */
155 val quickAffordances: Flow<List<KeyguardQuickAffordanceViewModel>> =
156 combine(
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -0800157 quickAffordanceInteractor.slots,
158 quickAffordanceInteractor.affordances,
159 quickAffordanceInteractor.selections,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800160 selectedSlotId,
161 ) { slots, affordances, selections, selectedSlotIdOrNull ->
162 val selectedSlot =
163 selectedSlotIdOrNull?.let { slots.find { slot -> slot.id == it } } ?: slots.first()
164 val selectedAffordanceIds =
165 selections
166 .filter { selection -> selection.slotId == selectedSlot.id }
167 .map { selection -> selection.affordanceId }
168 .toSet()
169 listOf(
170 none(
171 slotId = selectedSlot.id,
172 isSelected = selectedAffordanceIds.isEmpty(),
173 )
174 ) +
175 affordances.map { affordance ->
176 val isSelected = selectedAffordanceIds.contains(affordance.id)
Alejandro Nijamkina5477062022-12-22 16:36:47 -0800177 val affordanceIcon = getAffordanceIcon(affordance.iconResourceId)
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800178 KeyguardQuickAffordanceViewModel(
Alejandro Nijamkina5477062022-12-22 16:36:47 -0800179 icon = affordanceIcon,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800180 contentDescription = affordance.name,
181 isSelected = isSelected,
182 onClicked =
183 if (affordance.isEnabled) {
184 {
185 viewModelScope.launch {
186 if (isSelected) {
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -0800187 quickAffordanceInteractor.unselect(
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800188 slotId = selectedSlot.id,
189 affordanceId = affordance.id,
190 )
191 } else {
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -0800192 quickAffordanceInteractor.select(
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800193 slotId = selectedSlot.id,
194 affordanceId = affordance.id,
195 )
196 }
197 }
198 }
199 } else {
200 {
201 showEnablementDialog(
Alejandro Nijamkina5477062022-12-22 16:36:47 -0800202 icon = affordanceIcon,
203 name = affordance.name,
204 instructions = affordance.enablementInstructions,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800205 actionText = affordance.enablementActionText,
206 actionComponentName =
207 affordance.enablementActionComponentName,
208 )
209 }
210 },
Alejandro Nijamkin7bda0fd2022-12-28 14:14:20 -0800211 onLongClicked =
212 if (affordance.configureIntent != null) {
213 { activityStarter(affordance.configureIntent) }
214 } else {
215 null
216 },
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800217 isEnabled = affordance.isEnabled,
218 )
219 }
220 }
221
Alejandro Nijamkinabda67b2022-11-30 14:34:56 -0800222 @SuppressLint("UseCompatLoadingForDrawables")
223 val summary: Flow<KeyguardQuickAffordanceSummaryViewModel> =
224 slots.map { slots ->
225 val icon2 =
226 slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]
227 ?.selectedQuickAffordances
228 ?.firstOrNull()
229 ?.icon
230 val icon1 =
231 slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]
232 ?.selectedQuickAffordances
233 ?.firstOrNull()
234 ?.icon
235
Alejandro Nijamkinabda67b2022-11-30 14:34:56 -0800236 KeyguardQuickAffordanceSummaryViewModel(
237 description = toDescriptionText(context, slots),
238 icon1 = icon1
239 ?: if (icon2 == null) {
240 context.getDrawable(R.drawable.link_off)
241 } else {
242 null
243 },
244 icon2 = icon2,
Alejandro Nijamkinabda67b2022-11-30 14:34:56 -0800245 )
246 }
247
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800248 private val _dialog = MutableStateFlow<DialogViewModel?>(null)
249 /**
250 * The current dialog to show. If `null`, no dialog should be shown.
251 *
252 * When the dialog is dismissed, [onDialogDismissed] must be called.
253 */
254 val dialog: Flow<DialogViewModel?> = _dialog.asStateFlow()
255
256 /** Notifies that the dialog has been dismissed in the UI. */
257 fun onDialogDismissed() {
258 _dialog.value = null
259 }
260
261 private fun showEnablementDialog(
Alejandro Nijamkina5477062022-12-22 16:36:47 -0800262 icon: Drawable,
263 name: String,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800264 instructions: List<String>,
265 actionText: String?,
266 actionComponentName: String?,
267 ) {
268 _dialog.value =
269 DialogViewModel(
Alejandro Nijamkind42f5722023-01-17 17:58:41 -0800270 icon =
271 Icon.Loaded(
272 drawable = icon,
273 contentDescription = null,
274 ),
275 title = Text.Loaded(name),
276 message =
277 Text.Loaded(
278 buildString {
279 instructions.forEachIndexed { index, instruction ->
280 if (index > 0) {
281 append('\n')
282 }
283
284 append(instruction)
285 }
286 }
287 ),
288 buttons =
289 listOf(
290 ButtonViewModel(
291 text = actionText?.let { Text.Loaded(actionText) }
292 ?: Text.Resource(
293 R.string
294 .keyguard_affordance_enablement_dialog_dismiss_button,
295 ),
296 style = ButtonStyle.Primary,
297 onClicked = {
298 actionComponentName.toIntent()?.let { intent ->
299 applicationContext.startActivity(intent)
300 }
301 }
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800302 ),
Alejandro Nijamkind42f5722023-01-17 17:58:41 -0800303 ),
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800304 )
305 }
306
307 @SuppressLint("UseCompatLoadingForDrawables")
308 private fun none(
309 slotId: String,
310 isSelected: Boolean,
311 ): KeyguardQuickAffordanceViewModel {
312 return KeyguardQuickAffordanceViewModel.none(
313 context = applicationContext,
314 isSelected = isSelected,
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -0800315 onSelected = {
316 viewModelScope.launch { quickAffordanceInteractor.unselectAll(slotId) }
317 },
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800318 )
319 }
320
321 private fun getSlotName(slotId: String): String {
322 return applicationContext.getString(
323 when (slotId) {
324 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START ->
325 R.string.keyguard_slot_name_bottom_start
326 KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END ->
327 R.string.keyguard_slot_name_bottom_end
328 else -> error("No name for slot with ID of \"$slotId\"!")
329 }
330 )
331 }
332
333 private suspend fun getAffordanceIcon(@DrawableRes iconResourceId: Int): Drawable {
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -0800334 return quickAffordanceInteractor.getAffordanceIcon(iconResourceId)
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800335 }
336
337 private fun String?.toIntent(): Intent? {
338 if (isNullOrEmpty()) {
339 return null
340 }
341
Alejandro Nijamkin6238c2e2022-12-24 08:11:52 -0800342 val splitUp =
343 split(
344 CustomizationProviderContract.LockScreenQuickAffordances.AffordanceTable
345 .COMPONENT_NAME_SEPARATOR
346 )
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800347 check(splitUp.size == 1 || splitUp.size == 2) {
348 "Illegal component name \"$this\". Must be either just an action or a package and an" +
349 " action separated by a" +
Alejandro Nijamkin6238c2e2022-12-24 08:11:52 -0800350 " \"${CustomizationProviderContract.LockScreenQuickAffordances.AffordanceTable.COMPONENT_NAME_SEPARATOR}\"!"
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800351 }
352
353 return Intent(splitUp.last()).apply {
354 if (splitUp.size > 1) {
355 setPackage(splitUp[0])
356 }
357 }
358 }
359
Alejandro Nijamkinabda67b2022-11-30 14:34:56 -0800360 private fun toDescriptionText(
361 context: Context,
362 slots: Map<String, KeyguardQuickAffordanceSlotViewModel>,
363 ): String {
364 val bottomStartAffordanceName =
365 slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]
366 ?.selectedQuickAffordances
367 ?.firstOrNull()
368 ?.contentDescription
369 val bottomEndAffordanceName =
370 slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]
371 ?.selectedQuickAffordances
372 ?.firstOrNull()
373 ?.contentDescription
374
375 return when {
376 !bottomStartAffordanceName.isNullOrEmpty() &&
377 !bottomEndAffordanceName.isNullOrEmpty() -> {
378 context.getString(
379 R.string.keyguard_quick_affordance_two_selected_template,
380 bottomStartAffordanceName,
381 bottomEndAffordanceName,
382 )
383 }
384 !bottomStartAffordanceName.isNullOrEmpty() -> bottomStartAffordanceName
385 !bottomEndAffordanceName.isNullOrEmpty() -> bottomEndAffordanceName
386 else -> context.getString(R.string.keyguard_quick_affordance_none_selected)
387 }
388 }
389
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800390 class Factory(
391 private val context: Context,
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -0800392 private val quickAffordanceInteractor: KeyguardQuickAffordancePickerInteractor,
393 private val undoInteractor: UndoInteractor,
Alejandro Nijamkin2fe5f2d2022-12-22 15:24:22 -0800394 private val wallpaperInfoFactory: CurrentWallpaperInfoFactory,
Alejandro Nijamkin7bda0fd2022-12-28 14:14:20 -0800395 private val activityStarter: (Intent) -> Unit,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800396 ) : ViewModelProvider.Factory {
397 override fun <T : ViewModel> create(modelClass: Class<T>): T {
398 @Suppress("UNCHECKED_CAST")
399 return KeyguardQuickAffordancePickerViewModel(
400 context = context,
Alejandro Nijamkinc27b1d32022-12-21 15:27:35 -0800401 quickAffordanceInteractor = quickAffordanceInteractor,
402 undoInteractor = undoInteractor,
Alejandro Nijamkin2fe5f2d2022-12-22 15:24:22 -0800403 wallpaperInfoFactory = wallpaperInfoFactory,
Alejandro Nijamkin7bda0fd2022-12-28 14:14:20 -0800404 activityStarter = activityStarter,
Alejandro Nijamkin0f02b082022-11-24 13:43:43 -0800405 )
406 as T
407 }
408 }
409}