1 /*
<lambda>null2  * Copyright (C) 2023 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 package com.android.wallpaper.picker.preview.ui.viewmodel
17 
18 import android.graphics.Point
19 import android.graphics.Rect
20 import android.stats.style.StyleEnums
21 import androidx.lifecycle.SavedStateHandle
22 import androidx.lifecycle.ViewModel
23 import androidx.lifecycle.viewModelScope
24 import com.android.wallpaper.config.BaseFlags
25 import com.android.wallpaper.model.Screen
26 import com.android.wallpaper.model.wallpaper.DeviceDisplayType
27 import com.android.wallpaper.picker.BasePreviewActivity.EXTRA_VIEW_AS_HOME
28 import com.android.wallpaper.picker.customization.shared.model.WallpaperColorsModel
29 import com.android.wallpaper.picker.customization.shared.model.WallpaperDestination
30 import com.android.wallpaper.picker.data.WallpaperModel
31 import com.android.wallpaper.picker.data.WallpaperModel.LiveWallpaperModel
32 import com.android.wallpaper.picker.data.WallpaperModel.StaticWallpaperModel
33 import com.android.wallpaper.picker.di.modules.HomeScreenPreviewUtils
34 import com.android.wallpaper.picker.di.modules.LockScreenPreviewUtils
35 import com.android.wallpaper.picker.preview.data.repository.ImageEffectsRepository
36 import com.android.wallpaper.picker.preview.domain.interactor.PreviewActionsInteractor
37 import com.android.wallpaper.picker.preview.domain.interactor.WallpaperPreviewInteractor
38 import com.android.wallpaper.picker.preview.shared.model.FullPreviewCropModel
39 import com.android.wallpaper.picker.preview.ui.WallpaperPreviewActivity
40 import com.android.wallpaper.picker.preview.ui.binder.PreviewTooltipBinder
41 import com.android.wallpaper.util.DisplayUtils
42 import com.android.wallpaper.util.PreviewUtils
43 import com.android.wallpaper.util.WallpaperConnection.WhichPreview
44 import dagger.hilt.android.lifecycle.HiltViewModel
45 import java.util.EnumSet
46 import javax.inject.Inject
47 import kotlinx.coroutines.delay
48 import kotlinx.coroutines.flow.Flow
49 import kotlinx.coroutines.flow.MutableStateFlow
50 import kotlinx.coroutines.flow.StateFlow
51 import kotlinx.coroutines.flow.asStateFlow
52 import kotlinx.coroutines.flow.combine
53 import kotlinx.coroutines.flow.distinctUntilChanged
54 import kotlinx.coroutines.flow.filter
55 import kotlinx.coroutines.flow.filterNotNull
56 import kotlinx.coroutines.flow.flowOf
57 import kotlinx.coroutines.flow.map
58 import kotlinx.coroutines.flow.merge
59 import kotlinx.coroutines.launch
60 
61 /** Top level [ViewModel] for [WallpaperPreviewActivity] and its fragments */
62 @HiltViewModel
63 class WallpaperPreviewViewModel
64 @Inject
65 constructor(
66     private val interactor: WallpaperPreviewInteractor,
67     actionsInteractor: PreviewActionsInteractor,
68     staticWallpaperPreviewViewModelFactory: StaticWallpaperPreviewViewModel.Factory,
69     val previewActionsViewModel: PreviewActionsViewModel,
70     private val displayUtils: DisplayUtils,
71     @HomeScreenPreviewUtils private val homePreviewUtils: PreviewUtils,
72     @LockScreenPreviewUtils private val lockPreviewUtils: PreviewUtils,
73     savedStateHandle: SavedStateHandle,
74 ) : ViewModel() {
75 
76     // Don't update smaller display since we always use portrait, always use wallpaper display on
77     // single display device.
78     val smallerDisplaySize = displayUtils.getRealSize(displayUtils.getSmallerDisplay())
79     private val _wallpaperDisplaySize =
80         MutableStateFlow(displayUtils.getRealSize(displayUtils.getWallpaperDisplay()))
81     val wallpaperDisplaySize = _wallpaperDisplaySize.asStateFlow()
82 
83     val staticWallpaperPreviewViewModel =
84         staticWallpaperPreviewViewModelFactory.create(viewModelScope)
85 
86     var isNewTask = false
87 
88     val isViewAsHome = savedStateHandle.get<Boolean>(EXTRA_VIEW_AS_HOME) ?: false
89 
90     fun getWallpaperPreviewSource(): Screen =
91         if (isViewAsHome) Screen.HOME_SCREEN else Screen.LOCK_SCREEN
92 
93     val wallpaper: StateFlow<WallpaperModel?> = interactor.wallpaperModel
94 
95     // Used to display loading indication on the preview.
96     val imageEffectsModel = actionsInteractor.imageEffectsModel
97 
98     // This flag prevents launching the creative edit activity again when orientation change.
99     // On orientation change, the fragment's onCreateView will be called again.
100     var isCurrentlyEditingCreativeWallpaper = false
101 
102     private val _currentPreviewScreen = MutableStateFlow(PreviewScreen.SMALL_PREVIEW)
103     val currentPreviewScreen = _currentPreviewScreen.asStateFlow()
104 
105     val shouldEnableClickOnPager: Flow<Boolean> =
106         _currentPreviewScreen.map { it != PreviewScreen.FULL_PREVIEW }
107 
108     val smallPreviewTabs = Screen.entries.toList()
109 
110     private val _smallPreviewSelectedTab = MutableStateFlow(getWallpaperPreviewSource())
111     val smallPreviewSelectedTab = _smallPreviewSelectedTab.asStateFlow()
112 
113     val smallPreviewSelectedTabIndex = smallPreviewSelectedTab.map { smallPreviewTabs.indexOf(it) }
114 
115     /**
116      * Returns true if back pressed is handled due to conditions like users at a secondary screen.
117      */
118     fun handleBackPressed(): Boolean {
119         if (_currentPreviewScreen.value == PreviewScreen.APPLY_WALLPAPER) {
120             _currentPreviewScreen.value = PreviewScreen.SMALL_PREVIEW
121             return true
122         } else if (_currentPreviewScreen.value == PreviewScreen.FULL_PREVIEW) {
123             _currentPreviewScreen.value = PreviewScreen.SMALL_PREVIEW
124             // TODO(b/367374790): Returns true when shared element transition is removed
125             return false
126         }
127         return false
128     }
129 
130     fun getSmallPreviewTabIndex(): Int {
131         return smallPreviewTabs.indexOf(smallPreviewSelectedTab.value)
132     }
133 
134     fun setSmallPreviewSelectedTab(screen: Screen) {
135         _smallPreviewSelectedTab.value = screen
136     }
137 
138     fun setSmallPreviewSelectedTabIndex(index: Int) {
139         _smallPreviewSelectedTab.value = smallPreviewTabs[index]
140     }
141 
142     fun updateDisplayConfiguration() {
143         _wallpaperDisplaySize.value = displayUtils.getRealSize(displayUtils.getWallpaperDisplay())
144     }
145 
146     private val isWallpaperCroppable: Flow<Boolean> =
147         wallpaper.map { wallpaper ->
148             wallpaper is StaticWallpaperModel && !wallpaper.isDownloadableWallpaper()
149         }
150 
151     val smallTooltipViewModel =
152         object : PreviewTooltipBinder.TooltipViewModel {
153             override val shouldShowTooltip: Flow<Boolean> =
154                 combine(isWallpaperCroppable, interactor.hasSmallPreviewTooltipBeenShown) {
155                         isCroppable,
156                         hasTooltipBeenShown ->
157                         // Only show tooltip if it has not been shown before.
158                         isCroppable && !hasTooltipBeenShown
159                     }
160                     .distinctUntilChanged()
161 
162             override fun dismissTooltip() = interactor.hideSmallPreviewTooltip()
163         }
164 
165     val fullTooltipViewModel =
166         object : PreviewTooltipBinder.TooltipViewModel {
167             override val shouldShowTooltip: Flow<Boolean> =
168                 combine(isWallpaperCroppable, interactor.hasFullPreviewTooltipBeenShown) {
169                         isCroppable,
170                         hasTooltipBeenShown ->
171                         // Only show tooltip if it has not been shown before.
172                         isCroppable && !hasTooltipBeenShown
173                     }
174                     .distinctUntilChanged()
175 
176             override fun dismissTooltip() = interactor.hideFullPreviewTooltip()
177         }
178 
179     private val _whichPreview = MutableStateFlow<WhichPreview?>(null)
180     private val whichPreview: Flow<WhichPreview> = _whichPreview.asStateFlow().filterNotNull()
181 
182     fun setWhichPreview(whichPreview: WhichPreview) {
183         _whichPreview.value = whichPreview
184     }
185 
186     fun setCropHints(cropHints: Map<Point, Rect>) {
187         wallpaper.value?.let { model ->
188             if (model is StaticWallpaperModel && !model.isDownloadableWallpaper()) {
189                 staticWallpaperPreviewViewModel.updateCropHintsInfo(
190                     cropHints.mapValues {
191                         FullPreviewCropModel(cropHint = it.value, cropSizeModel = null)
192                     }
193                 )
194             }
195         }
196     }
197 
198     private val _isWallpaperColorPreviewEnabled = MutableStateFlow(false)
199     val isWallpaperColorPreviewEnabled = _isWallpaperColorPreviewEnabled.asStateFlow()
200 
201     fun setIsWallpaperColorPreviewEnabled(isWallpaperColorPreviewEnabled: Boolean) {
202         _isWallpaperColorPreviewEnabled.value = isWallpaperColorPreviewEnabled
203     }
204 
205     private val _wallpaperConnectionColors: MutableStateFlow<WallpaperColorsModel> =
206         MutableStateFlow(WallpaperColorsModel.Loading as WallpaperColorsModel).apply {
207             viewModelScope.launch {
208                 delay(1000)
209                 if (value == WallpaperColorsModel.Loading) {
210                     emit(WallpaperColorsModel.Loaded(null))
211                 }
212             }
213         }
214     private val liveWallpaperColors: Flow<WallpaperColorsModel> =
215         wallpaper
216             .filter { it is LiveWallpaperModel }
217             .combine(_wallpaperConnectionColors) { _, wallpaperConnectionColors ->
218                 wallpaperConnectionColors
219             }
220     val wallpaperColorsModel: Flow<WallpaperColorsModel> =
221         merge(liveWallpaperColors, staticWallpaperPreviewViewModel.wallpaperColors).combine(
222             isWallpaperColorPreviewEnabled
223         ) { colors, isEnabled ->
224             if (isEnabled) colors else WallpaperColorsModel.Loaded(null)
225         }
226 
227     // This is only used for the full screen preview.
228     private val _fullPreviewConfigViewModel: MutableStateFlow<FullPreviewConfigViewModel?> =
229         MutableStateFlow(null)
230     val fullPreviewConfigViewModel = _fullPreviewConfigViewModel.asStateFlow()
231 
232     // This is only used for the small screen wallpaper preview.
233     val smallWallpaper: Flow<Pair<WallpaperModel, WhichPreview>> =
234         combine(wallpaper.filterNotNull(), whichPreview) { wallpaper, whichPreview ->
235             Pair(wallpaper, whichPreview)
236         }
237 
238     // This is only used for the full screen wallpaper preview.
239     val fullWallpaper: Flow<FullWallpaperPreviewViewModel> =
240         combine(
241             wallpaper.filterNotNull(),
242             fullPreviewConfigViewModel.filterNotNull(),
243             whichPreview,
244             wallpaperDisplaySize,
245         ) { wallpaper, config, whichPreview, wallpaperDisplaySize ->
246             val displaySize =
247                 when (config.deviceDisplayType) {
248                     DeviceDisplayType.SINGLE -> wallpaperDisplaySize
249                     DeviceDisplayType.FOLDED -> smallerDisplaySize
250                     DeviceDisplayType.UNFOLDED -> wallpaperDisplaySize
251                 }
252             FullWallpaperPreviewViewModel(
253                 wallpaper = wallpaper,
254                 config = FullPreviewConfigViewModel(config.screen, config.deviceDisplayType),
255                 displaySize = displaySize,
256                 allowUserCropping =
257                     wallpaper is StaticWallpaperModel && !wallpaper.isDownloadableWallpaper(),
258                 whichPreview = whichPreview,
259             )
260         }
261 
262     // This is only used for the full screen workspace preview.
263     val fullWorkspacePreviewConfigViewModel: Flow<WorkspacePreviewConfigViewModel> =
264         fullPreviewConfigViewModel.filterNotNull().map {
265             getWorkspacePreviewConfig(it.screen, it.deviceDisplayType)
266         }
267 
268     val onCropButtonClick: Flow<(() -> Unit)?> =
269         combine(wallpaper, fullPreviewConfigViewModel.filterNotNull(), fullWallpaper) {
270             wallpaper,
271             _,
272             fullWallpaper ->
273             if (wallpaper is StaticWallpaperModel && !wallpaper.isDownloadableWallpaper()) {
274                 {
275                     staticWallpaperPreviewViewModel.run {
276                         updateCropHintsInfo(
277                             fullPreviewCropModels.filterKeys { it == fullWallpaper.displaySize }
278                         )
279                     }
280                 }
281             } else {
282                 null
283             }
284         }
285 
286     // Set wallpaper button and set wallpaper dialog
287     val isSetWallpaperButtonVisible: Flow<Boolean> =
288         wallpaper.map { it != null && !it.isDownloadableWallpaper() }
289 
290     val isSetWallpaperButtonEnabled: Flow<Boolean> =
291         combine(
292             isSetWallpaperButtonVisible,
293             wallpaper,
294             staticWallpaperPreviewViewModel.fullResWallpaperViewModel,
295             actionsInteractor.imageEffectsModel,
296         ) { isSetWallpaperButtonVisible, wallpaper, fullResWallpaperViewModel, imageEffectsModel ->
297             isSetWallpaperButtonVisible &&
298                 !(wallpaper is StaticWallpaperModel && fullResWallpaperViewModel == null) &&
299                 imageEffectsModel.status !=
300                     ImageEffectsRepository.EffectStatus.EFFECT_APPLY_IN_PROGRESS
301         }
302 
303     val onSetWallpaperButtonClicked: Flow<(() -> Unit)?> =
304         combine(isSetWallpaperButtonVisible, isSetWallpaperButtonEnabled) {
305             isSetWallpaperButtonVisible,
306             isSetWallpaperButtonEnabled ->
307             if (isSetWallpaperButtonVisible && isSetWallpaperButtonEnabled) {
308                 { _showSetWallpaperDialog.value = true }
309             } else null
310         }
311 
312     val onNextButtonClicked: Flow<(() -> Unit)?> =
313         isSetWallpaperButtonEnabled.map {
314             if (it) {
315                 { _currentPreviewScreen.value = PreviewScreen.APPLY_WALLPAPER }
316             } else null
317         }
318 
319     val onCancelButtonClicked: Flow<() -> Unit> = flowOf {
320         _currentPreviewScreen.value = PreviewScreen.SMALL_PREVIEW
321     }
322 
323     private val _showSetWallpaperDialog = MutableStateFlow(false)
324     val showSetWallpaperDialog = _showSetWallpaperDialog.asStateFlow()
325 
326     private val _setWallpaperDialogSelectedScreens: MutableStateFlow<Set<Screen>> =
327         MutableStateFlow(EnumSet.allOf(Screen::class.java))
328     val setWallpaperDialogSelectedScreens: StateFlow<Set<Screen>> =
329         _setWallpaperDialogSelectedScreens.asStateFlow()
330 
331     val isApplyButtonEnabled: Flow<Boolean> =
332         setWallpaperDialogSelectedScreens.map { it.isNotEmpty() }
333 
334     val isHomeCheckBoxChecked: Flow<Boolean> =
335         setWallpaperDialogSelectedScreens.map { it.contains(Screen.HOME_SCREEN) }
336 
337     val isLockCheckBoxChecked: Flow<Boolean> =
338         setWallpaperDialogSelectedScreens.map { it.contains(Screen.LOCK_SCREEN) }
339 
340     val onHomeCheckBoxChecked: Flow<() -> Unit> = flowOf {
341         onSetWallpaperDialogScreenSelected(Screen.HOME_SCREEN)
342     }
343 
344     val onLockCheckBoxChecked: Flow<() -> Unit> = flowOf {
345         onSetWallpaperDialogScreenSelected(Screen.LOCK_SCREEN)
346     }
347 
348     fun onSetWallpaperDialogScreenSelected(screen: Screen) {
349         val previousSelection = _setWallpaperDialogSelectedScreens.value
350         _setWallpaperDialogSelectedScreens.value =
351             if (
352                 previousSelection.contains(screen) &&
353                     (previousSelection.size > 1 || BaseFlags.get().isNewPickerUi())
354             ) {
355                 previousSelection.minus(screen)
356             } else {
357                 previousSelection.plus(screen)
358             }
359     }
360 
361     private val _isSetWallpaperProgressBarVisible = MutableStateFlow(false)
362     val isSetWallpaperProgressBarVisible: Flow<Boolean> =
363         _isSetWallpaperProgressBarVisible.asStateFlow()
364 
365     val setWallpaperDialogOnConfirmButtonClicked: Flow<suspend () -> Unit> =
366         combine(
367             wallpaper.filterNotNull(),
368             staticWallpaperPreviewViewModel.fullResWallpaperViewModel,
369             setWallpaperDialogSelectedScreens,
370         ) { wallpaper, fullResWallpaperViewModel, selectedScreens ->
371             {
372                 _isSetWallpaperProgressBarVisible.value = true
373                 val destination = selectedScreens.getDestination()
374                 _showSetWallpaperDialog.value = false
375                 when (wallpaper) {
376                     is StaticWallpaperModel ->
377                         fullResWallpaperViewModel?.let {
378                             interactor.setStaticWallpaper(
379                                 setWallpaperEntryPoint =
380                                     StyleEnums.SET_WALLPAPER_ENTRY_POINT_WALLPAPER_PREVIEW,
381                                 destination = destination,
382                                 wallpaperModel = wallpaper,
383                                 bitmap = it.rawWallpaperBitmap,
384                                 wallpaperSize = it.rawWallpaperSize,
385                                 asset = it.asset,
386                                 fullPreviewCropModels =
387                                     if (it.fullPreviewCropModels.isNullOrEmpty()) {
388                                         staticWallpaperPreviewViewModel.fullPreviewCropModels
389                                     } else {
390                                         it.fullPreviewCropModels
391                                     },
392                             )
393                         }
394                     is LiveWallpaperModel -> {
395                         interactor.setLiveWallpaper(
396                             setWallpaperEntryPoint =
397                                 StyleEnums.SET_WALLPAPER_ENTRY_POINT_WALLPAPER_PREVIEW,
398                             destination = destination,
399                             wallpaperModel = wallpaper,
400                         )
401                     }
402                 }
403             }
404         }
405 
406     private fun Set<Screen>.getDestination(): WallpaperDestination {
407         return if (containsAll(Screen.entries)) {
408             WallpaperDestination.BOTH
409         } else if (contains(Screen.HOME_SCREEN)) {
410             WallpaperDestination.HOME
411         } else if (contains(Screen.LOCK_SCREEN)) {
412             WallpaperDestination.LOCK
413         } else {
414             throw IllegalArgumentException("Unknown screens selected: $this")
415         }
416     }
417 
418     fun dismissSetWallpaperDialog() {
419         _showSetWallpaperDialog.value = false
420     }
421 
422     fun setWallpaperConnectionColors(wallpaperColors: WallpaperColorsModel) {
423         _wallpaperConnectionColors.value = wallpaperColors
424     }
425 
426     fun getWorkspacePreviewConfig(
427         screen: Screen,
428         deviceDisplayType: DeviceDisplayType,
429     ): WorkspacePreviewConfigViewModel {
430         val previewUtils =
431             when (screen) {
432                 Screen.HOME_SCREEN -> {
433                     homePreviewUtils
434                 }
435                 Screen.LOCK_SCREEN -> {
436                     lockPreviewUtils
437                 }
438             }
439         // Do not directly store display Id in the view model because display Id can change on fold
440         // and unfold whereas view models persist. Store FoldableDisplay instead and convert in the
441         // binder.
442         return WorkspacePreviewConfigViewModel(
443             previewUtils = previewUtils,
444             deviceDisplayType = deviceDisplayType,
445         )
446     }
447 
448     fun getDisplayId(deviceDisplayType: DeviceDisplayType): Int {
449         return when (deviceDisplayType) {
450             DeviceDisplayType.SINGLE -> {
451                 displayUtils.getWallpaperDisplay().displayId
452             }
453             DeviceDisplayType.FOLDED -> {
454                 displayUtils.getSmallerDisplay().displayId
455             }
456             DeviceDisplayType.UNFOLDED -> {
457                 displayUtils.getWallpaperDisplay().displayId
458             }
459         }
460     }
461 
462     val isSmallPreviewClickable =
463         actionsInteractor.imageEffectsModel.map {
464             it.status != ImageEffectsRepository.EffectStatus.EFFECT_APPLY_IN_PROGRESS
465         }
466 
467     fun onSmallPreviewClicked(
468         screen: Screen,
469         deviceDisplayType: DeviceDisplayType,
470         navigate: () -> Unit,
471     ): Flow<(() -> Unit)?> =
472         combine(isSmallPreviewClickable, smallPreviewSelectedTab) { isClickable, selectedTab ->
473             if (isClickable) {
474                 if (selectedTab == screen) {
475                     // If the selected preview matches the selected tab, navigate to full preview.
476                     {
477                         smallTooltipViewModel.dismissTooltip()
478                         _fullPreviewConfigViewModel.value =
479                             FullPreviewConfigViewModel(screen, deviceDisplayType)
480                         navigate()
481                     }
482                 } else {
483                     // If the selected preview doesn't match the selected tab, switch tab to match.
484                     { setSmallPreviewSelectedTab(screen) }
485                 }
486             } else {
487                 null
488             }
489         }
490 
491     fun setDefaultFullPreviewConfigViewModel(deviceDisplayType: DeviceDisplayType) {
492         _fullPreviewConfigViewModel.value =
493             FullPreviewConfigViewModel(Screen.HOME_SCREEN, deviceDisplayType)
494     }
495 
496     fun resetFullPreviewConfigViewModel() {
497         _fullPreviewConfigViewModel.value = null
498     }
499 
500     companion object {
501         private fun WallpaperModel.isDownloadableWallpaper(): Boolean {
502             return this is StaticWallpaperModel && downloadableWallpaperData != null
503         }
504 
505         /** The current preview screen or the screen being transition to. */
506         enum class PreviewScreen {
507             SMALL_PREVIEW,
508             FULL_PREVIEW,
509             APPLY_WALLPAPER,
510         }
511     }
512 }
513