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.app.WallpaperColors 19 import android.content.Context 20 import android.graphics.Bitmap 21 import android.graphics.Point 22 import android.graphics.Rect 23 import androidx.annotation.VisibleForTesting 24 import com.android.wallpaper.asset.Asset 25 import com.android.wallpaper.module.WallpaperPreferences 26 import com.android.wallpaper.picker.customization.shared.model.WallpaperColorsModel 27 import com.android.wallpaper.picker.data.WallpaperModel.StaticWallpaperModel 28 import com.android.wallpaper.picker.di.modules.BackgroundDispatcher 29 import com.android.wallpaper.picker.preview.domain.interactor.WallpaperPreviewInteractor 30 import com.android.wallpaper.picker.preview.shared.model.FullPreviewCropModel 31 import com.android.wallpaper.picker.preview.ui.WallpaperPreviewActivity 32 import dagger.hilt.android.qualifiers.ApplicationContext 33 import dagger.hilt.android.scopes.ViewModelScoped 34 import javax.inject.Inject 35 import kotlinx.coroutines.CancellableContinuation 36 import kotlinx.coroutines.CoroutineDispatcher 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.flow.Flow 39 import kotlinx.coroutines.flow.MutableStateFlow 40 import kotlinx.coroutines.flow.SharingStarted 41 import kotlinx.coroutines.flow.combine 42 import kotlinx.coroutines.flow.distinctUntilChanged 43 import kotlinx.coroutines.flow.filterNotNull 44 import kotlinx.coroutines.flow.flowOn 45 import kotlinx.coroutines.flow.map 46 import kotlinx.coroutines.flow.shareIn 47 import kotlinx.coroutines.suspendCancellableCoroutine 48 49 /** View model for static wallpaper preview used in [WallpaperPreviewActivity] and its fragments */ 50 @ViewModelScoped 51 class StaticWallpaperPreviewViewModel 52 @Inject 53 constructor( 54 interactor: WallpaperPreviewInteractor, 55 @ApplicationContext private val context: Context, 56 private val wallpaperPreferences: WallpaperPreferences, 57 @BackgroundDispatcher private val bgDispatcher: CoroutineDispatcher, 58 viewModelScope: CoroutineScope, 59 ) { 60 /** 61 * The state of static wallpaper crop in full preview, before user confirmation. 62 * 63 * The initial value should be the default crop on small preview, which could be the cropHints 64 * for current wallpaper or default crop area for a new wallpaper. 65 */ 66 val fullPreviewCropModels: MutableMap<Point, FullPreviewCropModel> = mutableMapOf() 67 68 /** 69 * The default crops for the current wallpaper, which is center aligned on the preview. 70 * 71 * Always update default through [updateDefaultPreviewCropModel] to make sure multiple updates 72 * of the same preview only counts the first time it appears. 73 */ 74 private val defaultPreviewCropModels: MutableMap<Point, FullPreviewCropModel> = mutableMapOf() 75 76 /** 77 * The info picker needs to post process crops for setting static wallpaper. 78 * 79 * It will be filled with current cropHints when previewing current wallpaper, and null when 80 * previewing a new wallpaper, and gets updated through [updateCropHintsInfo] when user picks a 81 * new crop. 82 */ 83 @get:VisibleForTesting 84 val cropHintsInfo: MutableStateFlow<Map<Point, FullPreviewCropModel>?> = MutableStateFlow(null) 85 86 private val cropHints: Flow<Map<Point, Rect>?> = 87 cropHintsInfo.map { cropHintsInfoMap -> 88 cropHintsInfoMap?.map { entry -> entry.key to entry.value.cropHint }?.toMap() 89 } 90 91 val staticWallpaperModel: Flow<StaticWallpaperModel> = 92 interactor.wallpaperModel.map { it as? StaticWallpaperModel }.filterNotNull() 93 94 /** Null indicates the wallpaper has no low res image. */ 95 val lowResBitmap: Flow<Bitmap?> = 96 staticWallpaperModel 97 .map { it.staticWallpaperData.asset.getLowResBitmap(context) } 98 .flowOn(bgDispatcher) 99 // Asset detail includes the dimensions, bitmap and the asset. 100 private val assetDetail: Flow<Triple<Point, Bitmap?, Asset>?> = 101 interactor.wallpaperModel 102 .map { (it as? StaticWallpaperModel)?.staticWallpaperData?.asset } 103 .map { asset -> 104 asset?.decodeRawDimensions()?.let { Triple(it, asset.decodeBitmap(it), asset) } 105 } 106 .flowOn(bgDispatcher) 107 // We only want to decode bitmap every time when wallpaper model is updated, instead of 108 // a new subscriber listens to this flow. So we need to use shareIn. 109 .shareIn(viewModelScope, SharingStarted.Lazily, 1) 110 111 val fullResWallpaperViewModel: Flow<FullResWallpaperViewModel?> = 112 combine(assetDetail, cropHintsInfo) { assetDetail, cropHintsInfo -> 113 if (assetDetail == null) { 114 null 115 } else { 116 val (dimensions, bitmap, asset) = assetDetail 117 bitmap?.let { 118 FullResWallpaperViewModel( 119 bitmap, 120 dimensions, 121 asset, 122 cropHintsInfo, 123 ) 124 } 125 } 126 } 127 .flowOn(bgDispatcher) 128 val subsamplingScaleImageViewModel: Flow<FullResWallpaperViewModel> = 129 fullResWallpaperViewModel.filterNotNull() 130 // TODO (b/315856338): cache wallpaper colors in preferences 131 private val storedWallpaperColors: Flow<WallpaperColors?> = 132 staticWallpaperModel 133 .map { wallpaperPreferences.getWallpaperColors(it.commonWallpaperData.id.uniqueId) } 134 .distinctUntilChanged() 135 val wallpaperColors: Flow<WallpaperColorsModel> = 136 combine(storedWallpaperColors, subsamplingScaleImageViewModel, cropHints) { 137 storedColors, 138 wallpaperViewModel, 139 cropHints -> 140 WallpaperColorsModel.Loaded( 141 if (cropHints == null) { 142 storedColors 143 ?: interactor.getWallpaperColors( 144 wallpaperViewModel.rawWallpaperBitmap, 145 null 146 ) 147 } else { 148 interactor.getWallpaperColors(wallpaperViewModel.rawWallpaperBitmap, cropHints) 149 } 150 ) 151 } 152 153 /** 154 * Updates new cropHints per displaySize that's been confirmed by the user or from a new default 155 * crop. 156 * 157 * That's when picker gets current cropHints from [WallpaperManager] or when user crops and 158 * confirms a crop, or when a small preview for a new display size has been discovered the first 159 * time. 160 */ 161 fun updateCropHintsInfo( 162 cropHintsInfo: Map<Point, FullPreviewCropModel>, 163 updateDefaultCrop: Boolean = false 164 ) { 165 val newInfo = 166 this.cropHintsInfo.value?.let { currentCropHintsInfo -> 167 currentCropHintsInfo.plus( 168 if (updateDefaultCrop) 169 cropHintsInfo.filterKeys { !currentCropHintsInfo.keys.contains(it) } 170 else cropHintsInfo 171 ) 172 } ?: cropHintsInfo 173 this.cropHintsInfo.value = newInfo 174 fullPreviewCropModels.putAll(newInfo) 175 } 176 177 /** Updates default cropHint for [displaySize] if it's not already exist. */ 178 fun updateDefaultPreviewCropModel(displaySize: Point, cropModel: FullPreviewCropModel) { 179 defaultPreviewCropModels.let { cropModels -> 180 if (!cropModels.contains(displaySize)) { 181 cropModels[displaySize] = cropModel 182 updateCropHintsInfo( 183 cropModels.filterKeys { it == displaySize }, 184 updateDefaultCrop = true, 185 ) 186 } 187 } 188 } 189 190 // TODO b/296288298 Create a util class for Bitmap and Asset 191 private suspend fun Asset.decodeRawDimensions(): Point? = 192 suspendCancellableCoroutine { k: CancellableContinuation<Point?> -> 193 val callback = Asset.DimensionsReceiver { k.resumeWith(Result.success(it)) } 194 decodeRawDimensions(null, callback) 195 } 196 197 // TODO b/296288298 Create a util class functions for Bitmap and Asset 198 private suspend fun Asset.decodeBitmap(dimensions: Point): Bitmap? = 199 suspendCancellableCoroutine { k: CancellableContinuation<Bitmap?> -> 200 val callback = Asset.BitmapReceiver { k.resumeWith(Result.success(it)) } 201 decodeBitmap(dimensions.x, dimensions.y, /* hardwareBitmapAllowed= */ false, callback) 202 } 203 204 class Factory 205 @Inject 206 constructor( 207 private val interactor: WallpaperPreviewInteractor, 208 @ApplicationContext private val context: Context, 209 private val wallpaperPreferences: WallpaperPreferences, 210 @BackgroundDispatcher private val bgDispatcher: CoroutineDispatcher, 211 ) { 212 fun create(viewModelScope: CoroutineScope): StaticWallpaperPreviewViewModel { 213 return StaticWallpaperPreviewViewModel( 214 interactor = interactor, 215 context = context, 216 wallpaperPreferences = wallpaperPreferences, 217 bgDispatcher = bgDispatcher, 218 viewModelScope = viewModelScope, 219 ) 220 } 221 } 222 } 223