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