1 /* <lambda>null2 * Copyright (C) 2024 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 17 package com.android.wallpaper.picker.category.ui.viewmodel 18 19 import android.content.Context 20 import android.content.Intent 21 import android.content.pm.ResolveInfo 22 import android.service.wallpaper.WallpaperService 23 import androidx.lifecycle.ViewModel 24 import androidx.lifecycle.viewModelScope 25 import com.android.wallpaper.R 26 import com.android.wallpaper.module.PackageStatusNotifier 27 import com.android.wallpaper.module.PackageStatusNotifier.PackageStatus 28 import com.android.wallpaper.picker.category.domain.interactor.CategoriesLoadingStatusInteractor 29 import com.android.wallpaper.picker.category.domain.interactor.CategoryInteractor 30 import com.android.wallpaper.picker.category.domain.interactor.CreativeCategoryInteractor 31 import com.android.wallpaper.picker.category.domain.interactor.MyPhotosInteractor 32 import com.android.wallpaper.picker.category.domain.interactor.ThirdPartyCategoryInteractor 33 import com.android.wallpaper.picker.category.ui.view.SectionCardinality 34 import com.android.wallpaper.picker.data.WallpaperModel 35 import com.android.wallpaper.picker.data.category.CategoryModel 36 import com.android.wallpaper.picker.network.domain.NetworkStatusInteractor 37 import dagger.hilt.android.lifecycle.HiltViewModel 38 import dagger.hilt.android.qualifiers.ApplicationContext 39 import javax.inject.Inject 40 import kotlinx.coroutines.flow.Flow 41 import kotlinx.coroutines.flow.MutableSharedFlow 42 import kotlinx.coroutines.flow.asSharedFlow 43 import kotlinx.coroutines.flow.combine 44 import kotlinx.coroutines.flow.distinctUntilChanged 45 import kotlinx.coroutines.flow.map 46 import kotlinx.coroutines.launch 47 48 /** Top level [ViewModel] for the categories screen */ 49 @HiltViewModel 50 class CategoriesViewModel 51 @Inject 52 constructor( 53 private val singleCategoryInteractor: CategoryInteractor, 54 private val creativeCategoryInteractor: CreativeCategoryInteractor, 55 private val myPhotosInteractor: MyPhotosInteractor, 56 private val thirdPartyCategoryInteractor: ThirdPartyCategoryInteractor, 57 private val loadindStatusInteractor: CategoriesLoadingStatusInteractor, 58 private val networkStatusInteractor: NetworkStatusInteractor, 59 private val packageStatusNotifier: PackageStatusNotifier, 60 @ApplicationContext private val context: Context, 61 ) : ViewModel() { 62 63 private val _navigationEvents = MutableSharedFlow<NavigationEvent>() 64 val navigationEvents = _navigationEvents.asSharedFlow() 65 66 init { 67 registerLiveWallpaperReceiver() 68 registerThirdPartyWallpaperCategories() 69 } 70 71 // TODO: b/379138560: Add tests for this method and method below 72 private fun registerLiveWallpaperReceiver() { 73 packageStatusNotifier.addListener( 74 { packageName, status -> 75 if (packageName != null) { 76 updateLiveWallpapersCategories(packageName, status) 77 } 78 }, 79 WallpaperService.SERVICE_INTERFACE, 80 ) 81 } 82 83 private fun registerThirdPartyWallpaperCategories() { 84 packageStatusNotifier.addListener( 85 { packageName, status -> 86 if (packageName != null) { 87 updateThirdPartyAppCategories(packageName, status) 88 } 89 }, 90 Intent.ACTION_SET_WALLPAPER, 91 ) 92 } 93 94 private fun updateLiveWallpapersCategories(packageName: String, @PackageStatus status: Int) { 95 refreshThirdPartyLiveWallpaperCategories() 96 } 97 98 private fun updateThirdPartyAppCategories(packageName: String, @PackageStatus status: Int) { 99 refreshThirdPartyCategories() 100 } 101 102 private fun refreshThirdPartyLiveWallpaperCategories() { 103 singleCategoryInteractor.refreshThirdPartyLiveWallpaperCategories() 104 } 105 106 private fun refreshThirdPartyCategories() { 107 thirdPartyCategoryInteractor.refreshThirdPartyAppCategories() 108 } 109 110 private fun navigateToWallpaperCollection(collectionId: String, categoryType: CategoryType) { 111 viewModelScope.launch { 112 _navigationEvents.emit( 113 NavigationEvent.NavigateToWallpaperCollection(collectionId, categoryType) 114 ) 115 } 116 } 117 118 private fun navigateToPreviewScreen( 119 wallpaperModel: WallpaperModel, 120 categoryType: CategoryType, 121 ) { 122 viewModelScope.launch { 123 _navigationEvents.emit( 124 NavigationEvent.NavigateToPreviewScreen(wallpaperModel, categoryType) 125 ) 126 } 127 } 128 129 private fun navigateToPhotosPicker(wallpaperModel: WallpaperModel?) { 130 viewModelScope.launch { 131 _navigationEvents.emit(NavigationEvent.NavigateToPhotosPicker(wallpaperModel)) 132 } 133 } 134 135 private fun navigateToThirdPartyApp(resolveInfo: ResolveInfo) { 136 viewModelScope.launch { 137 _navigationEvents.emit(NavigationEvent.NavigateToThirdParty(resolveInfo)) 138 } 139 } 140 141 val categoryModelListDifferentiator = 142 { oldList: List<CategoryModel>, newList: List<CategoryModel> -> 143 if (oldList.size != newList.size) { 144 false 145 } else { 146 !oldList.containsAll(newList) 147 } 148 } 149 150 /** 151 * This section is only for third party category apps, and not third party live wallpaper 152 * category apps which are handled as part of default category sections. 153 */ 154 private val thirdPartyCategorySections: Flow<List<SectionViewModel>> = 155 thirdPartyCategoryInteractor.categories 156 .distinctUntilChanged { old, new -> categoryModelListDifferentiator(old, new) } 157 .map { categories -> 158 return@map categories.map { category -> 159 SectionViewModel( 160 tileViewModels = 161 listOf( 162 TileViewModel( 163 /* defaultDrawable = */ category.thirdPartyCategoryData 164 ?.defaultDrawable, 165 /* thumbnailAsset = */ null, 166 /* text = */ category.commonCategoryData.title, 167 ) { 168 category.thirdPartyCategoryData?.resolveInfo?.let { 169 navigateToThirdPartyApp(it) 170 } 171 } 172 ), 173 columnCount = 1, 174 sectionTitle = null, 175 ) 176 } 177 } 178 179 private val defaultCategorySections: Flow<List<SectionViewModel>> = 180 singleCategoryInteractor.categories 181 .distinctUntilChanged { old, new -> categoryModelListDifferentiator(old, new) } 182 .map { categories -> 183 return@map categories.map { category -> 184 SectionViewModel( 185 tileViewModels = 186 listOf( 187 TileViewModel( 188 defaultDrawable = null, 189 thumbnailAsset = category.collectionCategoryData?.thumbAsset, 190 text = category.commonCategoryData.title, 191 ) { 192 if ( 193 category.collectionCategoryData 194 ?.isSingleWallpaperCategory == true 195 ) { 196 navigateToPreviewScreen( 197 category.collectionCategoryData.wallpaperModels[0], 198 CategoryType.DefaultCategories, 199 ) 200 } else { 201 navigateToWallpaperCollection( 202 category.commonCategoryData.collectionId, 203 CategoryType.DefaultCategories, 204 ) 205 } 206 } 207 ), 208 columnCount = 1, 209 sectionTitle = null, 210 ) 211 } 212 } 213 214 private val individualSectionViewModels: Flow<List<SectionViewModel>> = 215 combine(defaultCategorySections, thirdPartyCategorySections) { list1, list2 -> 216 list1 + list2 217 } 218 219 private val creativeSectionViewModel: Flow<SectionViewModel> = 220 creativeCategoryInteractor.categories 221 .distinctUntilChanged { old, new -> categoryModelListDifferentiator(old, new) } 222 .map { categories -> 223 val tiles = 224 categories.map { category -> 225 TileViewModel( 226 defaultDrawable = null, 227 thumbnailAsset = category.collectionCategoryData?.thumbAsset, 228 text = category.commonCategoryData.title, 229 maxCategoriesInRow = SectionCardinality.Triple, 230 ) { 231 if ( 232 category.collectionCategoryData?.isSingleWallpaperCategory == true 233 ) { 234 navigateToPreviewScreen( 235 category.collectionCategoryData.wallpaperModels[0], 236 CategoryType.CreativeCategories, 237 ) 238 } else { 239 navigateToWallpaperCollection( 240 category.commonCategoryData.collectionId, 241 CategoryType.CreativeCategories, 242 ) 243 } 244 } 245 } 246 return@map SectionViewModel( 247 tileViewModels = tiles, 248 columnCount = 3, 249 sectionTitle = context.getString(R.string.creative_wallpaper_title), 250 ) 251 } 252 253 private val myPhotosSectionViewModel: Flow<SectionViewModel> = 254 myPhotosInteractor.category.distinctUntilChanged().map { category -> 255 SectionViewModel( 256 tileViewModels = 257 listOf( 258 TileViewModel( 259 defaultDrawable = category.imageCategoryData?.defaultDrawable, 260 thumbnailAsset = category.imageCategoryData?.thumbnailAsset, 261 text = category.commonCategoryData.title, 262 maxCategoriesInRow = SectionCardinality.Single, 263 ) { 264 // TODO(b/352081782): trigger the effect with effect controller 265 navigateToPhotosPicker(null) 266 } 267 ), 268 columnCount = 3, 269 sectionTitle = context.getString(R.string.choose_a_wallpaper_section_title), 270 ) 271 } 272 273 val sections: Flow<List<SectionViewModel>> = 274 combine(individualSectionViewModels, creativeSectionViewModel, myPhotosSectionViewModel) { 275 individualViewModels, 276 creativeViewModel, 277 myPhotosViewModel -> 278 buildList { 279 add(creativeViewModel) 280 add(myPhotosViewModel) 281 addAll(individualViewModels) 282 } 283 } 284 285 val isLoading: Flow<Boolean> = loadindStatusInteractor.isLoading 286 287 /** A [Flow] to indicate when the network status has been made enabled */ 288 val isConnectionObtained: Flow<Boolean> = networkStatusInteractor.isConnectionObtained 289 290 /** This method updates network categories */ 291 fun refreshNetworkCategories() { 292 singleCategoryInteractor.refreshNetworkCategories() 293 } 294 295 /** This method updates the photos category */ 296 fun updateMyPhotosCategory() { 297 myPhotosInteractor.updateMyPhotos() 298 } 299 300 /** This method updates the specified category */ 301 fun refreshCategory() { 302 // update creative categories at this time only 303 creativeCategoryInteractor.updateCreativeCategories() 304 } 305 306 enum class CategoryType { 307 ThirdPartyCategories, 308 DefaultCategories, 309 CreativeCategories, 310 MyPhotosCategories, 311 Default, 312 } 313 314 sealed class NavigationEvent { 315 data class NavigateToWallpaperCollection( 316 val categoryId: String, 317 val categoryType: CategoryType, 318 ) : NavigationEvent() 319 320 data class NavigateToPreviewScreen( 321 val wallpaperModel: WallpaperModel, 322 val categoryType: CategoryType, 323 ) : NavigationEvent() 324 325 data class NavigateToPhotosPicker(val wallpaperModel: WallpaperModel?) : NavigationEvent() 326 327 data class NavigateToThirdParty(val resolveInfo: ResolveInfo) : NavigationEvent() 328 } 329 } 330