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.photopicker.data.paging
18 
19 import android.net.Uri
20 import androidx.paging.PagingSource
21 import androidx.paging.PagingSource.LoadParams
22 import androidx.paging.PagingSource.LoadResult
23 import androidx.paging.PagingState
24 import com.android.photopicker.data.model.Group
25 import com.android.photopicker.data.model.MediaPageKey
26 import com.android.photopicker.data.model.MediaSource
27 import java.time.LocalDateTime
28 import java.time.ZoneOffset
29 import java.time.temporal.ChronoUnit
30 
31 /**
32  * This [FakeInMemoryAlbumPagingSource] class is responsible to providing paginated album data from
33  * Picker Database by serving requests from Paging library.
34  *
35  * It generates and returns its own fake data.
36  */
37 class FakeInMemoryAlbumPagingSource
38 private constructor(
39     val DATA_SIZE: Int = DEFAULT_SIZE,
40     private val DATA_LIST: List<Group.Album>? = null,
41 ) : PagingSource<MediaPageKey, Group.Album>() {
42 
43     companion object {
44         const val TEST_ALBUM_NAME_PREFIX = "AlbumNumber_"
45         const val DEFAULT_SIZE = 1_000
46     }
47 
48     constructor(dataSize: Int = DEFAULT_SIZE) : this(dataSize, null)
49 
50     constructor(dataList: List<Group.Album>) : this(DEFAULT_SIZE, dataList)
51 
52     private val currentDateTime = LocalDateTime.now()
53 
54     // If a [DATA_LIST] was provided, use it, otherwise generate a list of the requested size.
55     val DATA =
56         DATA_LIST
57             ?: buildList<Group.Album> {
58                 for (i in 1..DATA_SIZE) {
59                     add(
60                         Group.Album(
61                             id = "$i",
62                             pickerId = i.toLong(),
63                             authority = "a",
64                             displayName = TEST_ALBUM_NAME_PREFIX + "$i",
65                             coverUri =
66                                 Uri.EMPTY.buildUpon()
67                                     .apply {
68                                         scheme("content")
69                                         authority("a")
70                                         path("$i")
71                                     }
72                                     .build(),
73                             dateTakenMillisLong =
74                                 currentDateTime
75                                     .minus(i.toLong(), ChronoUnit.DAYS)
76                                     .toEpochSecond(ZoneOffset.UTC) * 1000,
77                             coverMediaSource = MediaSource.LOCAL,
78                         )
79                     )
80                 }
81             }
82 
83     override suspend fun load(
84         params: LoadParams<MediaPageKey>
85     ): LoadResult<MediaPageKey, Group.Album> {
86 
87         // Handle a data size of 0 for the first page, and return an empty page with no further
88         // keys.
89         if (DATA.size == 0 && params.key == null) {
90             return LoadResult.Page(data = emptyList(), nextKey = null, prevKey = null)
91         }
92 
93         // This is inefficient, but a reliable way to locate the record being requested by the
94         // [MediaPageKey] without having to keep track of offsets.
95         val startIndex =
96             if (params.key == null) {
97                 0
98             } else {
99                 DATA.indexOfFirst({ item -> item.pickerId == params.key?.pickerId ?: 1 })
100             }
101 
102         // The list is zero-based, and loadSize isn't; so, offset by 1
103         val endIndex = Math.min((startIndex + params.loadSize) - 1, DATA.lastIndex)
104 
105         // Item at start position doesn't exist, so this isn't a valid page.
106         if (DATA.getOrNull(startIndex) == null) {
107             return LoadResult.Invalid()
108         }
109 
110         val pageData = DATA.slice(startIndex..endIndex)
111 
112         // Find the start of the next page and generate a Page key.
113         val nextRow = DATA.getOrNull(endIndex + 1)
114         val nextKey =
115             if (nextRow == null) {
116                 null
117             } else {
118                 MediaPageKey(
119                     pickerId = nextRow.pickerId,
120                     dateTakenMillis = nextRow.dateTakenMillisLong,
121                 )
122             }
123 
124         // Find the start of the previous page and generate a Page key.
125         val prevPageRow = DATA.getOrNull((startIndex) - params.loadSize)
126         val prevKey =
127             if (prevPageRow == null) {
128                 null
129             } else {
130                 MediaPageKey(
131                     pickerId = prevPageRow.pickerId,
132                     dateTakenMillis = prevPageRow.dateTakenMillisLong,
133                 )
134             }
135 
136         return LoadResult.Page(data = pageData, nextKey = nextKey, prevKey = prevKey)
137     }
138 
139     override fun getRefreshKey(state: PagingState<MediaPageKey, Group.Album>): MediaPageKey? {
140         return state.anchorPosition?.let { null }
141     }
142 }
143