LimitOffsetDataSource.kt

/*
 * 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.
 */
@file:Suppress("DEPRECATION")

package androidx.room.paging

import android.database.Cursor
import androidx.annotation.RestrictTo
import androidx.paging.PositionalDataSource
import androidx.room.InvalidationTracker
import androidx.room.RoomDatabase
import androidx.room.RoomSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import java.util.concurrent.atomic.AtomicBoolean

/**
 * A simple data source implementation that uses Limit & Offset to page the query.
 *
 * This is NOT the most efficient way to do paging on SQLite. It is
 * [recommended](http://www.sqlite.org/cvstrac/wiki?p=ScrollingCursor) to use an indexed
 * ORDER BY statement but that requires a more complex API. This solution is technically equal to
 * receiving a [Cursor] from a large query but avoids the need to manually manage it, and
 * never returns inconsistent data if it is invalidated.
 *
 * This class is used for both Paging2 and Paging3 (via its compat API). When used with Paging3,
 * it does lazy registration for observers to be suitable for initialization on the main thread
 * whereas in Paging2, it will register observer eagerly to obey Paging2's strict Data Source
 * rules. (Paging2 does not let data source to possibly return invalidated data).
 *
 * @property <T> Data type returned by the data source.
 *
 * @hide
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
abstract class LimitOffsetDataSource<T : Any> protected constructor(
    private val db: RoomDatabase,
    private val sourceQuery: RoomSQLiteQuery,
    private val inTransaction: Boolean,
    registerObserverImmediately: Boolean,
    vararg tables: String
) : PositionalDataSource<T>() {
    private val countQuery = "SELECT COUNT(*) FROM ( " + sourceQuery.sql + " )"
    private val limitOffsetQuery = "SELECT * FROM ( " + sourceQuery.sql + " ) LIMIT ? OFFSET ?"
    private val observer: InvalidationTracker.Observer
    private val registeredObserver = AtomicBoolean(false)

    protected constructor(
        db: RoomDatabase,
        query: SupportSQLiteQuery,
        inTransaction: Boolean,
        vararg tables: String
    ) : this(db, RoomSQLiteQuery.copyFrom(query), inTransaction, *tables)

    protected constructor(
        db: RoomDatabase,
        query: SupportSQLiteQuery,
        inTransaction: Boolean,
        registerObserverImmediately: Boolean,
        vararg tables: String
    ) : this(
        db, RoomSQLiteQuery.copyFrom(query), inTransaction, registerObserverImmediately, *tables
    )

    protected constructor(
        db: RoomDatabase,
        query: RoomSQLiteQuery,
        inTransaction: Boolean,
        vararg tables: String
    ) : this(db, query, inTransaction, true, *tables)

    init {
        observer = object : InvalidationTracker.Observer(tables) {
            override fun onInvalidated(tables: Set<String>) {
                invalidate()
            }
        }
        if (registerObserverImmediately) {
            registerObserverIfNecessary()
        }
    }

    private fun registerObserverIfNecessary() {
        if (registeredObserver.compareAndSet(false, true)) {
            db.invalidationTracker.addWeakObserver(observer)
        }
    }

    /**
     * Count number of rows query can return
     *
     * @hide
     */
    fun countItems(): Int {
        registerObserverIfNecessary()
        val sqLiteQuery = RoomSQLiteQuery.acquire(
            countQuery,
            sourceQuery.argCount
        )
        sqLiteQuery.copyArgumentsFrom(sourceQuery)
        val cursor = db.query(sqLiteQuery)
        return try {
            if (cursor.moveToFirst()) {
                cursor.getInt(0)
            } else 0
        } finally {
            cursor.close()
            sqLiteQuery.release()
        }
    }

    override val isInvalid: Boolean
        get() {
        registerObserverIfNecessary()
        db.invalidationTracker.refreshVersionsSync()
        return super.isInvalid
    }

    protected abstract fun convertRows(cursor: Cursor): List<T>

    @Suppress("deprecation")
    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
        registerObserverIfNecessary()
        val onResultCaller: () -> Unit
        db.beginTransaction()
        try {
            val totalCount = countItems()
            if (totalCount != 0) {
                // bound the size requested, based on known count
                val firstLoadPosition = computeInitialLoadPosition(params, totalCount)
                val firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount)
                db.query(getSQLiteQuery(firstLoadPosition, firstLoadSize)).use { cursor ->
                    val rows = convertRows(cursor)
                    db.setTransactionSuccessful()
                    onResultCaller = { callback.onResult(rows, firstLoadPosition, totalCount) }
                }
            } else {
                onResultCaller = { callback.onResult(emptyList(), 0, totalCount) }
            }
        } finally {
            db.endTransaction()
        }
        onResultCaller.invoke()
    }

    override fun loadRange(
        params: LoadRangeParams,
        callback: LoadRangeCallback<T>
    ) {
        callback.onResult(loadRange(params.startPosition, params.loadSize))
    }

    /**
     * Return the rows from startPos to startPos + loadCount
     *
     * @hide
     */
    @Suppress("deprecation")
    fun loadRange(startPosition: Int, loadCount: Int): List<T> {
        val sqLiteQuery = getSQLiteQuery(startPosition, loadCount)
        try {
            if (inTransaction) {
                db.beginTransaction()
                try {
                    db.query(sqLiteQuery).use { cursor ->
                        val rows = convertRows(cursor)
                        db.setTransactionSuccessful()
                        return rows
                    }
                } finally {
                    db.endTransaction()
                }
            } else {
                db.query(sqLiteQuery).use { cursor ->
                    return convertRows(cursor)
                }
            }
        } finally {
            sqLiteQuery.release()
        }
    }

    private fun getSQLiteQuery(startPosition: Int, loadCount: Int): RoomSQLiteQuery {
        val sqLiteQuery = RoomSQLiteQuery.acquire(
            limitOffsetQuery,
            sourceQuery.argCount + 2
        )
        sqLiteQuery.copyArgumentsFrom(sourceQuery)
        sqLiteQuery.bindLong(sqLiteQuery.argCount - 1, loadCount.toLong())
        sqLiteQuery.bindLong(sqLiteQuery.argCount, startPosition.toLong())
        return sqLiteQuery
    }
}