|  | /* | 
|  | * Copyright 2017 The Android Open Source Project | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. | 
|  | */ | 
|  |  | 
|  | package android.arch.paging; | 
|  |  | 
|  | import android.support.annotation.NonNull; | 
|  | import android.support.annotation.Nullable; | 
|  | import android.support.annotation.WorkerThread; | 
|  |  | 
|  | import java.util.Collections; | 
|  | import java.util.List; | 
|  | import java.util.concurrent.Executor; | 
|  |  | 
|  | /** | 
|  | * Position-based data loader for a fixed-size, countable data set, supporting fixed-size loads at | 
|  | * arbitrary page positions. | 
|  | * <p> | 
|  | * Extend PositionalDataSource if you can load pages of a requested size at arbitrary | 
|  | * positions, and provide a fixed item count. If your data source can't support loading arbitrary | 
|  | * requested page sizes (e.g. when network page size constraints are only known at runtime), use | 
|  | * either {@link PageKeyedDataSource} or {@link ItemKeyedDataSource} instead. | 
|  | * <p> | 
|  | * Note that unless {@link PagedList.Config#enablePlaceholders placeholders are disabled} | 
|  | * PositionalDataSource requires counting the size of the data set. This allows pages to be tiled in | 
|  | * at arbitrary, non-contiguous locations based upon what the user observes in a {@link PagedList}. | 
|  | * If placeholders are disabled, initialize with the two parameter | 
|  | * {@link LoadInitialCallback#onResult(List, int)}. | 
|  | * <p> | 
|  | * Room can generate a Factory of PositionalDataSources for you: | 
|  | * <pre> | 
|  | * {@literal @}Dao | 
|  | * interface UserDao { | 
|  | *     {@literal @}Query("SELECT * FROM user ORDER BY mAge DESC") | 
|  | *     public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc(); | 
|  | * }</pre> | 
|  | * | 
|  | * @param <T> Type of items being loaded by the PositionalDataSource. | 
|  | */ | 
|  | public abstract class PositionalDataSource<T> extends DataSource<Integer, T> { | 
|  |  | 
|  | /** | 
|  | * Holder object for inputs to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}. | 
|  | */ | 
|  | @SuppressWarnings("WeakerAccess") | 
|  | public static class LoadInitialParams { | 
|  | /** | 
|  | * Initial load position requested. | 
|  | * <p> | 
|  | * Note that this may not be within the bounds of your data set, it may need to be adjusted | 
|  | * before you execute your load. | 
|  | */ | 
|  | public final int requestedStartPosition; | 
|  |  | 
|  | /** | 
|  | * Requested number of items to load. | 
|  | * <p> | 
|  | * Note that this may be larger than available data. | 
|  | */ | 
|  | public final int requestedLoadSize; | 
|  |  | 
|  | /** | 
|  | * Defines page size acceptable for return values. | 
|  | * <p> | 
|  | * List of items passed to the callback must be an integer multiple of page size. | 
|  | */ | 
|  | public final int pageSize; | 
|  |  | 
|  | /** | 
|  | * Defines whether placeholders are enabled, and whether the total count passed to | 
|  | * {@link LoadInitialCallback#onResult(List, int, int)} will be ignored. | 
|  | */ | 
|  | public final boolean placeholdersEnabled; | 
|  |  | 
|  | LoadInitialParams( | 
|  | int requestedStartPosition, | 
|  | int requestedLoadSize, | 
|  | int pageSize, | 
|  | boolean placeholdersEnabled) { | 
|  | this.requestedStartPosition = requestedStartPosition; | 
|  | this.requestedLoadSize = requestedLoadSize; | 
|  | this.pageSize = pageSize; | 
|  | this.placeholdersEnabled = placeholdersEnabled; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Holder object for inputs to {@link #loadRange(LoadRangeParams, LoadRangeCallback)}. | 
|  | */ | 
|  | @SuppressWarnings("WeakerAccess") | 
|  | public static class LoadRangeParams { | 
|  | /** | 
|  | * Start position of data to load. | 
|  | * <p> | 
|  | * Returned data must start at this position. | 
|  | */ | 
|  | public final int startPosition; | 
|  | /** | 
|  | * Number of items to load. | 
|  | * <p> | 
|  | * Returned data must be of this size, unless at end of the list. | 
|  | */ | 
|  | public final int loadSize; | 
|  |  | 
|  | LoadRangeParams(int startPosition, int loadSize) { | 
|  | this.startPosition = startPosition; | 
|  | this.loadSize = loadSize; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Callback for {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} | 
|  | * to return data, position, and count. | 
|  | * <p> | 
|  | * A callback can be called only once, and will throw if called again. | 
|  | * <p> | 
|  | * It is always valid for a DataSource loading method that takes a callback to stash the | 
|  | * callback and call it later. This enables DataSources to be fully asynchronous, and to handle | 
|  | * temporary, recoverable error states (such as a network error that can be retried). | 
|  | * | 
|  | * @param <T> Type of items being loaded. | 
|  | */ | 
|  | public static class LoadInitialCallback<T> extends BaseLoadCallback<T> { | 
|  | private final boolean mCountingEnabled; | 
|  | private final int mPageSize; | 
|  |  | 
|  | LoadInitialCallback(@NonNull PositionalDataSource dataSource, boolean countingEnabled, | 
|  | int pageSize, PageResult.Receiver<T> receiver) { | 
|  | super(dataSource, PageResult.INIT, null, receiver); | 
|  | mCountingEnabled = countingEnabled; | 
|  | mPageSize = pageSize; | 
|  | if (mPageSize < 1) { | 
|  | throw new IllegalArgumentException("Page size must be non-negative"); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Called to pass initial load state from a DataSource. | 
|  | * <p> | 
|  | * Call this method from your DataSource's {@code loadInitial} function to return data, | 
|  | * and inform how many placeholders should be shown before and after. If counting is cheap | 
|  | * to compute (for example, if a network load returns the information regardless), it's | 
|  | * recommended to pass the total size to the totalCount parameter. If placeholders are not | 
|  | * requested (when {@link LoadInitialParams#placeholdersEnabled} is false), you can instead | 
|  | * call {@link #onResult(List, int)}. | 
|  | * | 
|  | * @param data List of items loaded from the DataSource. If this is empty, the DataSource | 
|  | *             is treated as empty, and no further loads will occur. | 
|  | * @param position Position of the item at the front of the list. If there are {@code N} | 
|  | *                 items before the items in data that can be loaded from this DataSource, | 
|  | *                 pass {@code N}. | 
|  | * @param totalCount Total number of items that may be returned from this DataSource. | 
|  | *                   Includes the number in the initial {@code data} parameter | 
|  | *                   as well as any items that can be loaded in front or behind of | 
|  | *                   {@code data}. | 
|  | */ | 
|  | public void onResult(@NonNull List<T> data, int position, int totalCount) { | 
|  | if (!dispatchInvalidResultIfInvalid()) { | 
|  | validateInitialLoadParams(data, position, totalCount); | 
|  | if (position + data.size() != totalCount | 
|  | && data.size() % mPageSize != 0) { | 
|  | throw new IllegalArgumentException("PositionalDataSource requires initial load" | 
|  | + " size to be a multiple of page size to support internal tiling." | 
|  | + " loadSize " + data.size() + ", position " + position | 
|  | + ", totalCount " + totalCount + ", pageSize " + mPageSize); | 
|  | } | 
|  |  | 
|  | if (mCountingEnabled) { | 
|  | int trailingUnloadedCount = totalCount - position - data.size(); | 
|  | dispatchResultToReceiver( | 
|  | new PageResult<>(data, position, trailingUnloadedCount, 0)); | 
|  | } else { | 
|  | // Only occurs when wrapped as contiguous | 
|  | dispatchResultToReceiver(new PageResult<>(data, position)); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Called to pass initial load state from a DataSource without total count, | 
|  | * when placeholders aren't requested. | 
|  | * <p class="note"><strong>Note:</strong> This method can only be called when placeholders | 
|  | * are disabled ({@link LoadInitialParams#placeholdersEnabled} is false). | 
|  | * <p> | 
|  | * Call this method from your DataSource's {@code loadInitial} function to return data, | 
|  | * if position is known but total size is not. If placeholders are requested, call the three | 
|  | * parameter variant: {@link #onResult(List, int, int)}. | 
|  | * | 
|  | * @param data List of items loaded from the DataSource. If this is empty, the DataSource | 
|  | *             is treated as empty, and no further loads will occur. | 
|  | * @param position Position of the item at the front of the list. If there are {@code N} | 
|  | *                 items before the items in data that can be provided by this DataSource, | 
|  | *                 pass {@code N}. | 
|  | */ | 
|  | @SuppressWarnings("WeakerAccess") | 
|  | public void onResult(@NonNull List<T> data, int position) { | 
|  | if (!dispatchInvalidResultIfInvalid()) { | 
|  | if (position < 0) { | 
|  | throw new IllegalArgumentException("Position must be non-negative"); | 
|  | } | 
|  | if (data.isEmpty() && position != 0) { | 
|  | throw new IllegalArgumentException( | 
|  | "Initial result cannot be empty if items are present in data set."); | 
|  | } | 
|  | if (mCountingEnabled) { | 
|  | throw new IllegalStateException("Placeholders requested, but totalCount not" | 
|  | + " provided. Please call the three-parameter onResult method, or" | 
|  | + " disable placeholders in the PagedList.Config"); | 
|  | } | 
|  | dispatchResultToReceiver(new PageResult<>(data, position)); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Callback for PositionalDataSource {@link #loadRange(LoadRangeParams, LoadRangeCallback)} | 
|  | * to return data. | 
|  | * <p> | 
|  | * A callback can be called only once, and will throw if called again. | 
|  | * <p> | 
|  | * It is always valid for a DataSource loading method that takes a callback to stash the | 
|  | * callback and call it later. This enables DataSources to be fully asynchronous, and to handle | 
|  | * temporary, recoverable error states (such as a network error that can be retried). | 
|  | * | 
|  | * @param <T> Type of items being loaded. | 
|  | */ | 
|  | public static class LoadRangeCallback<T> extends BaseLoadCallback<T> { | 
|  | private final int mPositionOffset; | 
|  | LoadRangeCallback(@NonNull PositionalDataSource dataSource, | 
|  | @PageResult.ResultType int resultType, int positionOffset, | 
|  | Executor mainThreadExecutor, PageResult.Receiver<T> receiver) { | 
|  | super(dataSource, resultType, mainThreadExecutor, receiver); | 
|  | mPositionOffset = positionOffset; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Called to pass loaded data from {@link #loadRange(LoadRangeParams, LoadRangeCallback)}. | 
|  | * | 
|  | * @param data List of items loaded from the DataSource. Must be same size as requested, | 
|  | *             unless at end of list. | 
|  | */ | 
|  | public void onResult(@NonNull List<T> data) { | 
|  | if (!dispatchInvalidResultIfInvalid()) { | 
|  | dispatchResultToReceiver(new PageResult<>( | 
|  | data, 0, 0, mPositionOffset)); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | final void dispatchLoadInitial(boolean acceptCount, | 
|  | int requestedStartPosition, int requestedLoadSize, int pageSize, | 
|  | @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) { | 
|  | LoadInitialCallback<T> callback = | 
|  | new LoadInitialCallback<>(this, acceptCount, pageSize, receiver); | 
|  |  | 
|  | LoadInitialParams params = new LoadInitialParams( | 
|  | requestedStartPosition, requestedLoadSize, pageSize, acceptCount); | 
|  | loadInitial(params, callback); | 
|  |  | 
|  | // If initialLoad's callback is not called within the body, we force any following calls | 
|  | // to post to the UI thread. This constructor may be run on a background thread, but | 
|  | // after constructor, mutation must happen on UI thread. | 
|  | callback.setPostExecutor(mainThreadExecutor); | 
|  | } | 
|  |  | 
|  | final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition, | 
|  | int count, @NonNull Executor mainThreadExecutor, | 
|  | @NonNull PageResult.Receiver<T> receiver) { | 
|  | LoadRangeCallback<T> callback = new LoadRangeCallback<>( | 
|  | this, resultType, startPosition, mainThreadExecutor, receiver); | 
|  | if (count == 0) { | 
|  | callback.onResult(Collections.<T>emptyList()); | 
|  | } else { | 
|  | loadRange(new LoadRangeParams(startPosition, count), callback); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Load initial list data. | 
|  | * <p> | 
|  | * This method is called to load the initial page(s) from the DataSource. | 
|  | * <p> | 
|  | * Result list must be a multiple of pageSize to enable efficient tiling. | 
|  | * | 
|  | * @param params Parameters for initial load, including requested start position, load size, and | 
|  | *               page size. | 
|  | * @param callback Callback that receives initial load data, including | 
|  | *                 position and total data set size. | 
|  | */ | 
|  | @WorkerThread | 
|  | public abstract void loadInitial( | 
|  | @NonNull LoadInitialParams params, | 
|  | @NonNull LoadInitialCallback<T> callback); | 
|  |  | 
|  | /** | 
|  | * Called to load a range of data from the DataSource. | 
|  | * <p> | 
|  | * This method is called to load additional pages from the DataSource after the | 
|  | * LoadInitialCallback passed to dispatchLoadInitial has initialized a PagedList. | 
|  | * <p> | 
|  | * Unlike {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, this method must return | 
|  | * the number of items requested, at the position requested. | 
|  | * | 
|  | * @param params Parameters for load, including start position and load size. | 
|  | * @param callback Callback that receives loaded data. | 
|  | */ | 
|  | @WorkerThread | 
|  | public abstract void loadRange(@NonNull LoadRangeParams params, | 
|  | @NonNull LoadRangeCallback<T> callback); | 
|  |  | 
|  | @Override | 
|  | boolean isContiguous() { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | @NonNull | 
|  | ContiguousDataSource<Integer, T> wrapAsContiguousWithoutPlaceholders() { | 
|  | return new ContiguousWithoutPlaceholdersWrapper<>(this); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Helper for computing an initial position in | 
|  | * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be | 
|  | * computed ahead of loading. | 
|  | * <p> | 
|  | * The value computed by this function will do bounds checking, page alignment, and positioning | 
|  | * based on initial load size requested. | 
|  | * <p> | 
|  | * Example usage in a PositionalDataSource subclass: | 
|  | * <pre> | 
|  | * class ItemDataSource extends PositionalDataSource<Item> { | 
|  | *     private int computeCount() { | 
|  | *         // actual count code here | 
|  | *     } | 
|  | * | 
|  | *     private List<Item> loadRangeInternal(int startPosition, int loadCount) { | 
|  | *         // actual load code here | 
|  | *     } | 
|  | * | 
|  | *     {@literal @}Override | 
|  | *     public void loadInitial({@literal @}NonNull LoadInitialParams params, | 
|  | *             {@literal @}NonNull LoadInitialCallback<Item> callback) { | 
|  | *         int totalCount = computeCount(); | 
|  | *         int position = computeInitialLoadPosition(params, totalCount); | 
|  | *         int loadSize = computeInitialLoadSize(params, position, totalCount); | 
|  | *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount); | 
|  | *     } | 
|  | * | 
|  | *     {@literal @}Override | 
|  | *     public void loadRange({@literal @}NonNull LoadRangeParams params, | 
|  | *             {@literal @}NonNull LoadRangeCallback<Item> callback) { | 
|  | *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize)); | 
|  | *     } | 
|  | * }</pre> | 
|  | * | 
|  | * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, | 
|  | *               including page size, and requested start/loadSize. | 
|  | * @param totalCount Total size of the data set. | 
|  | * @return Position to start loading at. | 
|  | * | 
|  | * @see #computeInitialLoadSize(LoadInitialParams, int, int) | 
|  | */ | 
|  | public static int computeInitialLoadPosition(@NonNull LoadInitialParams params, | 
|  | int totalCount) { | 
|  | int position = params.requestedStartPosition; | 
|  | int initialLoadSize = params.requestedLoadSize; | 
|  | int pageSize = params.pageSize; | 
|  |  | 
|  | int roundedPageStart = Math.round(position / pageSize) * pageSize; | 
|  |  | 
|  | // maximum start pos is that which will encompass end of list | 
|  | int maximumLoadPage = ((totalCount - initialLoadSize + pageSize - 1) / pageSize) * pageSize; | 
|  | roundedPageStart = Math.min(maximumLoadPage, roundedPageStart); | 
|  |  | 
|  | // minimum start position is 0 | 
|  | roundedPageStart = Math.max(0, roundedPageStart); | 
|  |  | 
|  | return roundedPageStart; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Helper for computing an initial load size in | 
|  | * {@link #loadInitial(LoadInitialParams, LoadInitialCallback)} when total data set size can be | 
|  | * computed ahead of loading. | 
|  | * <p> | 
|  | * This function takes the requested load size, and bounds checks it against the value returned | 
|  | * by {@link #computeInitialLoadPosition(LoadInitialParams, int)}. | 
|  | * <p> | 
|  | * Example usage in a PositionalDataSource subclass: | 
|  | * <pre> | 
|  | * class ItemDataSource extends PositionalDataSource<Item> { | 
|  | *     private int computeCount() { | 
|  | *         // actual count code here | 
|  | *     } | 
|  | * | 
|  | *     private List<Item> loadRangeInternal(int startPosition, int loadCount) { | 
|  | *         // actual load code here | 
|  | *     } | 
|  | * | 
|  | *     {@literal @}Override | 
|  | *     public void loadInitial({@literal @}NonNull LoadInitialParams params, | 
|  | *             {@literal @}NonNull LoadInitialCallback<Item> callback) { | 
|  | *         int totalCount = computeCount(); | 
|  | *         int position = computeInitialLoadPosition(params, totalCount); | 
|  | *         int loadSize = computeInitialLoadSize(params, position, totalCount); | 
|  | *         callback.onResult(loadRangeInternal(position, loadSize), position, totalCount); | 
|  | *     } | 
|  | * | 
|  | *     {@literal @}Override | 
|  | *     public void loadRange({@literal @}NonNull LoadRangeParams params, | 
|  | *             {@literal @}NonNull LoadRangeCallback<Item> callback) { | 
|  | *         callback.onResult(loadRangeInternal(params.startPosition, params.loadSize)); | 
|  | *     } | 
|  | * }</pre> | 
|  | * | 
|  | * @param params Params passed to {@link #loadInitial(LoadInitialParams, LoadInitialCallback)}, | 
|  | *               including page size, and requested start/loadSize. | 
|  | * @param initialLoadPosition Value returned by | 
|  | *                          {@link #computeInitialLoadPosition(LoadInitialParams, int)} | 
|  | * @param totalCount Total size of the data set. | 
|  | * @return Number of items to load. | 
|  | * | 
|  | * @see #computeInitialLoadPosition(LoadInitialParams, int) | 
|  | */ | 
|  | @SuppressWarnings("WeakerAccess") | 
|  | public static int computeInitialLoadSize(@NonNull LoadInitialParams params, | 
|  | int initialLoadPosition, int totalCount) { | 
|  | return Math.min(totalCount - initialLoadPosition, params.requestedLoadSize); | 
|  | } | 
|  |  | 
|  | @SuppressWarnings("deprecation") | 
|  | static class ContiguousWithoutPlaceholdersWrapper<Value> | 
|  | extends ContiguousDataSource<Integer, Value> { | 
|  |  | 
|  | @NonNull | 
|  | final PositionalDataSource<Value> mPositionalDataSource; | 
|  |  | 
|  | ContiguousWithoutPlaceholdersWrapper( | 
|  | @NonNull PositionalDataSource<Value> positionalDataSource) { | 
|  | mPositionalDataSource = positionalDataSource; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | void dispatchLoadInitial(@Nullable Integer position, int initialLoadSize, int pageSize, | 
|  | boolean enablePlaceholders, @NonNull Executor mainThreadExecutor, | 
|  | @NonNull PageResult.Receiver<Value> receiver) { | 
|  | final int convertPosition = position == null ? 0 : position; | 
|  |  | 
|  | // Note enablePlaceholders will be false here, but we don't have a way to communicate | 
|  | // this to PositionalDataSource. This is fine, because only the list and its position | 
|  | // offset will be consumed by the LoadInitialCallback. | 
|  | mPositionalDataSource.dispatchLoadInitial(false, convertPosition, initialLoadSize, | 
|  | pageSize, mainThreadExecutor, receiver); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | void dispatchLoadAfter(int currentEndIndex, @NonNull Value currentEndItem, int pageSize, | 
|  | @NonNull Executor mainThreadExecutor, | 
|  | @NonNull PageResult.Receiver<Value> receiver) { | 
|  | int startIndex = currentEndIndex + 1; | 
|  | mPositionalDataSource.dispatchLoadRange( | 
|  | PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | void dispatchLoadBefore(int currentBeginIndex, @NonNull Value currentBeginItem, | 
|  | int pageSize, @NonNull Executor mainThreadExecutor, | 
|  | @NonNull PageResult.Receiver<Value> receiver) { | 
|  |  | 
|  | int startIndex = currentBeginIndex - 1; | 
|  | if (startIndex < 0) { | 
|  | // trigger empty list load | 
|  | mPositionalDataSource.dispatchLoadRange( | 
|  | PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver); | 
|  | } else { | 
|  | int loadSize = Math.min(pageSize, startIndex + 1); | 
|  | startIndex = startIndex - loadSize + 1; | 
|  | mPositionalDataSource.dispatchLoadRange( | 
|  | PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | Integer getKey(int position, Value item) { | 
|  | return position; | 
|  | } | 
|  | } | 
|  | } |