blob: 2f9a8882d5c75c311387d1ae99d68dc84cd16578 [file] [log] [blame]
/*
* Copyright (C) 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.persistence.room.paging;
import android.arch.paging.TiledDataSource;
import android.arch.persistence.room.InvalidationTracker;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.RoomSQLiteQuery;
import android.database.Cursor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import java.util.List;
import java.util.Set;
/**
* A simple data source implementation that uses Limit & Offset to page the query.
* <p>
* This is NOT the most efficient way to do paging on SQLite. It is
* <a href="http://www.sqlite.org/cvstrac/wiki?p=ScrollingCursor">recommended</a> to use an indexed
* ORDER BY statement but that requires a more complex API. This solution is technically equal to
* receiving a {@link Cursor} from a large query but avoids the need to manually manage it, and
* never returns inconsistent data if it is invalidated.
*
* @param <T> Data type returned by the data source.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> {
private final RoomSQLiteQuery mSourceQuery;
private final String mCountQuery;
private final String mLimitOffsetQuery;
private final RoomDatabase mDb;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationTracker.Observer mObserver;
private final boolean mInTransaction;
protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
boolean inTransaction, String... tables) {
mDb = db;
mSourceQuery = query;
mInTransaction = inTransaction;
mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
mObserver = new InvalidationTracker.Observer(tables) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
invalidate();
}
};
db.getInvalidationTracker().addWeakObserver(mObserver);
}
@Override
public int countItems() {
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
mSourceQuery.getArgCount());
sqLiteQuery.copyArgumentsFrom(mSourceQuery);
Cursor cursor = mDb.query(sqLiteQuery);
try {
if (cursor.moveToFirst()) {
return cursor.getInt(0);
}
return 0;
} finally {
cursor.close();
sqLiteQuery.release();
}
}
@Override
public boolean isInvalid() {
mDb.getInvalidationTracker().refreshVersionsSync();
return super.isInvalid();
}
@SuppressWarnings("WeakerAccess")
protected abstract List<T> convertRows(Cursor cursor);
@Nullable
@Override
public List<T> loadRange(int startPosition, int loadCount) {
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
mSourceQuery.getArgCount() + 2);
sqLiteQuery.copyArgumentsFrom(mSourceQuery);
sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
if (mInTransaction) {
mDb.beginTransaction();
Cursor cursor = null;
try {
cursor = mDb.query(sqLiteQuery);
List<T> rows = convertRows(cursor);
mDb.setTransactionSuccessful();
return rows;
} finally {
if (cursor != null) {
cursor.close();
}
mDb.endTransaction();
sqLiteQuery.release();
}
} else {
Cursor cursor = mDb.query(sqLiteQuery);
//noinspection TryFinallyCanBeTryWithResources
try {
return convertRows(cursor);
} finally {
cursor.close();
sqLiteQuery.release();
}
}
}
}