1/*
2 * Copyright (C) 2017 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 android.arch.persistence.room.paging;
18
19import android.arch.paging.TiledDataSource;
20import android.arch.persistence.room.InvalidationTracker;
21import android.arch.persistence.room.RoomDatabase;
22import android.arch.persistence.room.RoomSQLiteQuery;
23import android.database.Cursor;
24import android.support.annotation.NonNull;
25import android.support.annotation.Nullable;
26import android.support.annotation.RestrictTo;
27
28import java.util.List;
29import java.util.Set;
30
31/**
32 * A simple data source implementation that uses Limit & Offset to page the query.
33 * <p>
34 * This is NOT the most efficient way to do paging on SQLite. It is
35 * <a href="http://www.sqlite.org/cvstrac/wiki?p=ScrollingCursor">recommended</a> to use an indexed
36 * ORDER BY statement but that requires a more complex API. This solution is technically equal to
37 * receiving a {@link Cursor} from a large query but avoids the need to manually manage it, and
38 * never returns inconsistent data if it is invalidated.
39 *
40 * @param <T> Data type returned by the data source.
41 *
42 * @hide
43 */
44@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
45public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> {
46    private final RoomSQLiteQuery mSourceQuery;
47    private final String mCountQuery;
48    private final String mLimitOffsetQuery;
49    private final RoomDatabase mDb;
50    @SuppressWarnings("FieldCanBeLocal")
51    private final InvalidationTracker.Observer mObserver;
52
53    protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query, String... tables) {
54        mDb = db;
55        mSourceQuery = query;
56        mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
57        mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
58        mObserver = new InvalidationTracker.Observer(tables) {
59            @Override
60            public void onInvalidated(@NonNull Set<String> tables) {
61                invalidate();
62            }
63        };
64        db.getInvalidationTracker().addWeakObserver(mObserver);
65    }
66
67    @Override
68    public int countItems() {
69        final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
70                mSourceQuery.getArgCount());
71        sqLiteQuery.copyArgumentsFrom(mSourceQuery);
72        Cursor cursor = mDb.query(sqLiteQuery);
73        try {
74            if (cursor.moveToFirst()) {
75                return cursor.getInt(0);
76            }
77            return 0;
78        } finally {
79            cursor.close();
80            sqLiteQuery.release();
81        }
82    }
83
84    @Override
85    public boolean isInvalid() {
86        mDb.getInvalidationTracker().refreshVersionsSync();
87        return super.isInvalid();
88    }
89
90    @SuppressWarnings("WeakerAccess")
91    protected abstract List<T> convertRows(Cursor cursor);
92
93    @Nullable
94    @Override
95    public List<T> loadRange(int startPosition, int loadCount) {
96        final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
97                mSourceQuery.getArgCount() + 2);
98        sqLiteQuery.copyArgumentsFrom(mSourceQuery);
99        sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
100        sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
101        Cursor cursor = mDb.query(sqLiteQuery);
102
103        try {
104            return convertRows(cursor);
105        } finally {
106            cursor.close();
107            sqLiteQuery.release();
108        }
109    }
110}
111