diff --git a/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/AbstractCursor.kt b/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/AbstractCursor.kt deleted file mode 100644 index 8f4ea90e..00000000 --- a/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/AbstractCursor.kt +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright 2024, the wasm-sqlite-open-helper project authors and contributors. Please see the AUTHORS file - * for details. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. - * SPDX-License-Identifier: Apache-2.0 - */ - -package ru.pixnews.wasm.sqlite.open.helper.base - -/* - * Original Copyrights: - * Copyright (C) 2017-2024 requery.io - * Copyright (C) 2005-2012 The Android Open Source Project - * Licensed under the Apache License, Version 2.0 (the "License") - */ - -import android.content.ContentResolver -import android.database.CharArrayBuffer -import android.database.ContentObserver -import android.database.Cursor -import android.database.CursorIndexOutOfBoundsException -import android.database.DataSetObserver -import android.net.Uri -import android.os.Bundle -import java.lang.ref.WeakReference - -/** - * This is an abstract cursor class that handles a lot of the common code - * that all cursors need to deal with and is provided for convenience reasons. - */ -internal abstract class AbstractCursor : Cursor { - protected var pos: Int = -1 - protected var closed: Boolean = false - - @Deprecated("deprecated in AOSP but still used for non-deprecated methods") - private var contentResolver: ContentResolver? = null - private var notifyUri: Uri? = null - private val selfObserverLock = Any() - private var selfObserver: ContentObserver? = null - private var selfObserverRegistered = false - private val dataSetObservable = DataSetObservable() - private val contentObservable = ContentObservable() - - abstract override fun getCount(): Int - - abstract override fun getColumnNames(): Array - - abstract override fun getString(column: Int): String? - abstract override fun getShort(column: Int): Short - abstract override fun getInt(column: Int): Int - abstract override fun getLong(column: Int): Long - abstract override fun getFloat(column: Int): Float - abstract override fun getDouble(column: Int): Double - abstract override fun isNull(column: Int): Boolean - - abstract override fun getType(column: Int): Int - - override fun getBlob(column: Int): ByteArray { - throw UnsupportedOperationException("getBlob is not supported") - } - - override fun getColumnCount(): Int = columnNames.size - - @Deprecated("Deprecated in Java") - override fun deactivate() { - onDeactivateOrClose() - } - - /** @hide - */ - protected open fun onDeactivateOrClose() { - if (selfObserver != null) { - contentResolver!!.unregisterContentObserver(selfObserver!!) - selfObserverRegistered = false - } - dataSetObservable.notifyInvalidated() - } - - @Deprecated("Deprecated in Java") - override fun requery(): Boolean = throw UnsupportedOperationException("Not supported") - - override fun isClosed(): Boolean = closed - - override fun close() { - closed = true - contentObservable.unregisterAll() - onDeactivateOrClose() - } - - override fun copyStringToBuffer(columnIndex: Int, buffer: CharArrayBuffer) { - // Default implementation, uses getString - val result = getString(columnIndex) - if (result != null) { - val data = buffer.data - if (data == null || data.size < result.length) { - buffer.data = result.toCharArray() - } else { - result.toCharArray(data, 0, 0, result.length) - } - buffer.sizeCopied = result.length - } else { - buffer.sizeCopied = 0 - } - } - - override fun getPosition(): Int { - return pos - } - - @Suppress("ReturnCount") - override fun moveToPosition(position: Int): Boolean { - // Make sure position isn't past the end of the cursor - val count = count - if (position >= count) { - pos = count - return false - } - - // Make sure position isn't before the beginning of the cursor - if (position < 0) { - pos = -1 - return false - } - - // Check for no-op moves, and skip the rest of the work for them - if (position == pos) { - return true - } - - val result = onMove(pos, position) - pos = if (!result) { - -1 - } else { - position - } - - return result - } - - /** - * This function is called every time the cursor is successfully scrolled - * to a new position, giving the subclass a chance to update any state it - * may have. If it returns false the move function will also do so and the - * cursor will scroll to the beforeFirst position. - * - * - * This function should be called by methods such as [.moveToPosition], - * so it will typically not be called from outside of the cursor class itself. - * - * - * @param oldPosition The position that we're moving from. - * @param newPosition The position that we're moving to. - * @return True if the move is successful, false otherwise. - */ - @Suppress("FUNCTION_BOOLEAN_PREFIX") - abstract fun onMove(oldPosition: Int, newPosition: Int): Boolean - - override fun move(offset: Int): Boolean { - return moveToPosition(pos + offset) - } - - override fun moveToFirst(): Boolean { - return moveToPosition(0) - } - - override fun moveToLast(): Boolean { - return moveToPosition(count - 1) - } - - override fun moveToNext(): Boolean { - return moveToPosition(pos + 1) - } - - override fun moveToPrevious(): Boolean { - return moveToPosition(pos - 1) - } - - override fun isFirst(): Boolean { - return pos == 0 && count != 0 - } - - override fun isLast(): Boolean { - val cnt = count - return pos == (cnt - 1) && cnt != 0 - } - - override fun isBeforeFirst(): Boolean { - return count == 0 || pos == -1 - } - - override fun isAfterLast(): Boolean { - return count == 0 || pos == count - } - - override fun getColumnIndex(columnName: String): Int { - // Hack according to some mysterious internal bug903852 - val tableColumnName = columnName.substringAfterLast(".") - return columnNames.indexOfFirst { it.equals(tableColumnName, ignoreCase = true) } - } - - override fun getColumnIndexOrThrow(columnName: String): Int { - val index = getColumnIndex(columnName) - require(index >= 0) { "column '$columnName' does not exist" } - return index - } - - override fun getColumnName(columnIndex: Int): String { - return columnNames[columnIndex] - } - - override fun registerContentObserver(observer: ContentObserver) { - contentObservable.registerObserver(observer) - } - - override fun unregisterContentObserver(observer: ContentObserver) { - // cursor will unregister all observers when it close - if (!closed) { - contentObservable.unregisterObserver(observer) - } - } - - override fun registerDataSetObserver(observer: DataSetObserver) { - dataSetObservable.registerObserver(observer) - } - - override fun unregisterDataSetObserver(observer: DataSetObserver) { - dataSetObservable.unregisterObserver(observer) - } - - /** - * Subclasses must call this method when they finish committing updates to notify all - * observers. - * - * @param selfChange value - */ - protected fun onChange(selfChange: Boolean) { - synchronized(selfObserverLock) { - contentObservable.dispatchChange(selfChange, null) - if (notifyUri != null && selfChange) { - contentResolver!!.notifyChange(notifyUri!!, selfObserver) - } - } - } - - /** - * Specifies a content URI to watch for changes. - * - * @param cr The content resolver from the caller's context. - * @param notifyUri The URI to watch for changes. This can be a - * specific row URI, or a base URI for a whole class of content. - */ - override fun setNotificationUri(cr: ContentResolver, notifyUri: Uri) = synchronized(selfObserverLock) { - this.notifyUri = notifyUri - contentResolver = cr - if (selfObserver != null) { - contentResolver!!.unregisterContentObserver(selfObserver!!) - } - SelfContentObserver(this).let { - selfObserver = it - contentResolver!!.registerContentObserver(this.notifyUri!!, true, it) - } - selfObserverRegistered = true - } - - override fun getNotificationUri(): Uri = synchronized(selfObserverLock) { - return notifyUri!! - } - - override fun getWantsAllOnMoveCalls(): Boolean = false - - override fun setExtras(extras: Bundle?) { - throw UnsupportedOperationException("Extras on cursor not supported") - } - - override fun getExtras(): Bundle { - throw UnsupportedOperationException("Extras on cursor not supported") - } - - override fun respond(extras: Bundle): Bundle { - throw UnsupportedOperationException("Extras on cursor not supported") - } - - /** - * This function throws CursorIndexOutOfBoundsException if the cursor position is out of bounds. - * Subclass implementations of the get functions should call this before attempting to - * retrieve data. - * - * @throws CursorIndexOutOfBoundsException - */ - protected open fun checkPosition() { - if (-1 == pos || count == pos) { - throw CursorIndexOutOfBoundsException(pos, count) - } - } - - protected open fun finalize() { - if (selfObserver != null && selfObserverRegistered) { - contentResolver!!.unregisterContentObserver(selfObserver!!) - } - try { - if (!closed) { - close() - } - } catch (ignored: Exception) { - } - } - - /** - * Cursors use this class to track changes others make to their URI. - */ - protected class SelfContentObserver(cursor: AbstractCursor) : ContentObserver(null) { - var cursor: WeakReference = WeakReference(cursor) - - override fun deliverSelfNotifications(): Boolean { - return false - } - - override fun onChange(selfChange: Boolean) { - val cursor = cursor.get() - cursor?.onChange(false) - } - } -} diff --git a/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/AbstractWindowedCursor.kt b/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/AbstractWindowedCursor.kt deleted file mode 100644 index d44ced0a..00000000 --- a/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/AbstractWindowedCursor.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright 2024, the wasm-sqlite-open-helper project authors and contributors. Please see the AUTHORS file - * for details. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. - * SPDX-License-Identifier: Apache-2.0 - */ - -package ru.pixnews.wasm.sqlite.open.helper.base - -/* - * Original Copyrights: - * Copyright (C) 2017-2024 requery.io - * Copyright (C) 2005-2012 The Android Open Source Project - * Licensed under the Apache License, Version 2.0 (the "License") - */ - -import android.database.CharArrayBuffer -import android.database.StaleDataException -import ru.pixnews.wasm.sqlite.open.helper.internal.cursor.CursorWindow -import ru.pixnews.wasm.sqlite.open.helper.internal.cursor.NativeCursorWindow - -/** - * A base class for Cursors that store their data in [android.database.CursorWindow]s. - * - * The cursor owns the cursor window it uses. When the cursor is closed, - * its window is also closed. Likewise, when the window used by the cursor is - * changed, its old window is closed. This policy of strict ownership ensures - * that cursor windows are not leaked. - * - * Subclasses are responsible for filling the cursor window with data during - * [.onMove], allocating a new cursor window if necessary. - * During [.requery], the existing cursor window should be cleared and - * filled with new data. - * - * If the contents of the cursor change or become invalid, the old window must be closed - * (because it is owned by the cursor) and set to null. - * - */ -internal abstract class AbstractWindowedCursor( - private val windowFactory: (name: String?) -> CursorWindow, -) : AbstractCursor() { - /** - * The cursor window owned by this cursor. - */ - open var window: CursorWindow? = null - /** - * Sets a new cursor window for the cursor to use. - * - * - * The cursor takes ownership of the provided cursor window; the cursor window - * will be closed when the cursor is closed or when the cursor adopts a new - * cursor window. - * - * - * If the cursor previously had a cursor window, then it is closed when the - * new cursor window is assigned. - * - * - * @param newWindow The new cursor window, typically a remote cursor window. - */ - set(newWindow) { - if (newWindow !== field) { - val old = field - field = newWindow - old?.close() - } - } - - override fun getBlob(column: Int): ByteArray { - checkPosition() - return requireWindow().getBlob(pos, column) ?: byteArrayOf() - } - - override fun getString(column: Int): String? { - checkPosition() - return requireWindow().getString(pos, column) - } - - /** - * Copies the text of the field at the specified row and column index into - * a [CharArrayBuffer]. - * - * - * The buffer is populated as follows: - * - * * If the buffer is too small for the value to be copied, then it is - * automatically resized. - * * If the field is of type [Cursor.FIELD_TYPE_NULL], then the buffer - * is set to an empty string. - * * If the field is of type [Cursor.FIELD_TYPE_STRING], then the buffer - * is set to the contents of the string. - * * If the field is of type [Cursor.FIELD_TYPE_INTEGER], then the buffer - * is set to a string representation of the integer in decimal, obtained by formatting the - * value with the `printf` family of functions using - * format specifier `%lld`. - * * If the field is of type [Cursor.FIELD_TYPE_FLOAT], then the buffer is - * set to a string representation of the floating-point value in decimal, obtained by - * formatting the value with the `printf` family of functions using - * format specifier `%g`. - * * If the field is of type [Cursor.FIELD_TYPE_BLOB], then a - * [SQLiteException] is thrown. - * - */ - override fun copyStringToBuffer(columnIndex: Int, buffer: CharArrayBuffer) { - val chars = getString(columnIndex)?.toCharArray() ?: charArrayOf() - buffer.data = chars - buffer.sizeCopied = chars.size - } - - override fun getShort(column: Int): Short { - checkPosition() - return requireWindow().getShort(pos, column) - } - - override fun getInt(column: Int): Int { - checkPosition() - return requireWindow().getInt(pos, column) - } - - override fun getLong(column: Int): Long { - checkPosition() - return requireWindow().getLong(pos, column) - } - - override fun getFloat(column: Int): Float { - checkPosition() - return requireWindow().getFloat(pos, column) - } - - override fun getDouble(column: Int): Double { - checkPosition() - return requireWindow().getDouble(pos, column) - } - - override fun isNull(column: Int): Boolean { - return requireWindow().getType(pos, column) == NativeCursorWindow.CursorFieldType.NULL - } - - override fun getType(column: Int): Int { - return requireWindow().getType(pos, column).id - } - - override fun checkPosition() { - super.checkPosition() - requireWindow() - } - - private fun requireWindow(): CursorWindow = window ?: throw StaleDataException( - "Attempting to access a closed CursorWindow." + - "Most probable cause: cursor is deactivated prior to calling this method.", - ) - - /** - * Creates a new window. - * - * @param name The window name. - * @hide - */ - protected fun createWindow(name: String?) { - window = windowFactory(name) - } - - override fun onDeactivateOrClose() { - super.onDeactivateOrClose() - window = null - } -} diff --git a/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/DefaultDatabaseErrorHandler.kt b/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/DefaultDatabaseErrorHandler.kt deleted file mode 100644 index d5430aa5..00000000 --- a/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/base/DefaultDatabaseErrorHandler.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2024, the wasm-sqlite-open-helper project authors and contributors. Please see the AUTHORS file - * for details. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. - * SPDX-License-Identifier: Apache-2.0 - */ - -package ru.pixnews.wasm.sqlite.open.helper.base - -/* - * Original Copyrights: - * Copyright (C) 2017-2024 requery.io - * Copyright (C) 2005-2012 The Android Open Source Project - * Licensed under the Apache License, Version 2.0 (the "License") - */ - -import android.database.sqlite.SQLiteException -import android.util.Log -import android.util.Pair -import ru.pixnews.wasm.sqlite.open.helper.internal.SQLiteDatabase -import java.io.File - -/** - * Default class used to define the actions to take when the database corruption is reported - * by sqlite. - * - * If null is specified for DatabaeErrorHandler param in the above calls, then this class is used - * as the default [DatabaseErrorHandler]. - */ -internal class DefaultDatabaseErrorHandler : DatabaseErrorHandler { - override fun onCorruption(dbObj: SQLiteDatabase<*, *>) { - Log.e(TAG, "Corruption reported by sqlite on database: ${dbObj.path}") - - // is the corruption detected even before database could be 'opened'? - if (!dbObj.isOpen) { - // database files are not even openable. delete this database file. - // NOTE if the database has attached databases, then any of them could be corrupt. - // and not deleting all of them could cause corrupted database file to remain and - // make the application crash on database open operation. To avoid this problem, - // the application should provide its own {@link DatabaseErrorHandler} impl class - // to delete ALL files of the database (including the attached databases). - dbObj.path?.let(::deleteDatabaseFile) - return - } - - var attachedDbs: List>? = null - try { - // Close the database, which will cause subsequent operations to fail. - // before that, get the attached database list first. - try { - attachedDbs = dbObj.attachedDbs - } catch (@Suppress("SwallowedException") e: SQLiteException) { - /* ignore */ - } - try { - dbObj.close() - } catch (@Suppress("SwallowedException") e: SQLiteException) { - /* ignore */ - } - } finally { - // Delete all files of this corrupt database and/or attached databases - if (attachedDbs != null) { - for (attachedDb in attachedDbs) { - deleteDatabaseFile(attachedDb.second) - } - } else { - // attachedDbs = null is possible when the database is so corrupt that even - // "PRAGMA database_list;" also fails. delete the main database file - dbObj.path?.let(::deleteDatabaseFile) - } - } - } - - private fun deleteDatabaseFile(fileName: String) { - if (fileName.equals(":memory:", ignoreCase = true) || fileName.trim { it <= ' ' }.isEmpty()) { - return - } - Log.e(TAG, "deleting the database file: $fileName") - try { - SQLiteDatabase.deleteDatabase(File(fileName)) - } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { - /* print warning and ignore exception */ - Log.w(TAG, "delete failed: ${e.message}") - } - } - - companion object { - private const val TAG = "DefaultDatabaseError" - } -} diff --git a/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/internal/SQLiteCursor.kt b/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/internal/SQLiteCursor.kt index bf4c456f..20c46146 100644 --- a/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/internal/SQLiteCursor.kt +++ b/sqlite-open-helper/src/androidMain/kotlin/ru/pixnews/wasm/sqlite/open/helper/internal/SQLiteCursor.kt @@ -13,10 +13,21 @@ package ru.pixnews.wasm.sqlite.open.helper.internal * Licensed under the Apache License, Version 2.0 (the "License") */ -import ru.pixnews.wasm.sqlite.open.helper.base.AbstractWindowedCursor +import android.content.ContentResolver +import android.database.CharArrayBuffer +import android.database.ContentObserver +import android.database.Cursor +import android.database.CursorIndexOutOfBoundsException +import android.database.DataSetObserver +import android.database.StaleDataException +import android.net.Uri +import android.os.Bundle +import ru.pixnews.wasm.sqlite.open.helper.base.ContentObservable +import ru.pixnews.wasm.sqlite.open.helper.base.DataSetObservable import ru.pixnews.wasm.sqlite.open.helper.common.api.Logger import ru.pixnews.wasm.sqlite.open.helper.internal.cursor.CursorWindow -import kotlin.LazyThreadSafetyMode.NONE +import ru.pixnews.wasm.sqlite.open.helper.internal.cursor.NativeCursorWindow +import java.lang.ref.WeakReference import kotlin.math.max /** @@ -30,63 +41,228 @@ import kotlin.math.max internal class SQLiteCursor( private val query: SQLiteQuery, rootLogger: Logger, -) : AbstractWindowedCursor({ name -> CursorWindow(name, rootLogger) }) { +) : Cursor { private val logger = rootLogger.withTag(TAG) - - /** The names of the columns in the rows */ - private val columns: List = query.columnNames + private var pos: Int = NO_COUNT + private var closed: Boolean = false /** The number of rows in the cursor */ - private var count = NO_COUNT + private var _count = NO_COUNT /** The number of rows that can fit in the cursor window, 0 if unknown */ private var cursorWindowCapacity = 0 /** A mapping of column names to column indices, to speed up lookups */ - private val columnNameMap: Map by lazy(NONE) { - columns.mapIndexed { columnNo, name -> name to columnNo }.toMap() + private val columnNameMap: Map by lazy { + query.columnNames + .mapIndexed { columnNo, name -> name to columnNo } + .toMap() } + @Deprecated("deprecated in AOSP but still used for non-deprecated methods") + private var contentResolver: ContentResolver? = null + private var notifyUri: Uri? = null + private val selfObserverLock = Any() + private var selfObserver: ContentObserver? = null + private var selfObserverRegistered = false + private val dataSetObservable = DataSetObservable() + private val contentObservable = ContentObservable() + /** Used to find out where a cursor was allocated in case it never got released. */ private val closeGuard: CloseGuard = CloseGuard.get() - @Suppress("NO_CORRESPONDING_PROPERTY") - override var window: CursorWindow? - get() = super.window - set(value) { - super.window = value - count = NO_COUNT + /** + * The cursor window owned by this cursor. + */ + var window: CursorWindow? = null + /** + * Sets a new cursor window for the cursor to use. + * + * + * The cursor takes ownership of the provided cursor window; the cursor window + * will be closed when the cursor is closed or when the cursor adopts a new + * cursor window. + * + * + * If the cursor previously had a cursor window, then it is closed when the + * new cursor window is assigned. + * + * + * @param newWindow The new cursor window, typically a remote cursor window. + */ + set(newWindow) { + if (newWindow !== field) { + val old = field + field = newWindow + old?.close() + } + _count = NO_COUNT } - override fun onMove(oldPosition: Int, newPosition: Int): Boolean { - // Make sure the row at newPosition is present in the window - window.let { - if ((it == null) || newPosition < it.startPosition || newPosition >= (it.startPosition + it.numRows)) { - fillWindow(newPosition) - } + override fun getCount(): Int { + if (_count == NO_COUNT) { + fillWindow(0) } + return _count + } - return true + override fun getColumnNames(): Array = columnNameMap.keys.toTypedArray() + + override fun getColumnCount(): Int = columnNameMap.size + + override fun getBlob(column: Int): ByteArray { + checkPosition() + return requireWindow().getBlob(pos, column) ?: byteArrayOf() } - override fun getCount(): Int { - if (count == NO_COUNT) { - fillWindow(0) + override fun getString(column: Int): String? { + checkPosition() + return requireWindow().getString(pos, column) + } + + /** + * Copies the text of the field at the specified row and column index into + * a [CharArrayBuffer]. + * + * + * The buffer is populated as follows: + * + * * If the field is of type [Cursor.FIELD_TYPE_NULL], then the buffer + * is set to an empty string. + * * If the field is of type [Cursor.FIELD_TYPE_STRING], then the buffer + * is set to the contents of the string. + * * If the field is of type [Cursor.FIELD_TYPE_INTEGER], then the buffer + * is set to a string representation of the integer in decimal, obtained by formatting the + * value with the `printf` family of functions using + * format specifier `%lld`. + * * If the field is of type [Cursor.FIELD_TYPE_FLOAT], then the buffer is + * set to a string representation of the floating-point value in decimal, obtained by + * formatting the value with the `printf` family of functions using + * format specifier `%g`. + * * If the field is of type [Cursor.FIELD_TYPE_BLOB], then [SQLiteException] is thrown. + * + */ + override fun copyStringToBuffer(columnIndex: Int, buffer: CharArrayBuffer) { + val chars = getString(columnIndex)?.toCharArray() ?: charArrayOf() + buffer.data = chars + buffer.sizeCopied = chars.size + } + + override fun getShort(column: Int): Short { + checkPosition() + return requireWindow().getShort(pos, column) + } + + override fun getInt(column: Int): Int { + checkPosition() + return requireWindow().getInt(pos, column) + } + + override fun getLong(column: Int): Long { + checkPosition() + return requireWindow().getLong(pos, column) + } + + override fun getFloat(column: Int): Float { + checkPosition() + return requireWindow().getFloat(pos, column) + } + + override fun getDouble(column: Int): Double { + checkPosition() + return requireWindow().getDouble(pos, column) + } + + override fun isNull(column: Int): Boolean { + return requireWindow().getType(pos, column) == NativeCursorWindow.CursorFieldType.NULL + } + + override fun getType(column: Int): Int { + return requireWindow().getType(pos, column).id + } + + override fun getPosition(): Int = pos + + /** + * This function throws CursorIndexOutOfBoundsException if the cursor position is out of bounds. + * Subclass implementations of the get functions should call this before attempting to + * retrieve data. + * + * @throws CursorIndexOutOfBoundsException + */ + private fun checkPosition() { + if (-1 == pos || _count == pos) { + throw CursorIndexOutOfBoundsException(pos, _count) + } + requireWindow() + } + + private fun requireWindow(): CursorWindow = window ?: throw StaleDataException( + "Attempting to access a closed CursorWindow." + + "Most probable cause: cursor is deactivated prior to calling this method.", + ) + + override fun move(offset: Int): Boolean = moveToPosition(pos + offset) + + override fun moveToFirst(): Boolean = moveToPosition(0) + + override fun moveToLast(): Boolean = moveToPosition(count - 1) + + override fun moveToNext(): Boolean = moveToPosition(pos + 1) + + override fun moveToPrevious(): Boolean = moveToPosition(pos - 1) + + override fun isFirst(): Boolean = pos == 0 && count != 0 + + override fun isLast(): Boolean = count.let { cnt -> + pos == (cnt - 1) && cnt != 0 + } + + override fun isBeforeFirst(): Boolean = count == 0 || pos == -1 + + override fun isAfterLast(): Boolean = count == 0 || pos == count + + @Suppress("ReturnCount") + override fun moveToPosition(position: Int): Boolean { + // Make sure position isn't past the end of the cursor + val count = count + if (position >= count) { + pos = count + return false + } + + // Make sure position isn't before the beginning of the cursor + if (position < 0) { + pos = -1 + return false + } + + // Check for no-op moves, and skip the rest of the work for them + if (position == pos) { + return true + } + + window.let { + if ((it == null) || position !in it.startPosition..= 0) { "column '$columnName' does not exist" } + return index + } + override fun getColumnIndex(columnName: String): Int { - // Hack according to bug 903852 + // Hack according to some mysterious internal bug903852 val cleanColumnName = columnName.substringAfterLast(".") return columnNameMap.getOrDefault(cleanColumnName, -1) } - override fun getColumnNames(): Array = columns.toTypedArray() + override fun getColumnName(columnIndex: Int): String = columnNames[columnIndex] - override fun close() { - super.close() - synchronized(this) { - query.close() + override fun registerContentObserver(observer: ContentObserver) { + contentObservable.registerObserver(observer) + } + + override fun unregisterContentObserver(observer: ContentObserver) { + // cursor will unregister all observers when it close + if (!closed) { + contentObservable.unregisterObserver(observer) } } + override fun registerDataSetObserver(observer: DataSetObserver) { + dataSetObservable.registerObserver(observer) + } + + override fun unregisterDataSetObserver(observer: DataSetObserver) { + dataSetObservable.unregisterObserver(observer) + } + + /** + * Specifies a content URI to watch for changes. + * + * @param cr The content resolver from the caller's context. + * @param notifyUri The URI to watch for changes. This can be a + * specific row URI, or a base URI for a whole class of content. + */ + override fun setNotificationUri(cr: ContentResolver, notifyUri: Uri) = synchronized(selfObserverLock) { + this.contentResolver = cr + this.notifyUri = notifyUri + + selfObserver?.let { + cr.unregisterContentObserver(it) + } + SelfContentObserver(this).let { + selfObserver = it + cr.registerContentObserver(notifyUri, true, it) + } + selfObserverRegistered = true + } + + override fun getNotificationUri(): Uri = synchronized(selfObserverLock) { notifyUri!! } + + override fun getWantsAllOnMoveCalls(): Boolean = false + + override fun setExtras(extras: Bundle?) { + throw UnsupportedOperationException("Extras on cursor not supported") + } + + override fun getExtras(): Bundle { + throw UnsupportedOperationException("Extras on cursor not supported") + } + + override fun respond(extras: Bundle): Bundle { + throw UnsupportedOperationException("Extras on cursor not supported") + } + /** * Release the native resources, if they haven't been released yet. */ - override fun finalize() { + protected fun finalize() { try { // if the cursor hasn't been closed yet, close it first if (window != null) { @@ -127,14 +358,73 @@ internal class SQLiteCursor( close() } } finally { - super.finalize() + if (selfObserver != null && selfObserverRegistered) { + contentResolver!!.unregisterContentObserver(selfObserver!!) + } + try { + if (!closed) { + close() + } + } catch (ignored: Exception) { + } } } - companion object { + @Deprecated("Deprecated in Java") + override fun deactivate() { + onDeactivateOrClose() + } + + @Deprecated("Deprecated in Java") + override fun requery(): Boolean = throw UnsupportedOperationException("Not supported") + + override fun isClosed(): Boolean = closed + + override fun close() { + closed = true + contentObservable.unregisterAll() + onDeactivateOrClose() + synchronized(this) { + query.close() + } + } + + private fun onDeactivateOrClose() { + if (selfObserver != null) { + contentResolver!!.unregisterContentObserver(selfObserver!!) + selfObserverRegistered = false + } + dataSetObservable.notifyInvalidated() + window = null + } + + /** + * Subclasses must call this method when they finish committing updates to notify all + * observers. + */ + private fun onChange() { + synchronized(selfObserverLock) { + contentObservable.dispatchChange(false, null) + } + } + + /** + * Cursors use this class to track changes others make to their URI. + */ + private class SelfContentObserver(cursor: SQLiteCursor) : ContentObserver(null) { + var cursor: WeakReference = WeakReference(cursor) + + override fun deliverSelfNotifications(): Boolean = false + + override fun onChange(selfChange: Boolean) { + val cursor = cursor.get() + cursor?.onChange() + } + } + + private companion object { const val TAG: String = "SQLiteCursor" const val NO_COUNT: Int = -1 - fun cursorPickFillWindowStartPosition( cursorPosition: Int, cursorWindowCapacity: Int,