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