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