• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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.photopicker.features.albumgrid
18 
19 import androidx.lifecycle.ViewModel
20 import androidx.lifecycle.viewModelScope
21 import androidx.paging.Pager
22 import androidx.paging.PagingConfig
23 import androidx.paging.PagingData
24 import androidx.paging.cachedIn
25 import com.android.photopicker.core.components.MediaGridItem
26 import com.android.photopicker.core.events.Event
27 import com.android.photopicker.core.events.Events
28 import com.android.photopicker.core.events.Telemetry
29 import com.android.photopicker.core.features.FeatureToken.ALBUM_GRID
30 import com.android.photopicker.core.selection.Selection
31 import com.android.photopicker.core.selection.SelectionModifiedResult.FAILURE_SELECTION_LIMIT_EXCEEDED
32 import com.android.photopicker.data.DataService
33 import com.android.photopicker.data.model.Group
34 import com.android.photopicker.data.model.Media
35 import com.android.photopicker.extensions.insertMonthSeparators
36 import com.android.photopicker.extensions.toMediaGridItemFromAlbum
37 import com.android.photopicker.extensions.toMediaGridItemFromMedia
38 import dagger.hilt.android.lifecycle.HiltViewModel
39 import javax.inject.Inject
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.flow.Flow
42 import kotlinx.coroutines.flow.MutableStateFlow
43 import kotlinx.coroutines.flow.StateFlow
44 import kotlinx.coroutines.launch
45 
46 /**
47  * The view model for the primary album grid.
48  *
49  * This view model collects the data from [DataService] and caches it in its scope so that loaded
50  * data is saved between navigations so that the composable can maintain list positions when
51  * navigating back and forth between routes.
52  */
53 @HiltViewModel
54 class AlbumGridViewModel
55 @Inject
56 constructor(
57     private val scopeOverride: CoroutineScope?,
58     private val selection: Selection<Media>,
59     private val dataService: DataService,
60     private val events: Events,
61 ) : ViewModel() {
62     // Check if a scope override was injected before using the default [viewModelScope]
63     private val scope: CoroutineScope =
64         if (scopeOverride == null) {
65             this.viewModelScope
66         } else {
67             scopeOverride
68         }
69 
70     // Request Media in batches of 50 items
71     private val ALBUM_GRID_PAGE_SIZE = 50
72 
73     // Keep up to 10 pages loaded in memory before unloading pages.
74     private val ALBUM_GRID_MAX_ITEMS_IN_MEMORY = ALBUM_GRID_PAGE_SIZE * 10
75 
76     private val _previouslySelectedItem = MutableStateFlow<MediaGridItem?>(null)
77     val previouslySelectedItem: StateFlow<MediaGridItem?> = _previouslySelectedItem
78 
79     /**
80      * Sets the previously selected album grid item.
81      *
82      * This function updates the [_previouslySelectedItem] with the provided item. The stored item
83      * is used to request focus to that album's cell in the album grid when the user navigates back
84      * from the album media grid.
85      *
86      * @param item The media grid item to store as the previously selected item, or null to reset
87      */
setPreviouslySelectedItemnull88     fun setPreviouslySelectedItem(item: MediaGridItem?) {
89         _previouslySelectedItem.value = item
90     }
91 
92     /**
93      * Returns [PagingData] of type [MediaGridItem] as a [Flow] containing media for the album
94      * represented by [albumId].
95      */
getAlbumMedianull96     fun getAlbumMedia(album: Group.Album): Flow<PagingData<MediaGridItem>> {
97         val pagerForAlbumMedia =
98             Pager(
99                 PagingConfig(
100                     pageSize = ALBUM_GRID_PAGE_SIZE,
101                     maxSize = ALBUM_GRID_MAX_ITEMS_IN_MEMORY,
102                 )
103             ) {
104                 // pagingSource
105                 dataService.albumMediaPagingSource(album)
106             }
107 
108         /** Export the data from the pager and prepare it for use in the [AlbumMediaGrid] */
109         val albumMedia =
110             pagerForAlbumMedia.flow
111                 .toMediaGridItemFromMedia()
112                 .insertMonthSeparators()
113                 // After the load and transformations, cache the data in the viewModelScope.
114                 // This ensures that the list position and state will be remembered by the MediaGrid
115                 // when navigating back to the AlbumGrid route.
116                 .cachedIn(scope)
117 
118         return albumMedia
119     }
120 
121     /**
122      * Returns [PagingData] of type [MediaGridItem] as a [Flow] containing data for user's albums.
123      */
getAlbumsnull124     fun getAlbums(): Flow<PagingData<MediaGridItem>> {
125         val pagerForAlbums =
126             Pager(
127                 PagingConfig(
128                     pageSize = ALBUM_GRID_PAGE_SIZE,
129                     maxSize = ALBUM_GRID_MAX_ITEMS_IN_MEMORY,
130                 )
131             ) {
132                 dataService.albumPagingSource()
133             }
134 
135         /** Export the data from the pager and prepare it for use in the [AlbumGrid] */
136         val albums =
137             pagerForAlbums.flow
138                 .toMediaGridItemFromAlbum()
139                 // After the load and transformations, cache the data in the viewModelScope.
140                 // This ensures that the list position and state will be remembered by the MediaGrid
141                 // when navigating back to the AlbumGrid route.
142                 .cachedIn(scope)
143         return albums
144     }
145 
146     /**
147      * Click handler that is called when items in the grid are clicked. Selection updates are made
148      * in the viewModelScope to ensure they aren't cancelled if the user navigates away from the
149      * AlbumMediaGrid composable.
150      */
handleAlbumMediaGridItemSelectionnull151     fun handleAlbumMediaGridItemSelection(
152         item: Media,
153         selectionLimitExceededMessage: String,
154         album: Group.Album,
155     ) {
156         // Update the selectable values in the received media item.
157         val updatedMediaItem =
158             Media.withSelectable(item, /* selectionSource */ Telemetry.MediaLocation.ALBUM, album)
159         scope.launch {
160             val result = selection.toggle(updatedMediaItem)
161             if (result == FAILURE_SELECTION_LIMIT_EXCEEDED) {
162                 events.dispatch(
163                     Event.ShowSnackbarMessage(ALBUM_GRID.token, selectionLimitExceededMessage)
164                 )
165             }
166         }
167     }
168 }
169