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.data.model.Group
24 import com.android.photopicker.data.model.Media
25 import java.time.LocalDateTime
26 import java.time.ZoneOffset
27 import java.time.format.DateTimeFormatter
28 import kotlinx.coroutines.flow.Flow
29 import kotlinx.coroutines.flow.map
30 
31 /**
32  * An extension function to prepare a flow of [PagingData<Media>] to be provided to the [MediaGrid]
33  * composable, by wrapping all of the [Media] objects in a [MediaGridItem.MediaItem].
34  *
35  * @return A [PagingData<MediaGridItem.MediaItem] that can be processed further, or provided to the
36  *   [MediaGrid].
37  */
38 fun Flow<PagingData<Media>>.toMediaGridItemFromMedia(): Flow<PagingData<MediaGridItem.MediaItem>> {
39     return this.map { pagingData -> pagingData.map { MediaGridItem.MediaItem(it) } }
40 }
41 
42 /**
43  * An extension function to prepare a flow of [PagingData<Album>] to be provided to the [MediaGrid]
44  * composable, by wrapping all of the [Album] objects in a [MediaGridItem].
45  *
46  * @return A [PagingData<MediaGridItem>] that can be processed further, or provided to the
47  *   [MediaGrid].
48  */
toMediaGridItemFromAlbumnull49 fun Flow<PagingData<Group.Album>>.toMediaGridItemFromAlbum(): Flow<PagingData<MediaGridItem>> {
50     return this.map { pagingData -> pagingData.map { MediaGridItem.AlbumItem(it) } }
51 }
52 
53 /**
54  * An extension function which accepts a flow of [PagingData<MediaGridItem.MediaItem] (the actual
55  * [Media] grid representation wrappers) and processes them inserting month separators in between
56  * items that have different month.
57  *
58  * TODO(b/323830434): Update logic for separators after 4th row when UX finalizes.
59  * Note: This does not include a separator for the first month of data.
60  *
61  * @return A [PagingData<MediaGridItem] that can be processed further, or provided to the
62  *   [MediaGrid].
63  */
insertMonthSeparatorsnull64 fun Flow<PagingData<MediaGridItem.MediaItem>>.insertMonthSeparators():
65     Flow<PagingData<MediaGridItem>> {
66     return this.map {
67         it.insertSeparators { before, after ->
68 
69             // If this is the first or last item in the list, no separators are required.
70             if (after == null || before == null) {
71                 return@insertSeparators null
72             }
73 
74             // ZoneOffset.UTC is used here because all timestamps are expected to be millisecionds
75             // since epoch in UTC. See [CloudMediaProviderContract#MediaColumns.DATE_TAKEN_MILLIS]
76             val beforeLocalDateTime =
77                 LocalDateTime.ofEpochSecond((before.media.getTimestamp() / 1000), 0, ZoneOffset.UTC)
78             val afterLocalDateTime =
79                 LocalDateTime.ofEpochSecond((after.media.getTimestamp() / 1000), 0, ZoneOffset.UTC)
80 
81             if (beforeLocalDateTime.getMonth() != afterLocalDateTime.getMonth()) {
82                 val format =
83                     // If the current calendar year is different from the items year, append the
84                     // year to to the month string.
85                     if (afterLocalDateTime.getYear() != LocalDateTime.now().getYear()) "MMMM YYYY"
86 
87                     // The year is the same, so just use the month's name.
88                     else "MMMM"
89 
90                 // The months are different, so insert a separator between [before] and [after]
91                 // by returning it here.
92                 MediaGridItem.SeparatorItem(
93                     afterLocalDateTime.format(DateTimeFormatter.ofPattern(format))
94                 )
95             } else {
96                 // Both Media have the same month, so no separator needed between the two.
97                 null
98             }
99         }
100     }
101 }
102