• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.extensions
18 
19 import androidx.paging.PagingData
20 import androidx.paging.insertSeparators
21 import androidx.paging.map
22 import com.android.photopicker.core.components.MediaGridItem
23 import com.android.photopicker.core.user.UserProfile
24 import com.android.photopicker.core.user.UserStatus
25 import com.android.photopicker.data.model.CategoryType
26 import com.android.photopicker.data.model.Group
27 import com.android.photopicker.data.model.Media
28 import java.time.LocalDateTime
29 import java.time.ZoneOffset
30 import java.time.format.DateTimeFormatter
31 import kotlinx.coroutines.flow.Flow
32 import kotlinx.coroutines.flow.map
33 
34 /**
35  * An extension function to prepare a flow of [PagingData<Media>] to be provided to the [MediaGrid]
36  * composable, by wrapping all of the [Media] objects in a [MediaGridItem.MediaItem].
37  *
38  * @return A [PagingData<MediaGridItem.MediaItem] that can be processed further, or provided to the
39  *   [MediaGrid].
40  */
41 fun Flow<PagingData<Media>>.toMediaGridItemFromMedia(): Flow<PagingData<MediaGridItem.MediaItem>> {
42     return this.map { pagingData -> pagingData.map { MediaGridItem.MediaItem(it) } }
43 }
44 
45 /**
46  * An extension function to prepare a flow of [PagingData<Album>] to be provided to the [MediaGrid]
47  * composable, by wrapping all of the [Album] objects in a [MediaGridItem].
48  *
49  * @return A [PagingData<MediaGridItem>] that can be processed further, or provided to the
50  *   [MediaGrid].
51  */
toMediaGridItemFromAlbumnull52 fun Flow<PagingData<Group.Album>>.toMediaGridItemFromAlbum(): Flow<PagingData<MediaGridItem>> {
53     return this.map { pagingData -> pagingData.map { MediaGridItem.AlbumItem(it) } }
54 }
55 
56 /**
57  * An extension function to prepare a flow of [PagingData<MediaSet>] to be provided to the
58  * [MediaGrid] composable, by wrapping all of the [MediaSet] objects in a [MediaGridItem].
59  *
60  * @return A [PagingData<MediaGridItem>] that can be processed further, or provided to the
61  *   [MediaGrid].
62  */
toMediaGridItemFromMediaSetnull63 fun Flow<PagingData<Group.MediaSet>>.toMediaGridItemFromMediaSet():
64     Flow<PagingData<MediaGridItem>> {
65     return this.map { pagingData -> pagingData.map { MediaGridItem.MediaSetItem(it) } }
66 }
67 
68 /**
69  * An extension function to prepare a flow of [PagingData<MediaSet>] for People & Pets category to
70  * be provided to the [MediaGrid] composable, by wrapping all of the [MediaSet] objects in a
71  * [MediaGridItem].
72  *
73  * @return A [PagingData<MediaGridItem>] that can be processed further, or provided to the
74  *   [MediaGrid].
75  */
toMediaGridItemFromPeopleMediaSetnull76 fun Flow<PagingData<Group.MediaSet>>.toMediaGridItemFromPeopleMediaSet():
77     Flow<PagingData<MediaGridItem>> {
78     return this.map { pagingData -> pagingData.map { MediaGridItem.PersonMediaSetItem(it) } }
79 }
80 
81 /**
82  * An extension function to prepare a flow of [PagingData<Category>] to be provided to the
83  * [MediaGrid] composable, by wrapping all of the [Category] objects in a [MediaGridItem].
84  *
85  * @return A [PagingData<MediaGridItem>] that can be processed further, or provided to the
86  *   [MediaGrid].
87  */
toMediaGridItemFromCategorynull88 fun Flow<PagingData<Group>>.toMediaGridItemFromCategory(
89     category: Group.Category? = null
90 ): Flow<PagingData<MediaGridItem>> {
91     return this.map { pagingData ->
92         pagingData.map { group ->
93             when (group) {
94                 is Group.MediaSet -> {
95                     if (category != null && category.categoryType == CategoryType.PEOPLE_AND_PETS) {
96                         MediaGridItem.PersonMediaSetItem(group)
97                     } else {
98                         MediaGridItem.MediaSetItem(group)
99                     }
100                 }
101                 is Group.Category -> MediaGridItem.CategoryItem(group)
102                 is Group.Album -> MediaGridItem.AlbumItem(group)
103             }
104         }
105     }
106 }
107 
108 /**
109  * An extension function which accepts a flow of [PagingData<MediaGridItem.MediaItem>] (the actual
110  * [Media] grid representation wrappers) and processes them inserting month separators in between
111  * items that have different month.
112  *
113  * @return A [PagingData<MediaGridItem>] that can be processed further, or provided to the
114  *   [MediaGrid].
115  *
116  * TODO(b/323830434): Update logic for separators after 4th row when UX finalizes. Note: This does
117  *   not include a separator for the first month of data.
118  */
insertMonthSeparatorsnull119 fun Flow<PagingData<MediaGridItem.MediaItem>>.insertMonthSeparators(
120     recentsCellCount: Int = Int.MIN_VALUE
121 ): Flow<PagingData<MediaGridItem>> {
122     return this.map {
123         it.insertSeparators { before, after ->
124             val afterIndex = after?.media?.index ?: Int.MAX_VALUE
125 
126             // If this is the first or last item in the list, no separators are required.
127             // If the item index is populated and it is part of the recents section,
128             // don't add separators.
129             if (after == null || before == null || afterIndex <= recentsCellCount) {
130                 return@insertSeparators null
131             }
132 
133             // ZoneOffset.UTC is used here because all timestamps are expected to be millisecionds
134             // since epoch in UTC. See [CloudMediaProviderContract#MediaColumns.DATE_TAKEN_MILLIS]
135             val beforeLocalDateTime =
136                 LocalDateTime.ofEpochSecond((before.media.getTimestamp() / 1000), 0, ZoneOffset.UTC)
137             val afterLocalDateTime =
138                 LocalDateTime.ofEpochSecond((after.media.getTimestamp() / 1000), 0, ZoneOffset.UTC)
139 
140             // Always add a separator after the recents section.
141             if (
142                 beforeLocalDateTime.getMonth() != afterLocalDateTime.getMonth() ||
143                     afterIndex == (recentsCellCount + 1)
144             ) {
145                 val format =
146                     // If the current calendar year is different from the items year, append the
147                     // year to to the month string.
148                     if (afterLocalDateTime.getYear() != LocalDateTime.now().getYear()) "MMMM yyyy"
149 
150                     // The year is the same, so just use the month's name.
151                     else "MMMM"
152 
153                 // The months are different, so insert a separator between [before] and [after]
154                 // by returning it here.
155                 MediaGridItem.SeparatorItem(
156                     afterLocalDateTime.format(DateTimeFormatter.ofPattern(format))
157                 )
158             } else {
159                 // Both Media have the same month, so no separator needed between the two.
160                 null
161             }
162         }
163     }
164 }
165 
166 /**
167  * An extension function which filters all the available user profiles based on whether a profile is
168  * hidden or not.
169  *
170  * @return A list of all the user profiles available to the photopicker
171  */
getUserProfilesVisibleToPhotopickernull172 fun Flow<UserStatus>.getUserProfilesVisibleToPhotopicker(): Flow<List<UserProfile>> {
173     return this.map {
174         it.allProfiles.filterNot {
175             it.disabledReasons.contains(UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW)
176         }
177     }
178 }
179