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