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