1/*
2 * Copyright 2018 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
17package androidx.paging;
18
19import androidx.annotation.AnyThread;
20import androidx.annotation.NonNull;
21import androidx.annotation.Nullable;
22import androidx.annotation.WorkerThread;
23
24import java.util.concurrent.Executor;
25
26class TiledPagedList<T> extends PagedList<T>
27        implements PagedStorage.Callback {
28    private final PositionalDataSource<T> mDataSource;
29
30    private PageResult.Receiver<T> mReceiver = new PageResult.Receiver<T>() {
31        // Creation thread for initial synchronous load, otherwise main thread
32        // Safe to access main thread only state - no other thread has reference during construction
33        @AnyThread
34        @Override
35        public void onPageResult(@PageResult.ResultType int type,
36                @NonNull PageResult<T> pageResult) {
37            if (pageResult.isInvalid()) {
38                detach();
39                return;
40            }
41
42            if (isDetached()) {
43                // No op, have detached
44                return;
45            }
46
47            if (type != PageResult.INIT && type != PageResult.TILE) {
48                throw new IllegalArgumentException("unexpected resultType" + type);
49            }
50
51            if (mStorage.getPageCount() == 0) {
52                mStorage.initAndSplit(
53                        pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls,
54                        pageResult.positionOffset, mConfig.pageSize, TiledPagedList.this);
55            } else {
56                mStorage.insertPage(pageResult.positionOffset, pageResult.page,
57                        TiledPagedList.this);
58            }
59
60            if (mBoundaryCallback != null) {
61                boolean deferEmpty = mStorage.size() == 0;
62                boolean deferBegin = !deferEmpty
63                        && pageResult.leadingNulls == 0
64                        && pageResult.positionOffset == 0;
65                int size = size();
66                boolean deferEnd = !deferEmpty
67                        && ((type == PageResult.INIT && pageResult.trailingNulls == 0)
68                                || (type == PageResult.TILE
69                                        && (pageResult.positionOffset + mConfig.pageSize >= size)));
70                deferBoundaryCallbacks(deferEmpty, deferBegin, deferEnd);
71            }
72        }
73    };
74
75    @WorkerThread
76    TiledPagedList(@NonNull PositionalDataSource<T> dataSource,
77            @NonNull Executor mainThreadExecutor,
78            @NonNull Executor backgroundThreadExecutor,
79            @Nullable BoundaryCallback<T> boundaryCallback,
80            @NonNull Config config,
81            int position) {
82        super(new PagedStorage<T>(), mainThreadExecutor, backgroundThreadExecutor,
83                boundaryCallback, config);
84        mDataSource = dataSource;
85
86        final int pageSize = mConfig.pageSize;
87        mLastLoad = position;
88
89        if (mDataSource.isInvalid()) {
90            detach();
91        } else {
92            final int firstLoadSize =
93                    (Math.max(Math.round(mConfig.initialLoadSizeHint / pageSize), 2)) * pageSize;
94
95            final int idealStart = position - firstLoadSize / 2;
96            final int roundedPageStart = Math.max(0, Math.round(idealStart / pageSize) * pageSize);
97
98            mDataSource.dispatchLoadInitial(true, roundedPageStart, firstLoadSize,
99                    pageSize, mMainThreadExecutor, mReceiver);
100        }
101    }
102
103    @Override
104    boolean isContiguous() {
105        return false;
106    }
107
108    @NonNull
109    @Override
110    public DataSource<?, T> getDataSource() {
111        return mDataSource;
112    }
113
114    @Nullable
115    @Override
116    public Object getLastKey() {
117        return mLastLoad;
118    }
119
120    @Override
121    protected void dispatchUpdatesSinceSnapshot(@NonNull PagedList<T> pagedListSnapshot,
122            @NonNull Callback callback) {
123        //noinspection UnnecessaryLocalVariable
124        final PagedStorage<T> snapshot = pagedListSnapshot.mStorage;
125
126        if (snapshot.isEmpty()
127                || mStorage.size() != snapshot.size()) {
128            throw new IllegalArgumentException("Invalid snapshot provided - doesn't appear"
129                    + " to be a snapshot of this PagedList");
130        }
131
132        // loop through each page and signal the callback for any pages that are present now,
133        // but not in the snapshot.
134        final int pageSize = mConfig.pageSize;
135        final int leadingNullPages = mStorage.getLeadingNullCount() / pageSize;
136        final int pageCount = mStorage.getPageCount();
137        for (int i = 0; i < pageCount; i++) {
138            int pageIndex = i + leadingNullPages;
139            int updatedPages = 0;
140            // count number of consecutive pages that were added since the snapshot...
141            while (updatedPages < mStorage.getPageCount()
142                    && mStorage.hasPage(pageSize, pageIndex + updatedPages)
143                    && !snapshot.hasPage(pageSize, pageIndex + updatedPages)) {
144                updatedPages++;
145            }
146            // and signal them all at once to the callback
147            if (updatedPages > 0) {
148                callback.onChanged(pageIndex * pageSize, pageSize * updatedPages);
149                i += updatedPages - 1;
150            }
151        }
152    }
153
154    @Override
155    protected void loadAroundInternal(int index) {
156        mStorage.allocatePlaceholders(index, mConfig.prefetchDistance, mConfig.pageSize, this);
157    }
158
159    @Override
160    public void onInitialized(int count) {
161        notifyInserted(0, count);
162    }
163
164    @Override
165    public void onPagePrepended(int leadingNulls, int changed, int added) {
166        throw new IllegalStateException("Contiguous callback on TiledPagedList");
167    }
168
169    @Override
170    public void onPageAppended(int endPosition, int changed, int added) {
171        throw new IllegalStateException("Contiguous callback on TiledPagedList");
172    }
173
174    @Override
175    public void onPagePlaceholderInserted(final int pageIndex) {
176        // placeholder means initialize a load
177        mBackgroundThreadExecutor.execute(new Runnable() {
178            @Override
179            public void run() {
180                if (isDetached()) {
181                    return;
182                }
183                final int pageSize = mConfig.pageSize;
184
185                if (mDataSource.isInvalid()) {
186                    detach();
187                } else {
188                    int startPosition = pageIndex * pageSize;
189                    int count = Math.min(pageSize, mStorage.size() - startPosition);
190                    mDataSource.dispatchLoadRange(
191                            PageResult.TILE, startPosition, count, mMainThreadExecutor, mReceiver);
192                }
193            }
194        });
195    }
196
197    @Override
198    public void onPageInserted(int start, int count) {
199        notifyChanged(start, count);
200    }
201}
202