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