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