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