1 /*
<lambda>null2  * Copyright 2019 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 androidx.paging
18 
19 import androidx.annotation.RestrictTo
20 import androidx.paging.DataSource.KeyType.ITEM_KEYED
21 import androidx.paging.DataSource.KeyType.PAGE_KEYED
22 import androidx.paging.DataSource.KeyType.POSITIONAL
23 import androidx.paging.DataSource.Params
24 import androidx.paging.LoadType.APPEND
25 import androidx.paging.LoadType.PREPEND
26 import androidx.paging.LoadType.REFRESH
27 import androidx.paging.internal.BUGANIZER_URL
28 import kotlin.coroutines.CoroutineContext
29 import kotlinx.coroutines.DelicateCoroutinesApi
30 import kotlinx.coroutines.withContext
31 
32 /** A wrapper around [DataSource] which adapts it to the [PagingSource] API. */
33 @OptIn(DelicateCoroutinesApi::class)
34 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
35 public class LegacyPagingSource<Key : Any, Value : Any>(
36     private val fetchContext: CoroutineContext,
37     internal val dataSource: DataSource<Key, Value>
38 ) : PagingSource<Key, Value>(), CompatLegacyPagingSource {
39     private var pageSize: Int = PAGE_SIZE_NOT_SET
40 
41     init {
42         dataSource.addInvalidatedCallback(::invalidate)
43         // technically, there is a possibly race where data source might call back our invalidate.
44         // in practice, it is fine because all fields are initialized at this point.
45         registerInvalidatedCallback {
46             dataSource.removeInvalidatedCallback(::invalidate)
47             dataSource.invalidate()
48         }
49     }
50 
51     /**  */
52     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
53     public override fun setPageSize(pageSize: Int) {
54         check(this.pageSize == PAGE_SIZE_NOT_SET || pageSize == this.pageSize) {
55             "Page size is already set to ${this.pageSize}."
56         }
57         this.pageSize = pageSize
58     }
59 
60     /**
61      * This only ever happens in testing if Pager / PagedList is not used hence we'll not get the
62      * page size. For those cases, guess :).
63      */
64     private fun guessPageSize(params: LoadParams<Key>): Int {
65         if (params is LoadParams.Refresh) {
66             if (params.loadSize % PagingConfig.DEFAULT_INITIAL_PAGE_MULTIPLIER == 0) {
67                 return params.loadSize / PagingConfig.DEFAULT_INITIAL_PAGE_MULTIPLIER
68             }
69         }
70         return params.loadSize
71     }
72 
73     override suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value> {
74         val type =
75             when (params) {
76                 is LoadParams.Refresh -> REFRESH
77                 is LoadParams.Append -> APPEND
78                 is LoadParams.Prepend -> PREPEND
79             }
80         if (pageSize == PAGE_SIZE_NOT_SET) {
81             // println because we don't have android logger here
82             println(
83                 """
84                 WARNING: pageSize on the LegacyPagingSource is not set.
85                 When using legacy DataSource / DataSourceFactory with Paging3, page size
86                 should've been set by the paging library but it is not set yet.
87 
88                 If you are seeing this message in tests where you are testing DataSource
89                 in isolation (without a Pager), it is expected and page size will be estimated
90                 based on parameters.
91 
92                 If you are seeing this message despite using a Pager, please file a bug:
93                 $BUGANIZER_URL
94                 """
95                     .trimIndent()
96             )
97             pageSize = guessPageSize(params)
98         }
99         val dataSourceParams =
100             Params(type, params.key, params.loadSize, params.placeholdersEnabled, pageSize)
101 
102         return withContext(fetchContext) {
103             dataSource.load(dataSourceParams).run {
104                 LoadResult.Page(
105                     data,
106                     @Suppress("UNCHECKED_CAST")
107                     if (data.isEmpty() && params is LoadParams.Prepend) null else prevKey as Key?,
108                     @Suppress("UNCHECKED_CAST")
109                     if (data.isEmpty() && params is LoadParams.Append) null else nextKey as Key?,
110                     itemsBefore,
111                     itemsAfter
112                 )
113             }
114         }
115     }
116 
117     @Suppress("UNCHECKED_CAST")
118     override fun getRefreshKey(state: PagingState<Key, Value>): Key? {
119         return when (dataSource.type) {
120             POSITIONAL ->
121                 state.anchorPosition?.let { anchorPosition ->
122                     state.anchorPositionToPagedIndices(anchorPosition) { _, indexInPage ->
123                         val offset = state.closestPageToPosition(anchorPosition)?.prevKey ?: 0
124                         (offset as Int).plus(indexInPage) as Key?
125                     }
126                 }
127             PAGE_KEYED -> null
128             ITEM_KEYED ->
129                 state.anchorPosition
130                     ?.let { anchorPosition -> state.closestItemToPosition(anchorPosition) }
131                     ?.let { item -> dataSource.getKeyInternal(item) }
132         }
133     }
134 
135     override val jumpingSupported: Boolean
136         get() = dataSource.type == POSITIONAL
137 
138     private companion object {
139         private const val PAGE_SIZE_NOT_SET = Int.MIN_VALUE
140     }
141 }
142