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