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