From 03a331175f657d6993de7d4734c9235b1a7a1d64 Mon Sep 17 00:00:00 2001 From: M66B <259573+M66B@users.noreply.github.com> Date: Thu, 2 May 2024 08:50:03 +0200 Subject: [PATCH] Revert "Updated room" This reverts commit 58a63a366de1fce372b918782e258e612293c0ec. --- app/build.gradle | 4 +- .../main/java/androidx/room/AutoCloser.java | 311 +++ app/src/main/java/androidx/room/AutoCloser.kt | 227 --- .../room/AutoClosingRoomOpenHelper.java | 878 +++++++++ .../room/AutoClosingRoomOpenHelper.kt | 570 ------ .../AutoClosingRoomOpenHelperFactory.java | 48 + .../room/AutoClosingRoomOpenHelperFactory.kt | 35 - ...igrationSpec.kt => AutoMigrationSpec.java} | 15 +- app/src/main/java/androidx/room/CopyLock.java | 112 ++ .../main/java/androidx/room/CursorUtil.java | 167 ++ app/src/main/java/androidx/room/DBUtil.java | 243 +++ .../androidx/room/DatabaseConfiguration.java | 629 ++++++ .../androidx/room/DatabaseConfiguration.kt | 680 ------- ...penHelper.kt => DelegatingOpenHelper.java} | 17 +- app/src/main/java/androidx/room/DeleteMe.kt | 17 + ....kt => EntityDeletionOrUpdateAdapter.java} | 87 +- .../androidx/room/EntityInsertionAdapter.java | 251 +++ .../androidx/room/EntityInsertionAdapter.kt | 228 --- .../androidx/room/EntityUpsertionAdapter.kt | 223 --- ...talRoomApi.kt => ExperimentalRoomApi.java} | 19 +- app/src/main/java/androidx/room/FileUtil.java | 71 + .../main/java/androidx/room/FtsTableInfo.java | 220 +++ .../room/InvalidationLiveDataContainer.java | 59 + .../room/InvalidationLiveDataContainer.kt | 55 - .../androidx/room/InvalidationTracker.java | 902 +++++++++ .../java/androidx/room/InvalidationTracker.kt | 839 --------- .../{paging => }/LimitOffsetDataSource.java | 9 +- .../Migration.kt => Migration.java} | 53 +- .../room/MultiInstanceInvalidationClient.java | 196 ++ .../room/MultiInstanceInvalidationClient.kt | 129 -- .../MultiInstanceInvalidationService.java | 137 ++ .../room/MultiInstanceInvalidationService.kt | 122 -- .../room/QueryInterceptorDatabase.java | 312 +++ .../androidx/room/QueryInterceptorDatabase.kt | 145 -- .../room/QueryInterceptorOpenHelper.java | 78 + .../room/QueryInterceptorOpenHelper.kt | 41 - .../QueryInterceptorOpenHelperFactory.java | 50 + .../room/QueryInterceptorOpenHelperFactory.kt | 39 - .../room/QueryInterceptorProgram.java | 82 + .../androidx/room/QueryInterceptorProgram.kt | 63 - .../room/QueryInterceptorStatement.java | 128 ++ .../room/QueryInterceptorStatement.kt | 109 -- app/src/main/java/androidx/room/Room.java | 115 ++ app/src/main/java/androidx/room/Room.kt | 116 -- .../main/java/androidx/room/RoomDatabase.java | 1678 +++++++++++++++++ .../main/java/androidx/room/RoomDatabase.kt | 1547 --------------- .../java/androidx/room/RoomOpenHelper.java | 277 +++ .../main/java/androidx/room/RoomOpenHelper.kt | 244 --- .../java/androidx/room/RoomSQLiteQuery.java | 299 +++ .../java/androidx/room/RoomSQLiteQuery.kt | 233 --- .../androidx/room/RoomTrackingLiveData.java | 173 ++ .../androidx/room/RoomTrackingLiveData.kt | 126 -- .../androidx/room/SQLiteCopyOpenHelper.java | 289 +++ .../androidx/room/SQLiteCopyOpenHelper.kt | 242 --- .../room/SQLiteCopyOpenHelperFactory.java | 64 + .../room/SQLiteCopyOpenHelperFactory.kt | 45 - .../androidx/room/SharedSQLiteStatement.java | 100 + .../androidx/room/SharedSQLiteStatement.kt | 87 - .../main/java/androidx/room/SneakyThrow.java | 47 + .../main/java/androidx/room/StringUtil.java | 118 ++ .../main/java/androidx/room/TableInfo.java | 743 ++++++++ ...onExecutor.kt => TransactionExecutor.java} | 52 +- app/src/main/java/androidx/room/UUIDUtil.java | 64 + app/src/main/java/androidx/room/ViewInfo.java | 97 + .../java/androidx/room/util/CursorUtil.kt | 183 -- .../main/java/androidx/room/util/DBUtil.kt | 213 --- .../main/java/androidx/room/util/FileUtil.kt | 58 - .../java/androidx/room/util/FtsTableInfo.kt | 181 -- .../java/androidx/room/util/RelationUtil.kt | 149 -- .../java/androidx/room/util/StringUtil.kt | 85 - .../main/java/androidx/room/util/TableInfo.kt | 645 ------- .../main/java/androidx/room/util/UUIDUtil.kt | 55 - .../main/java/androidx/room/util/ViewInfo.kt | 82 - app/src/main/java/eu/faircode/email/DB.java | 2 +- 74 files changed, 9103 insertions(+), 7906 deletions(-) create mode 100644 app/src/main/java/androidx/room/AutoCloser.java delete mode 100644 app/src/main/java/androidx/room/AutoCloser.kt create mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java delete mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt create mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java delete mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.kt rename app/src/main/java/androidx/room/{migration/AutoMigrationSpec.kt => AutoMigrationSpec.java} (77%) create mode 100644 app/src/main/java/androidx/room/CopyLock.java create mode 100644 app/src/main/java/androidx/room/CursorUtil.java create mode 100644 app/src/main/java/androidx/room/DBUtil.java create mode 100644 app/src/main/java/androidx/room/DatabaseConfiguration.java delete mode 100644 app/src/main/java/androidx/room/DatabaseConfiguration.kt rename app/src/main/java/androidx/room/{DelegatingOpenHelper.kt => DelegatingOpenHelper.java} (65%) create mode 100644 app/src/main/java/androidx/room/DeleteMe.kt rename app/src/main/java/androidx/room/{EntityDeletionOrUpdateAdapter.kt => EntityDeletionOrUpdateAdapter.java} (53%) create mode 100644 app/src/main/java/androidx/room/EntityInsertionAdapter.java delete mode 100644 app/src/main/java/androidx/room/EntityInsertionAdapter.kt delete mode 100644 app/src/main/java/androidx/room/EntityUpsertionAdapter.kt rename app/src/main/java/androidx/room/{ExperimentalRoomApi.kt => ExperimentalRoomApi.java} (73%) create mode 100644 app/src/main/java/androidx/room/FileUtil.java create mode 100644 app/src/main/java/androidx/room/FtsTableInfo.java create mode 100644 app/src/main/java/androidx/room/InvalidationLiveDataContainer.java delete mode 100644 app/src/main/java/androidx/room/InvalidationLiveDataContainer.kt create mode 100644 app/src/main/java/androidx/room/InvalidationTracker.java delete mode 100644 app/src/main/java/androidx/room/InvalidationTracker.kt rename app/src/main/java/androidx/room/{paging => }/LimitOffsetDataSource.java (98%) rename app/src/main/java/androidx/room/{migration/Migration.kt => Migration.java} (56%) create mode 100644 app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java delete mode 100644 app/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt create mode 100644 app/src/main/java/androidx/room/MultiInstanceInvalidationService.java delete mode 100644 app/src/main/java/androidx/room/MultiInstanceInvalidationService.kt create mode 100644 app/src/main/java/androidx/room/QueryInterceptorDatabase.java delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorDatabase.kt create mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt create mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.kt create mode 100644 app/src/main/java/androidx/room/QueryInterceptorProgram.java delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorProgram.kt create mode 100644 app/src/main/java/androidx/room/QueryInterceptorStatement.java delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorStatement.kt create mode 100644 app/src/main/java/androidx/room/Room.java delete mode 100644 app/src/main/java/androidx/room/Room.kt create mode 100644 app/src/main/java/androidx/room/RoomDatabase.java delete mode 100644 app/src/main/java/androidx/room/RoomDatabase.kt create mode 100644 app/src/main/java/androidx/room/RoomOpenHelper.java delete mode 100644 app/src/main/java/androidx/room/RoomOpenHelper.kt create mode 100644 app/src/main/java/androidx/room/RoomSQLiteQuery.java delete mode 100644 app/src/main/java/androidx/room/RoomSQLiteQuery.kt create mode 100644 app/src/main/java/androidx/room/RoomTrackingLiveData.java delete mode 100644 app/src/main/java/androidx/room/RoomTrackingLiveData.kt create mode 100644 app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java delete mode 100644 app/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt create mode 100644 app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java delete mode 100644 app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.kt create mode 100644 app/src/main/java/androidx/room/SharedSQLiteStatement.java delete mode 100644 app/src/main/java/androidx/room/SharedSQLiteStatement.kt create mode 100644 app/src/main/java/androidx/room/SneakyThrow.java create mode 100644 app/src/main/java/androidx/room/StringUtil.java create mode 100644 app/src/main/java/androidx/room/TableInfo.java rename app/src/main/java/androidx/room/{TransactionExecutor.kt => TransactionExecutor.java} (53%) create mode 100644 app/src/main/java/androidx/room/UUIDUtil.java create mode 100644 app/src/main/java/androidx/room/ViewInfo.java delete mode 100644 app/src/main/java/androidx/room/util/CursorUtil.kt delete mode 100644 app/src/main/java/androidx/room/util/DBUtil.kt delete mode 100644 app/src/main/java/androidx/room/util/FileUtil.kt delete mode 100644 app/src/main/java/androidx/room/util/FtsTableInfo.kt delete mode 100644 app/src/main/java/androidx/room/util/RelationUtil.kt delete mode 100644 app/src/main/java/androidx/room/util/StringUtil.kt delete mode 100644 app/src/main/java/androidx/room/util/TableInfo.kt delete mode 100644 app/src/main/java/androidx/room/util/UUIDUtil.kt delete mode 100644 app/src/main/java/androidx/room/util/ViewInfo.kt diff --git a/app/build.gradle b/app/build.gradle index 21a1ac7cc0..893b6b6e5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -516,7 +516,7 @@ configurations.configureEach { resolutionStrategy.eachDependency { details -> if (details.requested.group == "androidx.room") { //print("Pinning " + details.requested.group + ":" + details.requested.name + "\n") - details.useVersion "2.6.1" + details.useVersion "2.4.3" } else if (details.requested.group == "androidx.lifecycle" && details.requested.name != "lifecycle-extensions") { //print("Pinning " + details.requested.group + ":" + details.requested.name + "\n") @@ -558,7 +558,7 @@ dependencies { def documentfile_version = "1.1.0-alpha01" def lifecycle_version = "2.7.0" // 2.8.0-rc01 def lifecycle_extensions_version = "2.2.0" - def room_version = "2.6.1" // 2.7.0-alpha01 + def room_version = "2.4.3" // 2.5.2/2.6.1/2.7.0-alpha01 def sqlite_version = "2.4.0" // 2.5.0-alpha01 def requery_version = "3.39.2" def paging_version = "2.1.2" // 3.3.0-rc01 diff --git a/app/src/main/java/androidx/room/AutoCloser.java b/app/src/main/java/androidx/room/AutoCloser.java new file mode 100644 index 0000000000..ba157ec69a --- /dev/null +++ b/app/src/main/java/androidx/room/AutoCloser.java @@ -0,0 +1,311 @@ +/* + * Copyright 2020 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 androidx.room; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.arch.core.util.Function; +import androidx.room.util.SneakyThrow; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * AutoCloser is responsible for automatically opening (using + * delegateOpenHelper) and closing (on a timer started when there are no remaining references) a + * SupportSqliteDatabase. + * + * It is important to ensure that the ref count is incremented when using a returned database. + */ +final class AutoCloser { + + @Nullable + private SupportSQLiteOpenHelper mDelegateOpenHelper = null; + + @NonNull + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + // Package private for access from mAutoCloser + @Nullable + Runnable mOnAutoCloseCallback = null; + + // Package private for access from mAutoCloser + @NonNull + final Object mLock = new Object(); + + // Package private for access from mAutoCloser + final long mAutoCloseTimeoutInMs; + + // Package private for access from mExecuteAutoCloser + @NonNull + final Executor mExecutor; + + // Package private for access from mAutoCloser + @GuardedBy("mLock") + int mRefCount = 0; + + // Package private for access from mAutoCloser + @GuardedBy("mLock") + long mLastDecrementRefCountTimeStamp = SystemClock.uptimeMillis(); + + // The unwrapped SupportSqliteDatabase + // Package private for access from mAutoCloser + @GuardedBy("mLock") + @Nullable + SupportSQLiteDatabase mDelegateDatabase; + + private boolean mManuallyClosed = false; + + private final Runnable mExecuteAutoCloser = new Runnable() { + @Override + public void run() { + mExecutor.execute(mAutoCloser); + } + }; + + // Package private for access from mExecuteAutoCloser + @NonNull + final Runnable mAutoCloser = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (SystemClock.uptimeMillis() - mLastDecrementRefCountTimeStamp + < mAutoCloseTimeoutInMs) { + // An increment + decrement beat us to closing the db. We + // will not close the database, and there should be at least + // one more auto-close scheduled. + return; + } + + if (mRefCount != 0) { + // An increment beat us to closing the db. We don't close the + // db, and another closer will be scheduled once the ref + // count is decremented. + return; + } + + if (mOnAutoCloseCallback != null) { + mOnAutoCloseCallback.run(); + } else { + throw new IllegalStateException("mOnAutoCloseCallback is null but it should" + + " have been set before use. Please file a bug " + + "against Room at: https://issuetracker.google" + + ".com/issues/new?component=413107&template=1096568"); + } + + if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) { + try { + mDelegateDatabase.close(); + } catch (IOException e) { + SneakyThrow.reThrow(e); + } + mDelegateDatabase = null; + } + } + } + }; + + + /** + * Construct an AutoCloser. + * + * @param autoCloseTimeoutAmount time for auto close timer + * @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount + * @param autoCloseExecutor the executor on which the auto close operation will happen + */ + AutoCloser(long autoCloseTimeoutAmount, + @NonNull TimeUnit autoCloseTimeUnit, + @NonNull Executor autoCloseExecutor) { + mAutoCloseTimeoutInMs = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount); + mExecutor = autoCloseExecutor; + } + + /** + * Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the + * delegateOpenHelper after construction. + * + * @param delegateOpenHelper the open helper that is used to create + * new SupportSqliteDatabases + */ + public void init(@NonNull SupportSQLiteOpenHelper delegateOpenHelper) { + if (mDelegateOpenHelper != null) { + Log.e(Room.LOG_TAG, "AutoCloser initialized multiple times. Please file a bug against" + + " room at: https://issuetracker.google" + + ".com/issues/new?component=413107&template=1096568"); + return; + } + this.mDelegateOpenHelper = delegateOpenHelper; + } + + /** + * Execute a ref counting function. The function will receive an unwrapped open database and + * this database will stay open until at least after function returns. If there are no more + * references in use for the db once function completes, an auto close operation will be + * scheduled. + */ + @Nullable + public V executeRefCountingFunction(@NonNull Function function) { + try { + SupportSQLiteDatabase db = incrementCountAndEnsureDbIsOpen(); + return function.apply(db); + } finally { + decrementCountAndScheduleClose(); + } + } + + /** + * Confirms that autoCloser is no longer running and confirms that mDelegateDatabase is set + * and open. mDelegateDatabase will not be auto closed until + * decrementRefCountAndScheduleClose is called. decrementRefCountAndScheduleClose must be + * called once for each call to incrementCountAndEnsureDbIsOpen. + * + * If this throws an exception, decrementCountAndScheduleClose must still be called! + * + * @return the *unwrapped* SupportSQLiteDatabase. + */ + @NonNull + public SupportSQLiteDatabase incrementCountAndEnsureDbIsOpen() { + //TODO(rohitsat): avoid synchronized(mLock) when possible. We should be able to avoid it + // when refCount is not hitting zero or if there is no auto close scheduled if we use + // Atomics. + synchronized (mLock) { + // If there is a scheduled autoclose operation, we should remove it from the handler. + mHandler.removeCallbacks(mExecuteAutoCloser); + + mRefCount++; + + if (mManuallyClosed) { + throw new IllegalStateException("Attempting to open already closed database."); + } + + if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) { + return mDelegateDatabase; + } + + // Get the database while holding `mLock` so no other threads try to create it or + // destroy it. + if (mDelegateOpenHelper != null) { + mDelegateDatabase = mDelegateOpenHelper.getWritableDatabase(); + } else { + throw new IllegalStateException("AutoCloser has not been initialized. Please file " + + "a bug against Room at: " + + "https://issuetracker.google.com/issues/new?component=413107&template=1096568"); + } + + return mDelegateDatabase; + } + } + + /** + * Decrements the ref count and schedules a close if there are no other references to the db. + * This must only be called after a corresponding incrementCountAndEnsureDbIsOpen call. + */ + public void decrementCountAndScheduleClose() { + //TODO(rohitsat): avoid synchronized(mLock) when possible + synchronized (mLock) { + if (mRefCount <= 0) { + throw new IllegalStateException("ref count is 0 or lower but we're supposed to " + + "decrement"); + } + + // decrement refCount + mRefCount--; + + // if refcount is zero, schedule close operation + if (mRefCount == 0) { + if (mDelegateDatabase == null) { + // No db to close, this can happen due to exceptions when creating db... + return; + } + mHandler.postDelayed(mExecuteAutoCloser, mAutoCloseTimeoutInMs); + } + } + } + + /** + * Returns the underlying database. This does not ensure that the database is open; the + * caller is responsible for ensuring that the database is open and the ref count is non-zero. + * + * This is primarily meant for use cases where we don't want to open the database (isOpen) or + * we know that the database is already open (KeepAliveCursor). + */ + @Nullable // Since the db might be closed + public SupportSQLiteDatabase getDelegateDatabase() { + synchronized (mLock) { + return mDelegateDatabase; + } + } + + /** + * Close the database if it is still active. + * + * @throws IOException if an exception is encountered when closing the underlying db. + */ + public void closeDatabaseIfOpen() throws IOException { + synchronized (mLock) { + mManuallyClosed = true; + + if (mDelegateDatabase != null) { + mDelegateDatabase.close(); + } + mDelegateDatabase = null; + } + } + + /** + * The auto closer is still active if the database has not been closed. This means that + * whether or not the underlying database is closed, when active we will re-open it on the + * next access. + * + * @return a boolean indicating whether the auto closer is still active + */ + public boolean isActive() { + return !mManuallyClosed; + } + + /** + * Returns the current ref count for this auto closer. This is only visible for testing. + * + * @return current ref count + */ + @VisibleForTesting + public int getRefCountForTest() { + synchronized (mLock) { + return mRefCount; + } + } + + /** + * Sets a callback that will be run every time the database is auto-closed. This callback + * needs to be lightweight since it is run while holding a lock. + * + * @param onAutoClose the callback to run + */ + public void setAutoCloseCallback(Runnable onAutoClose) { + mOnAutoCloseCallback = onAutoClose; + } +} diff --git a/app/src/main/java/androidx/room/AutoCloser.kt b/app/src/main/java/androidx/room/AutoCloser.kt deleted file mode 100644 index 885f035cd0..0000000000 --- a/app/src/main/java/androidx/room/AutoCloser.kt +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2020 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 androidx.room - -import android.os.Handler -import android.os.Looper -import android.os.SystemClock -import androidx.annotation.GuardedBy -import androidx.annotation.VisibleForTesting -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import java.io.IOException -import java.util.concurrent.Executor -import java.util.concurrent.TimeUnit - -/** - * AutoCloser is responsible for automatically opening (using - * delegateOpenHelper) and closing (on a timer started when there are no remaining references) a - * SupportSqliteDatabase. - * - * It is important to ensure that the ref count is incremented when using a returned database. - * - * @param autoCloseTimeoutAmount time for auto close timer - * @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount - * @param autoCloseExecutor the executor on which the auto close operation will happen - */ -internal class AutoCloser( - autoCloseTimeoutAmount: Long, - autoCloseTimeUnit: TimeUnit, - autoCloseExecutor: Executor -) { - lateinit var delegateOpenHelper: SupportSQLiteOpenHelper - private val handler = Handler(Looper.getMainLooper()) - - internal var onAutoCloseCallback: Runnable? = null - - private val lock = Any() - - private var autoCloseTimeoutInMs: Long = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount) - - private val executor: Executor = autoCloseExecutor - - @GuardedBy("lock") - internal var refCount = 0 - - @GuardedBy("lock") - internal var lastDecrementRefCountTimeStamp = SystemClock.uptimeMillis() - - // The unwrapped SupportSqliteDatabase - @GuardedBy("lock") - internal var delegateDatabase: SupportSQLiteDatabase? = null - - private var manuallyClosed = false - - private val executeAutoCloser = Runnable { executor.execute(autoCloser) } - - private val autoCloser = Runnable { - synchronized(lock) { - if (SystemClock.uptimeMillis() - lastDecrementRefCountTimeStamp - < autoCloseTimeoutInMs - ) { - // An increment + decrement beat us to closing the db. We - // will not close the database, and there should be at least - // one more auto-close scheduled. - return@Runnable - } - if (refCount != 0) { - // An increment beat us to closing the db. We don't close the - // db, and another closer will be scheduled once the ref - // count is decremented. - return@Runnable - } - onAutoCloseCallback?.run() ?: error( - "onAutoCloseCallback is null but it should" + - " have been set before use. Please file a bug " + - "against Room at: $autoCloseBug" - ) - - delegateDatabase?.let { - if (it.isOpen) { - it.close() - } - } - delegateDatabase = null - } - } - - /** - * Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the - * delegateOpenHelper after construction. - * - * @param delegateOpenHelper the open helper that is used to create - * new SupportSqliteDatabases - */ - fun init(delegateOpenHelper: SupportSQLiteOpenHelper) { - this.delegateOpenHelper = delegateOpenHelper - } - - /** - * Execute a ref counting function. The function will receive an unwrapped open database and - * this database will stay open until at least after function returns. If there are no more - * references in use for the db once function completes, an auto close operation will be - * scheduled. - */ - fun executeRefCountingFunction(block: (SupportSQLiteDatabase) -> V): V = - try { - block(incrementCountAndEnsureDbIsOpen()) - } finally { - decrementCountAndScheduleClose() - } - - /** - * Confirms that autoCloser is no longer running and confirms that delegateDatabase is set - * and open. delegateDatabase will not be auto closed until - * decrementRefCountAndScheduleClose is called. decrementRefCountAndScheduleClose must be - * called once for each call to incrementCountAndEnsureDbIsOpen. - * - * If this throws an exception, decrementCountAndScheduleClose must still be called! - * - * @return the *unwrapped* SupportSQLiteDatabase. - */ - fun incrementCountAndEnsureDbIsOpen(): SupportSQLiteDatabase { - // TODO(rohitsat): avoid synchronized(lock) when possible. We should be able to avoid it - // when refCount is not hitting zero or if there is no auto close scheduled if we use - // Atomics. - synchronized(lock) { - - // If there is a scheduled autoclose operation, we should remove it from the handler. - handler.removeCallbacks(executeAutoCloser) - refCount++ - check(!manuallyClosed) { "Attempting to open already closed database." } - delegateDatabase?.let { - if (it.isOpen) { - return it - } - } - return delegateOpenHelper.writableDatabase.also { delegateDatabase = it } - } - } - - /** - * Decrements the ref count and schedules a close if there are no other references to the db. - * This must only be called after a corresponding incrementCountAndEnsureDbIsOpen call. - */ - fun decrementCountAndScheduleClose() { - // TODO(rohitsat): avoid synchronized(lock) when possible - synchronized(lock) { - check(refCount > 0) { - "ref count is 0 or lower but we're supposed to decrement" - } - // decrement refCount - refCount-- - - // if refcount is zero, schedule close operation - if (refCount == 0) { - if (delegateDatabase == null) { - // No db to close, this can happen due to exceptions when creating db... - return - } - handler.postDelayed(executeAutoCloser, autoCloseTimeoutInMs) - } - } - } - - /** - * Close the database if it is still active. - * - * @throws IOException if an exception is encountered when closing the underlying db. - */ - @Throws(IOException::class) - fun closeDatabaseIfOpen() { - synchronized(lock) { - manuallyClosed = true - delegateDatabase?.close() - delegateDatabase = null - } - } - - /** - * The auto closer is still active if the database has not been closed. This means that - * whether or not the underlying database is closed, when active we will re-open it on the - * next access. - * - * @return a boolean indicating whether the auto closer is still active - */ - val isActive: Boolean - get() = !manuallyClosed - - /** - * Returns the current ref count for this auto closer. This is only visible for testing. - * - * @return current ref count - */ - @get:VisibleForTesting - internal val refCountForTest: Int - get() { - synchronized(lock) { return refCount } - } - - /** - * Sets a callback that will be run every time the database is auto-closed. This callback - * needs to be lightweight since it is run while holding a lock. - * - * @param onAutoClose the callback to run - */ - fun setAutoCloseCallback(onAutoClose: Runnable) { - onAutoCloseCallback = onAutoClose - } - - companion object { - const val autoCloseBug = "https://issuetracker.google.com/issues/new?component=" + - "413107&template=1096568" - } -} diff --git a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java new file mode 100644 index 0000000000..548720d140 --- /dev/null +++ b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java @@ -0,0 +1,878 @@ +/* + * Copyright (C) 2020 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 androidx.room; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.CharArrayBuffer; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.database.SQLException; +import android.database.sqlite.SQLiteTransactionListener; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.arch.core.util.Function; +import androidx.room.util.SneakyThrow; +import androidx.sqlite.db.SupportSQLiteCompat; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; +import androidx.sqlite.db.SupportSQLiteQuery; +import androidx.sqlite.db.SupportSQLiteStatement; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * A SupportSQLiteOpenHelper that has autoclose enabled for database connections. + */ +final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper { + @NonNull + private final SupportSQLiteOpenHelper mDelegateOpenHelper; + + @NonNull + private final AutoClosingSupportSQLiteDatabase mAutoClosingDb; + + @NonNull + private final AutoCloser mAutoCloser; + + AutoClosingRoomOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper, + @NonNull AutoCloser autoCloser) { + mDelegateOpenHelper = supportSQLiteOpenHelper; + mAutoCloser = autoCloser; + autoCloser.init(mDelegateOpenHelper); + mAutoClosingDb = new AutoClosingSupportSQLiteDatabase(mAutoCloser); + } + + @Nullable + @Override + public String getDatabaseName() { + return mDelegateOpenHelper.getDatabaseName(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + public void setWriteAheadLoggingEnabled(boolean enabled) { + mDelegateOpenHelper.setWriteAheadLoggingEnabled(enabled); + } + + @NonNull + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public SupportSQLiteDatabase getWritableDatabase() { + // Note we don't differentiate between writable db and readable db + // We try to open the db so the open callbacks run + mAutoClosingDb.pokeOpen(); + return mAutoClosingDb; + } + + @NonNull + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public SupportSQLiteDatabase getReadableDatabase() { + // Note we don't differentiate between writable db and readable db + // We try to open the db so the open callbacks run + mAutoClosingDb.pokeOpen(); + return mAutoClosingDb; + } + + @Override + public void close() { + try { + mAutoClosingDb.close(); + } catch (IOException e) { + SneakyThrow.reThrow(e); + } + } + + /** + * package protected to pass it to invalidation tracker... + */ + @NonNull + AutoCloser getAutoCloser() { + return this.mAutoCloser; + } + + @NonNull + SupportSQLiteDatabase getAutoClosingDb() { + return this.mAutoClosingDb; + } + + @Override + @NonNull + public SupportSQLiteOpenHelper getDelegate() { + return mDelegateOpenHelper; + } + + /** + * SupportSQLiteDatabase that also keeps refcounts and autocloses the database + */ + static final class AutoClosingSupportSQLiteDatabase implements SupportSQLiteDatabase { + @NonNull + private final AutoCloser mAutoCloser; + + AutoClosingSupportSQLiteDatabase(@NonNull AutoCloser autoCloser) { + mAutoCloser = autoCloser; + } + + void pokeOpen() { + mAutoCloser.executeRefCountingFunction(db -> null); + } + + @Override + public SupportSQLiteStatement compileStatement(String sql) { + return new AutoClosingSupportSqliteStatement(sql, mAutoCloser); + } + + @Override + public void beginTransaction() { + // We assume that after every successful beginTransaction() call there *must* be a + // endTransaction() call. + SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen(); + try { + db.beginTransaction(); + } catch (Throwable t) { + // Note: we only want to decrement the ref count if the beginTransaction call + // fails since there won't be a corresponding endTransaction call. + mAutoCloser.decrementCountAndScheduleClose(); + throw t; + } + } + + @Override + public void beginTransactionNonExclusive() { + // We assume that after every successful beginTransaction() call there *must* be a + // endTransaction() call. + SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen(); + try { + db.beginTransactionNonExclusive(); + } catch (Throwable t) { + // Note: we only want to decrement the ref count if the beginTransaction call + // fails since there won't be a corresponding endTransaction call. + mAutoCloser.decrementCountAndScheduleClose(); + throw t; + } + } + + @Override + public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { + // We assume that after every successful beginTransaction() call there *must* be a + // endTransaction() call. + SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen(); + try { + db.beginTransactionWithListener(transactionListener); + } catch (Throwable t) { + // Note: we only want to decrement the ref count if the beginTransaction call + // fails since there won't be a corresponding endTransaction call. + mAutoCloser.decrementCountAndScheduleClose(); + throw t; + } + } + + @Override + public void beginTransactionWithListenerNonExclusive( + SQLiteTransactionListener transactionListener) { + // We assume that after every successful beginTransaction() call there *will* always + // be a corresponding endTransaction() call. Without a corresponding + // endTransactionCall we will never close the db. + SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen(); + try { + db.beginTransactionWithListenerNonExclusive(transactionListener); + } catch (Throwable t) { + // Note: we only want to decrement the ref count if the beginTransaction call + // fails since there won't be a corresponding endTransaction call. + mAutoCloser.decrementCountAndScheduleClose(); + throw t; + } + } + + @Override + public void endTransaction() { + if (mAutoCloser.getDelegateDatabase() == null) { + // This should never happen. + throw new IllegalStateException("End transaction called but delegateDb is null"); + } + + try { + mAutoCloser.getDelegateDatabase().endTransaction(); + } finally { + mAutoCloser.decrementCountAndScheduleClose(); + } + } + + @Override + public void setTransactionSuccessful() { + SupportSQLiteDatabase delegate = mAutoCloser.getDelegateDatabase(); + + if (delegate == null) { + // This should never happen. + throw new IllegalStateException("setTransactionSuccessful called but delegateDb " + + "is null"); + } + + delegate.setTransactionSuccessful(); + } + + @Override + public boolean inTransaction() { + if (mAutoCloser.getDelegateDatabase() == null) { + return false; + } + return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction); + } + + @Override + public boolean isDbLockedByCurrentThread() { + if (mAutoCloser.getDelegateDatabase() == null) { + return false; + } + + return mAutoCloser.executeRefCountingFunction( + SupportSQLiteDatabase::isDbLockedByCurrentThread); + } + + @Override + public boolean yieldIfContendedSafely() { + return mAutoCloser.executeRefCountingFunction( + SupportSQLiteDatabase::yieldIfContendedSafely); + } + + @Override + public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { + return mAutoCloser.executeRefCountingFunction( + SupportSQLiteDatabase::yieldIfContendedSafely); + + } + + @Override + public int getVersion() { + return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getVersion); + } + + @Override + public void setVersion(int version) { + mAutoCloser.executeRefCountingFunction(db -> { + db.setVersion(version); + return null; + }); + } + + @Override + public long getMaximumSize() { + return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getMaximumSize); + } + + @Override + public long setMaximumSize(long numBytes) { + return mAutoCloser.executeRefCountingFunction(db -> db.setMaximumSize(numBytes)); + } + + @Override + public long getPageSize() { + return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPageSize); + } + + @Override + public void setPageSize(long numBytes) { + mAutoCloser.executeRefCountingFunction(db -> { + db.setPageSize(numBytes); + return null; + }); + } + + @Override + public Cursor query(String query) { + Cursor result; + try { + SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen(); + result = db.query(query); + } catch (Throwable throwable) { + mAutoCloser.decrementCountAndScheduleClose(); + throw throwable; + } + + return new KeepAliveCursor(result, mAutoCloser); + } + + @Override + public Cursor query(String query, Object[] bindArgs) { + Cursor result; + try { + SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen(); + result = db.query(query, bindArgs); + } catch (Throwable throwable) { + mAutoCloser.decrementCountAndScheduleClose(); + throw throwable; + } + + return new KeepAliveCursor(result, mAutoCloser); + } + + @Override + public Cursor query(SupportSQLiteQuery query) { + + Cursor result; + try { + SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen(); + result = db.query(query); + } catch (Throwable throwable) { + mAutoCloser.decrementCountAndScheduleClose(); + throw throwable; + } + + return new KeepAliveCursor(result, mAutoCloser); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) { + Cursor result; + try { + SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen(); + result = db.query(query, cancellationSignal); + } catch (Throwable throwable) { + mAutoCloser.decrementCountAndScheduleClose(); + throw throwable; + } + + return new KeepAliveCursor(result, mAutoCloser); + } + + @Override + public long insert(String table, int conflictAlgorithm, ContentValues values) + throws SQLException { + return mAutoCloser.executeRefCountingFunction(db -> db.insert(table, conflictAlgorithm, + values)); + } + + @Override + public int delete(String table, String whereClause, Object[] whereArgs) { + return mAutoCloser.executeRefCountingFunction( + db -> db.delete(table, whereClause, whereArgs)); + } + + @Override + public int update(String table, int conflictAlgorithm, ContentValues values, + String whereClause, Object[] whereArgs) { + return mAutoCloser.executeRefCountingFunction(db -> db.update(table, conflictAlgorithm, + values, whereClause, whereArgs)); + } + + @Override + public void execSQL(String sql) throws SQLException { + mAutoCloser.executeRefCountingFunction(db -> { + db.execSQL(sql); + return null; + }); + } + + @Override + public void execSQL(String sql, Object[] bindArgs) throws SQLException { + mAutoCloser.executeRefCountingFunction(db -> { + db.execSQL(sql, bindArgs); + return null; + }); + } + + @Override + public boolean isReadOnly() { + return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::isReadOnly); + } + + @Override + public boolean isOpen() { + // Get the db without incrementing the reference cause we don't want to open + // the db for an isOpen call. + SupportSQLiteDatabase localDelegate = mAutoCloser.getDelegateDatabase(); + + if (localDelegate == null) { + return false; + } + return localDelegate.isOpen(); + } + + @Override + public boolean needUpgrade(int newVersion) { + return mAutoCloser.executeRefCountingFunction(db -> db.needUpgrade(newVersion)); + } + + @Override + public String getPath() { + return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPath); + } + + @Override + public void setLocale(Locale locale) { + mAutoCloser.executeRefCountingFunction(db -> { + db.setLocale(locale); + return null; + }); + } + + @Override + public void setMaxSqlCacheSize(int cacheSize) { + mAutoCloser.executeRefCountingFunction(db -> { + db.setMaxSqlCacheSize(cacheSize); + return null; + }); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void setForeignKeyConstraintsEnabled(boolean enable) { + mAutoCloser.executeRefCountingFunction(db -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + db.setForeignKeyConstraintsEnabled(enable); + } + return null; + }); + } + + @Override + public boolean enableWriteAheadLogging() { + throw new UnsupportedOperationException("Enable/disable write ahead logging on the " + + "OpenHelper instead of on the database directly."); + } + + @Override + public void disableWriteAheadLogging() { + throw new UnsupportedOperationException("Enable/disable write ahead logging on the " + + "OpenHelper instead of on the database directly."); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public boolean isWriteAheadLoggingEnabled() { + return mAutoCloser.executeRefCountingFunction(db -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return db.isWriteAheadLoggingEnabled(); + } + return false; + }); + } + + @Override + public List> getAttachedDbs() { + return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getAttachedDbs); + } + + @Override + public boolean isDatabaseIntegrityOk() { + return mAutoCloser.executeRefCountingFunction( + SupportSQLiteDatabase::isDatabaseIntegrityOk); + } + + @Override + public void close() throws IOException { + mAutoCloser.closeDatabaseIfOpen(); + } + + @Override + public boolean isExecPerConnectionSQLSupported() { + return false; + } + + @Override + public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs) { + } + } + + /** + * We need to keep the db alive until the cursor is closed, so we can't decrement our + * reference count until the cursor is closed. The underlying database will not close until + * this cursor is closed. + */ + private static final class KeepAliveCursor implements Cursor { + private final Cursor mDelegate; + private final AutoCloser mAutoCloser; + + KeepAliveCursor(Cursor delegate, AutoCloser autoCloser) { + mDelegate = delegate; + mAutoCloser = autoCloser; + } + + // close is the only important/changed method here: + @Override + public void close() { + mDelegate.close(); + mAutoCloser.decrementCountAndScheduleClose(); + } + + @Override + public boolean isClosed() { + return mDelegate.isClosed(); + } + + + @Override + public int getCount() { + return mDelegate.getCount(); + } + + @Override + public int getPosition() { + return mDelegate.getPosition(); + } + + @Override + public boolean move(int offset) { + return mDelegate.move(offset); + } + + @Override + public boolean moveToPosition(int position) { + return mDelegate.moveToPosition(position); + } + + @Override + public boolean moveToFirst() { + return mDelegate.moveToFirst(); + } + + @Override + public boolean moveToLast() { + return mDelegate.moveToLast(); + } + + @Override + public boolean moveToNext() { + return mDelegate.moveToNext(); + } + + @Override + public boolean moveToPrevious() { + return mDelegate.moveToPrevious(); + } + + @Override + public boolean isFirst() { + return mDelegate.isFirst(); + } + + @Override + public boolean isLast() { + return mDelegate.isLast(); + } + + @Override + public boolean isBeforeFirst() { + return mDelegate.isBeforeFirst(); + } + + @Override + public boolean isAfterLast() { + return mDelegate.isAfterLast(); + } + + @Override + public int getColumnIndex(String columnName) { + return mDelegate.getColumnIndex(columnName); + } + + @Override + public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { + return mDelegate.getColumnIndexOrThrow(columnName); + } + + @Override + public String getColumnName(int columnIndex) { + return mDelegate.getColumnName(columnIndex); + } + + @Override + public String[] getColumnNames() { + return mDelegate.getColumnNames(); + } + + @Override + public int getColumnCount() { + return mDelegate.getColumnCount(); + } + + @Override + public byte[] getBlob(int columnIndex) { + return mDelegate.getBlob(columnIndex); + } + + @Override + public String getString(int columnIndex) { + return mDelegate.getString(columnIndex); + } + + @Override + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + mDelegate.copyStringToBuffer(columnIndex, buffer); + } + + @Override + public short getShort(int columnIndex) { + return mDelegate.getShort(columnIndex); + } + + @Override + public int getInt(int columnIndex) { + return mDelegate.getInt(columnIndex); + } + + @Override + public long getLong(int columnIndex) { + return mDelegate.getLong(columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + return mDelegate.getFloat(columnIndex); + } + + @Override + public double getDouble(int columnIndex) { + return mDelegate.getDouble(columnIndex); + } + + @Override + public int getType(int columnIndex) { + return mDelegate.getType(columnIndex); + } + + @Override + public boolean isNull(int columnIndex) { + return mDelegate.isNull(columnIndex); + } + + /** + * @deprecated see Cursor.deactivate + */ + @Override + @Deprecated + public void deactivate() { + mDelegate.deactivate(); + } + + /** + * @deprecated see Cursor.requery + */ + @Override + @Deprecated + public boolean requery() { + return mDelegate.requery(); + } + + @Override + public void registerContentObserver(ContentObserver observer) { + mDelegate.registerContentObserver(observer); + } + + @Override + public void unregisterContentObserver(ContentObserver observer) { + mDelegate.unregisterContentObserver(observer); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mDelegate.registerDataSetObserver(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mDelegate.unregisterDataSetObserver(observer); + } + + @Override + public void setNotificationUri(ContentResolver cr, Uri uri) { + mDelegate.setNotificationUri(cr, uri); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + @Override + public void setNotificationUris(@NonNull ContentResolver cr, + @NonNull List uris) { + SupportSQLiteCompat.Api29Impl.setNotificationUris(mDelegate, cr, uris); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Override + public Uri getNotificationUri() { + return SupportSQLiteCompat.Api19Impl.getNotificationUri(mDelegate); + } + + @RequiresApi(api = Build.VERSION_CODES.Q) + @Nullable + @Override + public List getNotificationUris() { + return SupportSQLiteCompat.Api29Impl.getNotificationUris(mDelegate); + } + + @Override + public boolean getWantsAllOnMoveCalls() { + return mDelegate.getWantsAllOnMoveCalls(); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void setExtras(Bundle extras) { + SupportSQLiteCompat.Api23Impl.setExtras(mDelegate, extras); + } + + @Override + public Bundle getExtras() { + return mDelegate.getExtras(); + } + + @Override + public Bundle respond(Bundle extras) { + return mDelegate.respond(extras); + } + } + + /** + * We can't close our db if the SupportSqliteStatement is open. + * + * Each of these that are created need to be registered with RefCounter. + * + * On auto-close, RefCounter needs to close each of these before closing the db that these + * were constructed from. + * + * Each of the methods here need to get + */ + //TODO(rohitsat) cache the prepared statement... I'm not sure what the performance implications + // are for the way it's done here, but caching the prepared statement would definitely be more + // complicated since we need to invalidate any of the PreparedStatements that were created + // with this db + private static class AutoClosingSupportSqliteStatement implements SupportSQLiteStatement { + private final String mSql; + private final ArrayList mBinds = new ArrayList<>(); + private final AutoCloser mAutoCloser; + + AutoClosingSupportSqliteStatement( + String sql, AutoCloser autoCloser) { + mSql = sql; + mAutoCloser = autoCloser; + } + + private T executeSqliteStatementWithRefCount(Function func) { + return mAutoCloser.executeRefCountingFunction( + db -> { + SupportSQLiteStatement statement = db.compileStatement(mSql); + doBinds(statement); + return func.apply(statement); + } + ); + } + + private void doBinds(SupportSQLiteStatement supportSQLiteStatement) { + // Replay the binds + for (int i = 0; i < mBinds.size(); i++) { + int bindIndex = i + 1; // Bind indices are 1 based so we start at 1 not 0 + Object bind = mBinds.get(i); + if (bind == null) { + supportSQLiteStatement.bindNull(bindIndex); + } else if (bind instanceof Long) { + supportSQLiteStatement.bindLong(bindIndex, (Long) bind); + } else if (bind instanceof Double) { + supportSQLiteStatement.bindDouble(bindIndex, (Double) bind); + } else if (bind instanceof String) { + supportSQLiteStatement.bindString(bindIndex, (String) bind); + } else if (bind instanceof byte[]) { + supportSQLiteStatement.bindBlob(bindIndex, (byte[]) bind); + } + } + } + + private void saveBinds(int bindIndex, Object value) { + int index = bindIndex - 1; + if (index >= mBinds.size()) { + // Add null entries to the list until we have the desired # of indices + for (int i = mBinds.size(); i <= index; i++) { + mBinds.add(null); + } + } + mBinds.set(index, value); + } + + @Override + public void close() throws IOException { + // Nothing to do here since we re-compile the statement each time. + } + + @Override + public void execute() { + executeSqliteStatementWithRefCount(statement -> { + statement.execute(); + return null; + }); + } + + @Override + public int executeUpdateDelete() { + return executeSqliteStatementWithRefCount(SupportSQLiteStatement::executeUpdateDelete); + } + + @Override + public long executeInsert() { + return executeSqliteStatementWithRefCount(SupportSQLiteStatement::executeInsert); + } + + @Override + public long simpleQueryForLong() { + return executeSqliteStatementWithRefCount(SupportSQLiteStatement::simpleQueryForLong); + } + + @Override + public String simpleQueryForString() { + return executeSqliteStatementWithRefCount(SupportSQLiteStatement::simpleQueryForString); + } + + @Override + public void bindNull(int index) { + saveBinds(index, null); + } + + @Override + public void bindLong(int index, long value) { + saveBinds(index, value); + } + + @Override + public void bindDouble(int index, double value) { + saveBinds(index, value); + } + + @Override + public void bindString(int index, String value) { + saveBinds(index, value); + } + + @Override + public void bindBlob(int index, byte[] value) { + saveBinds(index, value); + } + + @Override + public void clearBindings() { + mBinds.clear(); + } + } +} diff --git a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt deleted file mode 100644 index 387fd5c2aa..0000000000 --- a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt +++ /dev/null @@ -1,570 +0,0 @@ -/* - * Copyright (C) 2020 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 androidx.room - -import android.content.ContentResolver -import android.content.ContentValues -import android.database.Cursor -import android.database.SQLException -import android.database.sqlite.SQLiteTransactionListener -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.CancellationSignal -import android.util.Pair -import androidx.annotation.RequiresApi -import androidx.sqlite.db.SupportSQLiteCompat -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import androidx.sqlite.db.SupportSQLiteQuery -import androidx.sqlite.db.SupportSQLiteStatement -import java.io.IOException -import java.util.Locale - -/** - * A SupportSQLiteOpenHelper that has auto close enabled for database connections. - */ -internal class AutoClosingRoomOpenHelper( - override val delegate: SupportSQLiteOpenHelper, - @JvmField - internal val autoCloser: AutoCloser -) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper { - private val autoClosingDb: AutoClosingSupportSQLiteDatabase - - init { - autoCloser.init(delegate) - autoClosingDb = AutoClosingSupportSQLiteDatabase( - autoCloser - ) - } - - @get:RequiresApi(api = Build.VERSION_CODES.N) - override val writableDatabase: SupportSQLiteDatabase - get() { - autoClosingDb.pokeOpen() - return autoClosingDb - } - - @get:RequiresApi(api = Build.VERSION_CODES.N) - override val readableDatabase: SupportSQLiteDatabase - get() { - // Note we don't differentiate between writable db and readable db - // We try to open the db so the open callbacks run - autoClosingDb.pokeOpen() - return autoClosingDb - } - - override fun close() { - autoClosingDb.close() - } - - /** - * SupportSQLiteDatabase that also keeps refcounts and autocloses the database - */ - internal class AutoClosingSupportSQLiteDatabase( - private val autoCloser: AutoCloser - ) : SupportSQLiteDatabase { - fun pokeOpen() { - autoCloser.executeRefCountingFunction { null } - } - - override fun compileStatement(sql: String): SupportSQLiteStatement { - return AutoClosingSupportSqliteStatement(sql, autoCloser) - } - - override fun beginTransaction() { - // We assume that after every successful beginTransaction() call there *must* be a - // endTransaction() call. - val db = autoCloser.incrementCountAndEnsureDbIsOpen() - try { - db.beginTransaction() - } catch (t: Throwable) { - // Note: we only want to decrement the ref count if the beginTransaction call - // fails since there won't be a corresponding endTransaction call. - autoCloser.decrementCountAndScheduleClose() - throw t - } - } - - override fun beginTransactionNonExclusive() { - // We assume that after every successful beginTransaction() call there *must* be a - // endTransaction() call. - val db = autoCloser.incrementCountAndEnsureDbIsOpen() - try { - db.beginTransactionNonExclusive() - } catch (t: Throwable) { - // Note: we only want to decrement the ref count if the beginTransaction call - // fails since there won't be a corresponding endTransaction call. - autoCloser.decrementCountAndScheduleClose() - throw t - } - } - - override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) { - // We assume that after every successful beginTransaction() call there *must* be a - // endTransaction() call. - val db = autoCloser.incrementCountAndEnsureDbIsOpen() - try { - db.beginTransactionWithListener(transactionListener) - } catch (t: Throwable) { - // Note: we only want to decrement the ref count if the beginTransaction call - // fails since there won't be a corresponding endTransaction call. - autoCloser.decrementCountAndScheduleClose() - throw t - } - } - - override fun beginTransactionWithListenerNonExclusive( - transactionListener: SQLiteTransactionListener - ) { - // We assume that after every successful beginTransaction() call there *will* always - // be a corresponding endTransaction() call. Without a corresponding - // endTransactionCall we will never close the db. - val db = autoCloser.incrementCountAndEnsureDbIsOpen() - try { - db.beginTransactionWithListenerNonExclusive(transactionListener) - } catch (t: Throwable) { - // Note: we only want to decrement the ref count if the beginTransaction call - // fails since there won't be a corresponding endTransaction call. - autoCloser.decrementCountAndScheduleClose() - throw t - } - } - - override fun endTransaction() { - checkNotNull(autoCloser.delegateDatabase) { - "End transaction called but delegateDb is null" - } - try { - autoCloser.delegateDatabase!!.endTransaction() - } finally { - autoCloser.decrementCountAndScheduleClose() - } - } - - override fun setTransactionSuccessful() { - autoCloser.delegateDatabase?.setTransactionSuccessful() ?: error( - "setTransactionSuccessful called but delegateDb is null" - ) - } - - override fun inTransaction(): Boolean { - return if (autoCloser.delegateDatabase == null) { - false - } else { - autoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction) - } - } - - override val isDbLockedByCurrentThread: Boolean - get() = if (autoCloser.delegateDatabase == null) { - false - } else { - autoCloser.executeRefCountingFunction( - SupportSQLiteDatabase::isDbLockedByCurrentThread - ) - } - - override fun yieldIfContendedSafely(): Boolean { - return autoCloser.executeRefCountingFunction( - SupportSQLiteDatabase::yieldIfContendedSafely - ) - } - - override fun yieldIfContendedSafely(sleepAfterYieldDelayMillis: Long): Boolean { - return autoCloser.executeRefCountingFunction( - SupportSQLiteDatabase::yieldIfContendedSafely - ) - } - - override var version: Int - get() = autoCloser.executeRefCountingFunction( - SupportSQLiteDatabase::version - ) - set(version) { - autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.version = version - null - } - } - - override val maximumSize: Long - get() = autoCloser.executeRefCountingFunction( - SupportSQLiteDatabase::maximumSize - ) - - override fun setMaximumSize(numBytes: Long): Long { - return autoCloser.executeRefCountingFunction { - db: SupportSQLiteDatabase -> db.setMaximumSize(numBytes) - } - } - - override var pageSize: Long - get() = autoCloser.executeRefCountingFunction(SupportSQLiteDatabase::pageSize) - set(numBytes) { - autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.pageSize = numBytes - null - } - } - - override fun query(query: String): Cursor { - val result = try { - autoCloser.incrementCountAndEnsureDbIsOpen().query(query) - } catch (throwable: Throwable) { - autoCloser.decrementCountAndScheduleClose() - throw throwable - } - return KeepAliveCursor(result, autoCloser) - } - - override fun query(query: String, bindArgs: Array): Cursor { - val result = try { - autoCloser.incrementCountAndEnsureDbIsOpen().query(query, bindArgs) - } catch (throwable: Throwable) { - autoCloser.decrementCountAndScheduleClose() - throw throwable - } - return KeepAliveCursor(result, autoCloser) - } - - override fun query(query: SupportSQLiteQuery): Cursor { - val result = try { - autoCloser.incrementCountAndEnsureDbIsOpen().query(query) - } catch (throwable: Throwable) { - autoCloser.decrementCountAndScheduleClose() - throw throwable - } - return KeepAliveCursor(result, autoCloser) - } - - @RequiresApi(api = Build.VERSION_CODES.N) - override fun query( - query: SupportSQLiteQuery, - cancellationSignal: CancellationSignal? - ): Cursor { - val result = try { - autoCloser.incrementCountAndEnsureDbIsOpen().query(query, cancellationSignal) - } catch (throwable: Throwable) { - autoCloser.decrementCountAndScheduleClose() - throw throwable - } - return KeepAliveCursor(result, autoCloser) - } - - @Throws(SQLException::class) - override fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long { - return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.insert( - table, conflictAlgorithm, - values - ) - } - } - - override fun delete(table: String, whereClause: String?, whereArgs: Array?): Int { - return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.delete( - table, - whereClause, - whereArgs - ) - } - } - - override fun update( - table: String, - conflictAlgorithm: Int, - values: ContentValues, - whereClause: String?, - whereArgs: Array? - ): Int { - return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.update( - table, conflictAlgorithm, - values, whereClause, whereArgs - ) - } - } - - @Throws(SQLException::class) - override fun execSQL(sql: String) { - autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.execSQL(sql) - null - } - } - - @Throws(SQLException::class) - override fun execSQL(sql: String, bindArgs: Array) { - autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.execSQL(sql, bindArgs) - null - } - } - - override val isReadOnly: Boolean - get() = autoCloser.executeRefCountingFunction { obj: SupportSQLiteDatabase -> - obj.isReadOnly - } - - override val isOpen: Boolean - get() { - // Get the db without incrementing the reference cause we don't want to open - // the db for an isOpen call. - val localDelegate = autoCloser.delegateDatabase ?: return false - return localDelegate.isOpen - } - - override fun needUpgrade(newVersion: Int): Boolean { - return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.needUpgrade( - newVersion - ) - } - } - - override val path: String? - get() = autoCloser.executeRefCountingFunction { obj: SupportSQLiteDatabase -> - obj.path - } - - override fun setLocale(locale: Locale) { - autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.setLocale(locale) - null - } - } - - override fun setMaxSqlCacheSize(cacheSize: Int) { - autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - db.setMaxSqlCacheSize(cacheSize) - null - } - } - - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) - override fun setForeignKeyConstraintsEnabled(enabled: Boolean) { - autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - db.setForeignKeyConstraintsEnabled(enabled) - } - null - } - } - - override fun enableWriteAheadLogging(): Boolean { - throw UnsupportedOperationException( - "Enable/disable write ahead logging on the " + - "OpenHelper instead of on the database directly." - ) - } - - override fun disableWriteAheadLogging() { - throw UnsupportedOperationException( - "Enable/disable write ahead logging on the " + - "OpenHelper instead of on the database directly." - ) - } - - @get:RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) - override val isWriteAheadLoggingEnabled: Boolean - get() = autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return@executeRefCountingFunction db.isWriteAheadLoggingEnabled - } - false - } - - override val attachedDbs: List>? - get() = autoCloser.executeRefCountingFunction { - obj: SupportSQLiteDatabase -> obj.attachedDbs - } - - override val isDatabaseIntegrityOk: Boolean - get() = autoCloser.executeRefCountingFunction { - obj: SupportSQLiteDatabase -> obj.isDatabaseIntegrityOk - } - - @Throws(IOException::class) - override fun close() { - autoCloser.closeDatabaseIfOpen() - } - } - - /** - * We need to keep the db alive until the cursor is closed, so we can't decrement our - * reference count until the cursor is closed. The underlying database will not close until - * this cursor is closed. - */ - private class KeepAliveCursor( - private val delegate: Cursor, - private val autoCloser: AutoCloser - ) : Cursor by delegate { - // close is the only important/changed method here: - override fun close() { - delegate.close() - autoCloser.decrementCountAndScheduleClose() - } - - @RequiresApi(api = Build.VERSION_CODES.Q) - override fun setNotificationUris( - cr: ContentResolver, - uris: List - ) { - SupportSQLiteCompat.Api29Impl.setNotificationUris(delegate, cr, uris) - } - - @RequiresApi(api = Build.VERSION_CODES.KITKAT) - override fun getNotificationUri(): Uri { - return SupportSQLiteCompat.Api19Impl.getNotificationUri(delegate) - } - - @RequiresApi(api = Build.VERSION_CODES.Q) - override fun getNotificationUris(): List { - return SupportSQLiteCompat.Api29Impl.getNotificationUris(delegate) - } - - @RequiresApi(api = Build.VERSION_CODES.M) - override fun setExtras(extras: Bundle) { - SupportSQLiteCompat.Api23Impl.setExtras(delegate, extras) - } - } - - /** - * We can't close our db if the SupportSqliteStatement is open. - * - * Each of these that are created need to be registered with RefCounter. - * - * On auto-close, RefCounter needs to close each of these before closing the db that these - * were constructed from. - * - * Each of the methods here need to get - */ - // TODO(rohitsat) cache the prepared statement... I'm not sure what the performance implications - // are for the way it's done here, but caching the prepared statement would definitely be more - // complicated since we need to invalidate any of the PreparedStatements that were created - // with this db - private class AutoClosingSupportSqliteStatement( - private val sql: String, - private val autoCloser: AutoCloser - ) : SupportSQLiteStatement { - private val binds = ArrayList() - private fun executeSqliteStatementWithRefCount( - block: (SupportSQLiteStatement) -> T - ): T { - return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase -> - val statement: SupportSQLiteStatement = db.compileStatement(sql) - doBinds(statement) - block(statement) - } - } - - private fun doBinds(supportSQLiteStatement: SupportSQLiteStatement) { - // Replay the binds - binds.forEachIndexed { i, _ -> - val bindIndex = i + 1 // Bind indices are 1 based so we start at 1 not 0 - when (val bind = binds[i]) { - null -> { - supportSQLiteStatement.bindNull(bindIndex) - } - is Long -> { - supportSQLiteStatement.bindLong(bindIndex, bind) - } - is Double -> { - supportSQLiteStatement.bindDouble(bindIndex, bind) - } - is String -> { - supportSQLiteStatement.bindString(bindIndex, bind) - } - is ByteArray -> { - supportSQLiteStatement.bindBlob(bindIndex, bind) - } - } - } - } - - private fun saveBinds(bindIndex: Int, value: Any?) { - val index = bindIndex - 1 - if (index >= binds.size) { - // Add null entries to the list until we have the desired # of indices - for (i in binds.size..index) { - binds.add(null) - } - } - binds[index] = value - } - - @Throws(IOException::class) - override fun close() { - // Nothing to do here since we re-compile the statement each time. - } - - override fun execute() { - executeSqliteStatementWithRefCount { statement: SupportSQLiteStatement -> - statement.execute() - null - } - } - - override fun executeUpdateDelete(): Int { - return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement -> - obj.executeUpdateDelete() } - } - - override fun executeInsert(): Long { - return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement -> - obj.executeInsert() - } - } - - override fun simpleQueryForLong(): Long { - return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement -> - obj.simpleQueryForLong() - } - } - - override fun simpleQueryForString(): String? { - return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement -> - obj.simpleQueryForString() - } - } - - override fun bindNull(index: Int) { - saveBinds(index, null) - } - - override fun bindLong(index: Int, value: Long) { - saveBinds(index, value) - } - - override fun bindDouble(index: Int, value: Double) { - saveBinds(index, value) - } - - override fun bindString(index: Int, value: String) { - saveBinds(index, value) - } - - override fun bindBlob(index: Int, value: ByteArray) { - saveBinds(index, value) - } - - override fun clearBindings() { - binds.clear() - } - } -} diff --git a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java new file mode 100644 index 0000000000..004f60aa35 --- /dev/null +++ b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 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 androidx.room; + +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +/** + * Factory class for AutoClosingRoomOpenHelper + */ +final class AutoClosingRoomOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { + @NonNull + private final SupportSQLiteOpenHelper.Factory mDelegate; + + @NonNull + private final AutoCloser mAutoCloser; + + AutoClosingRoomOpenHelperFactory( + @NonNull SupportSQLiteOpenHelper.Factory factory, + @NonNull AutoCloser autoCloser) { + mDelegate = factory; + mAutoCloser = autoCloser; + } + + /** + * @return AutoClosingRoomOpenHelper instances. + */ + @Override + @NonNull + public AutoClosingRoomOpenHelper create( + @NonNull SupportSQLiteOpenHelper.Configuration configuration) { + return new AutoClosingRoomOpenHelper(mDelegate.create(configuration), mAutoCloser); + } +} diff --git a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.kt b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.kt deleted file mode 100644 index 59ee28606a..0000000000 --- a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 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 androidx.room - -import androidx.sqlite.db.SupportSQLiteOpenHelper - -/** - * Factory class for AutoClosingRoomOpenHelper - */ -internal class AutoClosingRoomOpenHelperFactory( - private val delegate: SupportSQLiteOpenHelper.Factory, - private val autoCloser: AutoCloser -) : SupportSQLiteOpenHelper.Factory { - /** - * @return AutoClosingRoomOpenHelper instances. - */ - override fun create( - configuration: SupportSQLiteOpenHelper.Configuration - ): AutoClosingRoomOpenHelper { - return AutoClosingRoomOpenHelper(delegate.create(configuration), autoCloser) - } -} diff --git a/app/src/main/java/androidx/room/migration/AutoMigrationSpec.kt b/app/src/main/java/androidx/room/AutoMigrationSpec.java similarity index 77% rename from app/src/main/java/androidx/room/migration/AutoMigrationSpec.kt rename to app/src/main/java/androidx/room/AutoMigrationSpec.java index 99b2ea0e2d..1eea1897a8 100644 --- a/app/src/main/java/androidx/room/migration/AutoMigrationSpec.kt +++ b/app/src/main/java/androidx/room/AutoMigrationSpec.java @@ -13,23 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.room.migration -import androidx.sqlite.db.SupportSQLiteDatabase +package androidx.room.migration; +import androidx.annotation.NonNull; +import androidx.room.AutoMigration; +import androidx.sqlite.db.SupportSQLiteDatabase; /** * Interface for defining an automatic migration specification for Room databases. - * + *

* The methods defined in this interface will be called on a background thread from the executor * set in Room's builder. It is important to note that the methods are all in a transaction when * it is called. * - * For details, see [androidx.room.AutoMigration] + * @see AutoMigration */ -interface AutoMigrationSpec { +public interface AutoMigrationSpec { + /** * Invoked after the migration is completed. * @param db The SQLite database. */ - fun onPostMigrate(db: SupportSQLiteDatabase) {} + default void onPostMigrate(@NonNull SupportSQLiteDatabase db) {} } diff --git a/app/src/main/java/androidx/room/CopyLock.java b/app/src/main/java/androidx/room/CopyLock.java new file mode 100644 index 0000000000..8db020c932 --- /dev/null +++ b/app/src/main/java/androidx/room/CopyLock.java @@ -0,0 +1,112 @@ +/* + * Copyright 2019 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 androidx.room.util; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Utility class for in-process and multi-process key-based lock mechanism for safely copying + * database files. + *

+ * Acquiring the lock will be quick if no other thread or process has a lock with the same key. + * But if the lock is already held then acquiring it will block, until the other thread or process + * releases the lock. Note that the key and lock directory must be the same to achieve + * synchronization. + *

+ * Locking is done via two levels: + *

    + *
  1. + * Thread locking within the same JVM process is done via a map of String key to ReentrantLock + * objects. + *
  2. + * Multi-process locking is done via a lock file whose name contains the key and FileLock + * objects. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public class CopyLock { + + // in-process lock map + private static final Map sThreadLocks = new HashMap<>(); + + private final File mCopyLockFile; + private final Lock mThreadLock; + private final boolean mFileLevelLock; + private FileChannel mLockChannel; + + /** + * Creates a lock with {@code name} and using {@code lockDir} as the directory for the + * lock files. + * @param name the name of this lock. + * @param lockDir the directory where the lock files will be located. + * @param processLock whether to use file for process level locking or not. + */ + public CopyLock(@NonNull String name, @NonNull File lockDir, boolean processLock) { + mCopyLockFile = new File(lockDir, name + ".lck"); + mThreadLock = getThreadLock(mCopyLockFile.getAbsolutePath()); + mFileLevelLock = processLock; + } + + /** + * Attempts to grab the lock, blocking if already held by another thread or process. + */ + public void lock() { + mThreadLock.lock(); + if (mFileLevelLock) { + try { + mLockChannel = new FileOutputStream(mCopyLockFile).getChannel(); + mLockChannel.lock(); + } catch (IOException e) { + throw new IllegalStateException("Unable to grab copy lock.", e); + } + } + } + + /** + * Releases the lock. + */ + public void unlock() { + if (mLockChannel != null) { + try { + mLockChannel.close(); + } catch (IOException ignored) { } + } + mThreadLock.unlock(); + } + + private static Lock getThreadLock(String key) { + synchronized (sThreadLocks) { + Lock threadLock = sThreadLocks.get(key); + if (threadLock == null) { + threadLock = new ReentrantLock(); + sThreadLocks.put(key, threadLock); + } + return threadLock; + } + } +} diff --git a/app/src/main/java/androidx/room/CursorUtil.java b/app/src/main/java/androidx/room/CursorUtil.java new file mode 100644 index 0000000000..09da6e2bb9 --- /dev/null +++ b/app/src/main/java/androidx/room/CursorUtil.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2018 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 androidx.room.util; + +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; + +import java.util.Arrays; + +/** + * Cursor utilities for Room + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public class CursorUtil { + + /** + * Copies the given cursor into a in-memory cursor and then closes it. + *

    + * This is useful for iterating over a cursor multiple times without the cost of JNI while + * reading or IO while filling the window at the expense of memory consumption. + * + * @param c the cursor to copy. + * @return a new cursor containing the same data as the given cursor. + */ + @NonNull + public static Cursor copyAndClose(@NonNull Cursor c) { + final MatrixCursor matrixCursor; + try { + matrixCursor = new MatrixCursor(c.getColumnNames(), c.getCount()); + while (c.moveToNext()) { + final Object[] row = new Object[c.getColumnCount()]; + for (int i = 0; i < c.getColumnCount(); i++) { + switch (c.getType(i)) { + case Cursor.FIELD_TYPE_NULL: + row[i] = null; + break; + case Cursor.FIELD_TYPE_INTEGER: + row[i] = c.getLong(i); + break; + case Cursor.FIELD_TYPE_FLOAT: + row[i] = c.getDouble(i); + break; + case Cursor.FIELD_TYPE_STRING: + row[i] = c.getString(i); + break; + case Cursor.FIELD_TYPE_BLOB: + row[i] = c.getBlob(i); + break; + default: + throw new IllegalStateException(); + } + } + matrixCursor.addRow(row); + } + } finally { + c.close(); + } + return matrixCursor; + } + + /** + * Patches {@link Cursor#getColumnIndex(String)} to work around issues on older devices. + * If the column is not found, it retries with the specified name surrounded by backticks. + * + * @param c The cursor. + * @param name The name of the target column. + * @return The index of the column, or -1 if not found. + */ + public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) { + int index = c.getColumnIndex(name); + if (index >= 0) { + return index; + } + index = c.getColumnIndex("`" + name + "`"); + if (index >= 0) { + return index; + } + return findColumnIndexBySuffix(c, name); + } + + /** + * Patches {@link Cursor#getColumnIndexOrThrow(String)} to work around issues on older devices. + * If the column is not found, it retries with the specified name surrounded by backticks. + * + * @param c The cursor. + * @param name The name of the target column. + * @return The index of the column. + * @throws IllegalArgumentException if the column does not exist. + */ + public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) { + final int index = getColumnIndex(c, name); + if (index >= 0) { + return index; + } + String availableColumns = ""; + try { + availableColumns = Arrays.toString(c.getColumnNames()); + } catch (Exception e) { + Log.d("RoomCursorUtil", "Cannot collect column names for debug purposes", e); + } + throw new IllegalArgumentException("column '" + name + + "' does not exist. Available columns: " + availableColumns); + } + + /** + * Finds a column by name by appending `.` in front of it and checking by suffix match. + * Also checks for the version wrapped with `` (backticks). + * workaround for b/157261134 for API levels 25 and below + * + * e.g. "foo" will match "any.foo" and "`any.foo`" + */ + private static int findColumnIndexBySuffix(@NonNull Cursor cursor, @NonNull String name) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { + // we need this workaround only on APIs < 26. So just return not found on newer APIs + return -1; + } + if (name.length() == 0) { + return -1; + } + final String[] columnNames = cursor.getColumnNames(); + return findColumnIndexBySuffix(columnNames, name); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + static int findColumnIndexBySuffix(String[] columnNames, String name) { + String dotSuffix = "." + name; + String backtickSuffix = "." + name + "`"; + for (int index = 0; index < columnNames.length; index++) { + String columnName = columnNames[index]; + // do not check if column name is not long enough. 1 char for table name, 1 char for '.' + if (columnName.length() >= name.length() + 2) { + if (columnName.endsWith(dotSuffix)) { + return index; + } else if (columnName.charAt(0) == '`' + && columnName.endsWith(backtickSuffix)) { + return index; + } + } + } + return -1; + } + + private CursorUtil() { + } +} diff --git a/app/src/main/java/androidx/room/DBUtil.java b/app/src/main/java/androidx/room/DBUtil.java new file mode 100644 index 0000000000..8f876ae8ee --- /dev/null +++ b/app/src/main/java/androidx/room/DBUtil.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2018 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 androidx.room.util; + +import android.database.AbstractWindowedCursor; +import android.database.Cursor; +import android.os.Build; +import android.os.CancellationSignal; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.room.RoomDatabase; +import androidx.sqlite.db.SupportSQLiteCompat; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteQuery; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Database utilities for Room + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public class DBUtil { + + /** + * Performs the SQLiteQuery on the given database. + *

    + * This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is + * {@code true} and either the api level is below a certain threshold or the full result of the + * query does not fit in a single window. + * + * @param db The database to perform the query on. + * @param sqLiteQuery The query to perform. + * @param maybeCopy True if the result cursor should maybe be copied, false otherwise. + * @return Result of the query. + * + * @deprecated This is only used in the generated code and shouldn't be called directly. + */ + @Deprecated + @NonNull + public static Cursor query(RoomDatabase db, SupportSQLiteQuery sqLiteQuery, boolean maybeCopy) { + return query(db, sqLiteQuery, maybeCopy, null); + } + + /** + * Performs the SQLiteQuery on the given database. + *

    + * This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is + * {@code true} and either the api level is below a certain threshold or the full result of the + * query does not fit in a single window. + * + * @param db The database to perform the query on. + * @param sqLiteQuery The query to perform. + * @param maybeCopy True if the result cursor should maybe be copied, false otherwise. + * @param signal The cancellation signal to be attached to the query. + * @return Result of the query. + */ + @NonNull + public static Cursor query(@NonNull RoomDatabase db, @NonNull SupportSQLiteQuery sqLiteQuery, + boolean maybeCopy, @Nullable CancellationSignal signal) { + final Cursor cursor = db.query(sqLiteQuery, signal); + if (maybeCopy && cursor instanceof AbstractWindowedCursor) { + AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor; + int rowsInCursor = windowedCursor.getCount(); // Should fill the window. + int rowsInWindow; + if (windowedCursor.hasWindow()) { + rowsInWindow = windowedCursor.getWindow().getNumRows(); + } else { + rowsInWindow = rowsInCursor; + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) { + return CursorUtil.copyAndClose(windowedCursor); + } + } + + return cursor; + } + + /** + * Drops all FTS content sync triggers created by Room. + *

    + * FTS content sync triggers created by Room are those that are found in the sqlite_master table + * who's names start with 'room_fts_content_sync_'. + * + * @param db The database. + */ + public static void dropFtsSyncTriggers(SupportSQLiteDatabase db) { + List existingTriggers = new ArrayList<>(); + Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'"); + //noinspection TryFinallyCanBeTryWithResources + try { + while (cursor.moveToNext()) { + existingTriggers.add(cursor.getString(0)); + } + } finally { + cursor.close(); + } + + for (String triggerName : existingTriggers) { + if (triggerName.startsWith("room_fts_content_sync_")) { + db.execSQL("DROP TRIGGER IF EXISTS " + triggerName); + } + } + } + + /** + * Checks for foreign key violations by executing a PRAGMA foreign_key_check. + */ + public static void foreignKeyCheck(@NonNull SupportSQLiteDatabase db, + @NonNull String tableName) { + Cursor cursor = db.query("PRAGMA foreign_key_check(`" + tableName + "`)"); + try { + if (cursor.getCount() > 0) { + String errorMsg = processForeignKeyCheckFailure(cursor); + throw new IllegalStateException(errorMsg); + } + } finally { + cursor.close(); + } + } + + /** + * Reads the user version number out of the database header from the given file. + * + * @param databaseFile the database file. + * @return the database version + * @throws IOException if something goes wrong reading the file, such as bad database header or + * missing permissions. + * + * @see User Version + * Number. + */ + public static int readVersion(@NonNull File databaseFile) throws IOException { + FileChannel input = null; + try { + ByteBuffer buffer = ByteBuffer.allocate(4); + input = new FileInputStream(databaseFile).getChannel(); + input.tryLock(60, 4, true); + input.position(60); + int read = input.read(buffer); + if (read != 4) { + throw new IOException("Bad database header, unable to read 4 bytes at offset 60"); + } + buffer.rewind(); + return buffer.getInt(); // ByteBuffer is big-endian by default + } finally { + if (input != null) { + input.close(); + } + } + } + + /** + * CancellationSignal is only available from API 16 on. This function will create a new + * instance of the Cancellation signal only if the current API > 16. + * + * @return A new instance of CancellationSignal or null. + */ + @Nullable + public static CancellationSignal createCancellationSignal() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return SupportSQLiteCompat.Api16Impl.createCancellationSignal(); + } + return null; + } + + + /** + * Converts the {@link Cursor} returned in case of a foreign key violation into a detailed + * error message for debugging. + *

    + * The foreign_key_check pragma returns one row output for each foreign key violation. + *

    + * The cursor received has four columns for each row output. The first column is the name of + * the child table. The second column is the rowId of the row that contains the foreign key + * violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the + * name of the parent table. The fourth column is the index of the specific foreign key + * constraint that failed. + * + * @param cursor Cursor containing information regarding the FK violation + * @return Error message generated containing debugging information + */ + private static String processForeignKeyCheckFailure(Cursor cursor) { + int rowCount = cursor.getCount(); + String childTableName = null; + Map fkParentTables = new HashMap<>(); + + while (cursor.moveToNext()) { + if (childTableName == null) { + childTableName = cursor.getString(0); + } + String constraintIndex = cursor.getString(3); + if (!fkParentTables.containsKey(constraintIndex)) { + fkParentTables.put(constraintIndex, cursor.getString(2)); + } + } + + StringBuilder sb = new StringBuilder(); + sb.append("Foreign key violation(s) detected in '") + .append(childTableName).append("'.\n"); + sb.append("Number of different violations discovered: ") + .append(fkParentTables.keySet().size()).append("\n"); + sb.append("Number of rows in violation: ") + .append(rowCount).append("\n"); + sb.append("Violation(s) detected in the following constraint(s):\n"); + + for (Map.Entry entry : fkParentTables.entrySet()) { + sb.append("\tParent Table = ") + .append(entry.getValue()); + sb.append(", Foreign Key Constraint Index = ") + .append(entry.getKey()).append("\n"); + } + return sb.toString(); + } + + private DBUtil() { + } +} diff --git a/app/src/main/java/androidx/room/DatabaseConfiguration.java b/app/src/main/java/androidx/room/DatabaseConfiguration.java new file mode 100644 index 0000000000..01e96bc152 --- /dev/null +++ b/app/src/main/java/androidx/room/DatabaseConfiguration.java @@ -0,0 +1,629 @@ +/* + * Copyright (C) 2016 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 androidx.room; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.room.migration.AutoMigrationSpec; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.io.File; +import java.io.InputStream; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; + +/** + * Configuration class for a {@link RoomDatabase}. + */ +@SuppressWarnings("WeakerAccess") +public class DatabaseConfiguration { + + /** + * The factory to use to access the database. + */ + @NonNull + public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory; + /** + * The context to use while connecting to the database. + */ + @NonNull + public final Context context; + /** + * The name of the database file or null if it is an in-memory database. + */ + @Nullable + public final String name; + + /** + * Collection of available migrations. + */ + @NonNull + public final RoomDatabase.MigrationContainer migrationContainer; + + @Nullable + public final List callbacks; + + @Nullable + public final RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback; + + @NonNull + public final List typeConverters; + + @NonNull + public final List autoMigrationSpecs; + + /** + * Whether Room should throw an exception for queries run on the main thread. + */ + public final boolean allowMainThreadQueries; + + /** + * The journal mode for this database. + */ + public final RoomDatabase.JournalMode journalMode; + + /** + * The Executor used to execute asynchronous queries. + */ + @NonNull + public final Executor queryExecutor; + + /** + * The Executor used to execute asynchronous transactions. + */ + @NonNull + public final Executor transactionExecutor; + + /** + * If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and + * synchronized with other instances of the same {@link RoomDatabase} file, including those + * in a separate process. + */ + public final boolean multiInstanceInvalidation; + + /** + * Intent that should be bound to acquire the invalidation service or {@code null} if not used. + * + * @see {@link #multiInstanceInvalidation} + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public final Intent multiInstanceInvalidationServiceIntent; + + /** + * If true, Room should crash if a migration is missing. + */ + public final boolean requireMigration; + + /** + * If true, Room should perform a destructive migration when downgrading without an available + * migration. + */ + public final boolean allowDestructiveMigrationOnDowngrade; + + /** + * The collection of schema versions from which migrations aren't required. + */ + private final Set mMigrationNotRequiredFrom; + + /** + * The assets path to a pre-packaged database to copy from. + */ + @Nullable + public final String copyFromAssetPath; + + /** + * The pre-packaged database file to copy from. + */ + @Nullable + public final File copyFromFile; + + /** + * The callable to get the input stream from which a pre-package database file will be copied + * from. + */ + @Nullable + public final Callable copyFromInputStream; + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param requireMigration True if Room should require a valid migration if version changes, + * instead of recreating the tables. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * + * @hide + */ + @Deprecated + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + boolean requireMigration, + @Nullable Set migrationNotRequiredFrom) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false, + requireMigration, false, migrationNotRequiredFrom, null, null, null, null, null, + null); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * + * @hide + */ + @Deprecated + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + boolean multiInstanceInvalidation, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, + multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, + migrationNotRequiredFrom, null, null, null, null, null, null); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * @param copyFromAssetPath The assets path to the pre-packaged database. + * @param copyFromFile The pre-packaged database file. + * + * @hide + */ + @Deprecated + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + boolean multiInstanceInvalidation, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, + multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, + migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, null, null, null, null); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * @param copyFromAssetPath The assets path to the pre-packaged database. + * @param copyFromFile The pre-packaged database file. + * @param copyFromInputStream The callable to get the input stream from which a + * pre-package database file will be copied from. + * + * @hide + */ + @Deprecated + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + @NonNull RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + boolean multiInstanceInvalidation, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, + multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, + migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream, + null, null, null); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * @param copyFromAssetPath The assets path to the pre-packaged database. + * @param copyFromFile The pre-packaged database file. + * @param copyFromInputStream The callable to get the input stream from which a + * pre-package database file will be copied from. + * @param prepackagedDatabaseCallback The pre-packaged callback. + * + * @hide + */ + @Deprecated + @SuppressLint("LambdaLast") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + @NonNull RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + boolean multiInstanceInvalidation, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream, + @Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, + multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, + migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream, + prepackagedDatabaseCallback, null, null); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * @param copyFromAssetPath The assets path to the pre-packaged database. + * @param copyFromFile The pre-packaged database file. + * @param copyFromInputStream The callable to get the input stream from which a + * pre-package database file will be copied from. + * @param prepackagedDatabaseCallback The pre-packaged callback. + * @param typeConverters The type converters. + * + * @hide + */ + @Deprecated + @SuppressLint("LambdaLast") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + @NonNull RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + boolean multiInstanceInvalidation, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream, + @Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback, + @Nullable List typeConverters) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, + multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, + migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream, + prepackagedDatabaseCallback, typeConverters, null); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)} + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * @param copyFromAssetPath The assets path to the pre-packaged database. + * @param copyFromFile The pre-packaged database file. + * @param copyFromInputStream The callable to get the input stream from which a + * pre-package database file will be copied from. + * @param prepackagedDatabaseCallback The pre-packaged callback. + * @param typeConverters The type converters. + * @param autoMigrationSpecs The auto migration specs. + * + * @hide + */ + @Deprecated + @SuppressLint("LambdaLast") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + @NonNull RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + boolean multiInstanceInvalidation, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream, + @Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback, + @Nullable List typeConverters, + @Nullable List autoMigrationSpecs) { + this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, + allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, + multiInstanceInvalidation ? new Intent(context, + MultiInstanceInvalidationService.class) : null, + requireMigration, allowDestructiveMigrationOnDowngrade, migrationNotRequiredFrom, + copyFromAssetPath, copyFromFile, copyFromInputStream, prepackagedDatabaseCallback, + typeConverters, autoMigrationSpecs); + } + + /** + * Creates a database configuration with the given values. + * + * @param context The application context. + * @param name Name of the database, can be null if it is in memory. + * @param sqliteOpenHelperFactory The open helper factory to use. + * @param migrationContainer The migration container for migrations. + * @param callbacks The list of callbacks for database events. + * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. + * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. + * @param queryExecutor The Executor used to execute asynchronous queries. + * @param transactionExecutor The Executor used to execute asynchronous transactions. + * @param multiInstanceInvalidationServiceIntent The intent to use to bind to the + * invalidation service or {@code null} if not + * used. + * @param requireMigration True if Room should require a valid migration if version changes, + * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no + * migration is supplied during a downgrade. + * @param migrationNotRequiredFrom The collection of schema versions from which migrations + * aren't required. + * @param copyFromAssetPath The assets path to the pre-packaged database. + * @param copyFromFile The pre-packaged database file. + * @param copyFromInputStream The callable to get the input stream from which a + * pre-package database file will be copied from. + * @param prepackagedDatabaseCallback The pre-packaged callback. + * @param typeConverters The type converters. + * @param autoMigrationSpecs The auto migration specs. + * + * @hide + */ + @SuppressLint("LambdaLast") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public DatabaseConfiguration(@NonNull Context context, @Nullable String name, + @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, + @NonNull RoomDatabase.MigrationContainer migrationContainer, + @Nullable List callbacks, + boolean allowMainThreadQueries, + @NonNull RoomDatabase.JournalMode journalMode, + @NonNull Executor queryExecutor, + @NonNull Executor transactionExecutor, + @Nullable Intent multiInstanceInvalidationServiceIntent, + boolean requireMigration, + boolean allowDestructiveMigrationOnDowngrade, + @Nullable Set migrationNotRequiredFrom, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream, + @Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback, + @Nullable List typeConverters, + @Nullable List autoMigrationSpecs) { + this.sqliteOpenHelperFactory = sqliteOpenHelperFactory; + this.context = context; + this.name = name; + this.migrationContainer = migrationContainer; + this.callbacks = callbacks; + this.allowMainThreadQueries = allowMainThreadQueries; + this.journalMode = journalMode; + this.queryExecutor = queryExecutor; + this.transactionExecutor = transactionExecutor; + this.multiInstanceInvalidationServiceIntent = + multiInstanceInvalidationServiceIntent; + this.multiInstanceInvalidation = multiInstanceInvalidationServiceIntent != null; + this.requireMigration = requireMigration; + this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade; + this.mMigrationNotRequiredFrom = migrationNotRequiredFrom; + this.copyFromAssetPath = copyFromAssetPath; + this.copyFromFile = copyFromFile; + this.copyFromInputStream = copyFromInputStream; + this.prepackagedDatabaseCallback = prepackagedDatabaseCallback; + this.typeConverters = typeConverters == null ? Collections.emptyList() : typeConverters; + this.autoMigrationSpecs = autoMigrationSpecs == null + ? Collections.emptyList() : autoMigrationSpecs; + } + + /** + * Returns whether a migration is required from the specified version. + * + * @param version The schema version. + * @return True if a valid migration is required, false otherwise. + * + * @deprecated Use {@link #isMigrationRequired(int, int)} which takes + * {@link #allowDestructiveMigrationOnDowngrade} into account. + */ + @Deprecated + public boolean isMigrationRequiredFrom(int version) { + return isMigrationRequired(version, version + 1); + } + + /** + * Returns whether a migration is required between two versions. + * + * @param fromVersion The old schema version. + * @param toVersion The new schema version. + * @return True if a valid migration is required, false otherwise. + */ + public boolean isMigrationRequired(int fromVersion, int toVersion) { + // Migrations are not required if its a downgrade AND destructive migration during downgrade + // has been allowed. + final boolean isDowngrade = fromVersion > toVersion; + if (isDowngrade && allowDestructiveMigrationOnDowngrade) { + return false; + } + + // Migrations are required between the two versions if we generally require migrations + // AND EITHER there are no exceptions OR the supplied fromVersion is not one of the + // exceptions. + return requireMigration + && (mMigrationNotRequiredFrom == null + || !mMigrationNotRequiredFrom.contains(fromVersion)); + } +} diff --git a/app/src/main/java/androidx/room/DatabaseConfiguration.kt b/app/src/main/java/androidx/room/DatabaseConfiguration.kt deleted file mode 100644 index 0d4aa4fc17..0000000000 --- a/app/src/main/java/androidx/room/DatabaseConfiguration.kt +++ /dev/null @@ -1,680 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.room - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import androidx.annotation.RestrictTo -import androidx.room.migration.AutoMigrationSpec -import androidx.sqlite.db.SupportSQLiteOpenHelper -import java.io.File -import java.io.InputStream -import java.util.concurrent.Callable -import java.util.concurrent.Executor - -/** - * Configuration class for a [RoomDatabase]. - */ -@Suppress("UNUSED_PARAMETER") -open class DatabaseConfiguration @SuppressLint("LambdaLast") -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -constructor( - /** - * The context to use while connecting to the database. - */ - @JvmField - val context: Context, - - /** - * The name of the database file or null if it is an in-memory database. - */ - @JvmField - val name: String?, - - /** - * The factory to use to access the database. - */ - @JvmField - val sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory, - - /** - * Collection of available migrations. - */ - @JvmField - val migrationContainer: RoomDatabase.MigrationContainer, - - @JvmField - val callbacks: List?, - - /** - * Whether Room should throw an exception for queries run on the main thread. - */ - @JvmField - val allowMainThreadQueries: Boolean, - - /** - * The journal mode for this database. - */ - @JvmField - val journalMode: RoomDatabase.JournalMode, - - /** - * The Executor used to execute asynchronous queries. - */ - @JvmField - val queryExecutor: Executor, - - /** - * The Executor used to execute asynchronous transactions. - */ - @JvmField - val transactionExecutor: Executor, - - /** - * Intent that should be bound to acquire the invalidation service or `null` if not used. - * - * @see [multiInstanceInvalidation] - */ - @field:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @JvmField - val multiInstanceInvalidationServiceIntent: Intent?, - - @JvmField - val requireMigration: Boolean, - - @JvmField - val allowDestructiveMigrationOnDowngrade: Boolean, - - private val migrationNotRequiredFrom: Set?, - @JvmField - val copyFromAssetPath: String?, - - @JvmField - val copyFromFile: File?, - - @JvmField - val copyFromInputStream: Callable?, - - @JvmField - val prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?, - - @JvmField - val typeConverters: List, - - @JvmField - val autoMigrationSpecs: List -) { - /** - * If true, table invalidation in an instance of [RoomDatabase] is broadcast and - * synchronized with other instances of the same [RoomDatabase] file, including those - * in a separate process. - */ - @JvmField - val multiInstanceInvalidation: Boolean = multiInstanceInvalidationServiceIntent != null - - /** - * Creates a database configuration with the given values. - * - * @param context The application context. - * @param name Name of the database, can be null if it is in memory. - * @param sqliteOpenHelperFactory The open helper factory to use. - * @param migrationContainer The migration container for migrations. - * @param callbacks The list of callbacks for database events. - * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. - * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. - * @param queryExecutor The Executor used to execute asynchronous queries. - * @param requireMigration True if Room should require a valid migration if version changes, - * instead of recreating the tables. - * @param migrationNotRequiredFrom The collection of schema versions from which migrations - * aren't required. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated( - "This constructor is deprecated.", - ReplaceWith("DatabaseConfiguration(Context, String, " + - "SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " + - "List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " + - "boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " + - "List, List)") - ) - constructor( - context: Context, - name: String?, - sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory, - migrationContainer: RoomDatabase.MigrationContainer, - callbacks: List?, - allowMainThreadQueries: Boolean, - journalMode: RoomDatabase.JournalMode, - queryExecutor: Executor, - requireMigration: Boolean, - migrationNotRequiredFrom: Set? - ) : this( - context = context, - name = name, - sqliteOpenHelperFactory = sqliteOpenHelperFactory, - migrationContainer = migrationContainer, - callbacks = callbacks, - allowMainThreadQueries = allowMainThreadQueries, - journalMode = journalMode, - queryExecutor = queryExecutor, - transactionExecutor = queryExecutor, - multiInstanceInvalidationServiceIntent = null, - allowDestructiveMigrationOnDowngrade = false, - requireMigration = requireMigration, - migrationNotRequiredFrom = migrationNotRequiredFrom, - copyFromAssetPath = null, - copyFromFile = null, - prepackagedDatabaseCallback = null, - copyFromInputStream = null, - typeConverters = emptyList(), - autoMigrationSpecs = emptyList() - ) - - /** - * Creates a database configuration with the given values. - * - * @param context The application context. - * @param name Name of the database, can be null if it is in memory. - * @param sqliteOpenHelperFactory The open helper factory to use. - * @param migrationContainer The migration container for migrations. - * @param callbacks The list of callbacks for database events. - * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. - * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. - * @param queryExecutor The Executor used to execute asynchronous queries. - * @param transactionExecutor The Executor used to execute asynchronous transactions. - * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. - * @param requireMigration True if Room should require a valid migration if version changes, - * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no - * migration is supplied during a downgrade. - * @param migrationNotRequiredFrom The collection of schema versions from which migrations - * aren't required. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated( - "This constructor is deprecated.", - ReplaceWith("DatabaseConfiguration(Context, String, " + - "SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " + - "List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " + - "boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " + - "List, List)") - ) - constructor( - context: Context, - name: String?, - sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory, - migrationContainer: RoomDatabase.MigrationContainer, - callbacks: List?, - allowMainThreadQueries: Boolean, - journalMode: RoomDatabase.JournalMode, - queryExecutor: Executor, - transactionExecutor: Executor, - multiInstanceInvalidation: Boolean, - requireMigration: Boolean, - allowDestructiveMigrationOnDowngrade: Boolean, - migrationNotRequiredFrom: Set? - ) : this( - context = context, - name = name, - sqliteOpenHelperFactory = sqliteOpenHelperFactory, - migrationContainer = migrationContainer, - callbacks = callbacks, - allowMainThreadQueries = allowMainThreadQueries, - journalMode = journalMode, - queryExecutor = queryExecutor, - transactionExecutor = transactionExecutor, - multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent( - context, - MultiInstanceInvalidationService::class.java - ) else null, - allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade, - requireMigration = requireMigration, - migrationNotRequiredFrom = migrationNotRequiredFrom, - copyFromAssetPath = null, - copyFromFile = null, - prepackagedDatabaseCallback = null, - copyFromInputStream = null, - typeConverters = emptyList(), - autoMigrationSpecs = emptyList() - ) - - /** - * Creates a database configuration with the given values. - * - * @param context The application context. - * @param name Name of the database, can be null if it is in memory. - * @param sqliteOpenHelperFactory The open helper factory to use. - * @param migrationContainer The migration container for migrations. - * @param callbacks The list of callbacks for database events. - * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. - * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. - * @param queryExecutor The Executor used to execute asynchronous queries. - * @param transactionExecutor The Executor used to execute asynchronous transactions. - * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. - * @param requireMigration True if Room should require a valid migration if version changes, - * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no - * migration is supplied during a downgrade. - * @param migrationNotRequiredFrom The collection of schema versions from which migrations - * aren't required. - * @param copyFromAssetPath The assets path to the pre-packaged database. - * @param copyFromFile The pre-packaged database file. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated( - "This constructor is deprecated.", - ReplaceWith("DatabaseConfiguration(Context, String, " + - "SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " + - "List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " + - "boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " + - "List, List)") - ) - constructor( - context: Context, - name: String?, - sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory, - migrationContainer: RoomDatabase.MigrationContainer, - callbacks: List?, - allowMainThreadQueries: Boolean, - journalMode: RoomDatabase.JournalMode, - queryExecutor: Executor, - transactionExecutor: Executor, - multiInstanceInvalidation: Boolean, - requireMigration: Boolean, - allowDestructiveMigrationOnDowngrade: Boolean, - migrationNotRequiredFrom: Set?, - copyFromAssetPath: String?, - copyFromFile: File? - ) : this( - context = context, - name = name, - sqliteOpenHelperFactory = sqliteOpenHelperFactory, - migrationContainer = migrationContainer, - callbacks = callbacks, - allowMainThreadQueries = allowMainThreadQueries, - journalMode = journalMode, - queryExecutor = queryExecutor, - transactionExecutor = transactionExecutor, - multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent( - context, - MultiInstanceInvalidationService::class.java - ) else null, - allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade, - requireMigration = requireMigration, - migrationNotRequiredFrom = migrationNotRequiredFrom, - copyFromAssetPath = copyFromAssetPath, - copyFromFile = copyFromFile, - prepackagedDatabaseCallback = null, - copyFromInputStream = null, - typeConverters = emptyList(), - autoMigrationSpecs = emptyList() - ) - - /** - * Creates a database configuration with the given values. - * - * @param context The application context. - * @param name Name of the database, can be null if it is in memory. - * @param sqliteOpenHelperFactory The open helper factory to use. - * @param migrationContainer The migration container for migrations. - * @param callbacks The list of callbacks for database events. - * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. - * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. - * @param queryExecutor The Executor used to execute asynchronous queries. - * @param transactionExecutor The Executor used to execute asynchronous transactions. - * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. - * @param requireMigration True if Room should require a valid migration if version changes, - * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no - * migration is supplied during a downgrade. - * @param migrationNotRequiredFrom The collection of schema versions from which migrations - * aren't required. - * @param copyFromAssetPath The assets path to the pre-packaged database. - * @param copyFromFile The pre-packaged database file. - * @param copyFromInputStream The callable to get the input stream from which a - * pre-package database file will be copied from. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated( - "This constructor is deprecated.", - ReplaceWith("DatabaseConfiguration(Context, String, " + - "SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " + - "List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " + - "boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " + - "List, List)") - ) - constructor( - context: Context, - name: String?, - sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory, - migrationContainer: RoomDatabase.MigrationContainer, - callbacks: List?, - allowMainThreadQueries: Boolean, - journalMode: RoomDatabase.JournalMode, - queryExecutor: Executor, - transactionExecutor: Executor, - multiInstanceInvalidation: Boolean, - requireMigration: Boolean, - allowDestructiveMigrationOnDowngrade: Boolean, - migrationNotRequiredFrom: Set?, - copyFromAssetPath: String?, - copyFromFile: File?, - copyFromInputStream: Callable? - ) : this( - context = context, - name = name, - sqliteOpenHelperFactory = sqliteOpenHelperFactory, - migrationContainer = migrationContainer, - callbacks = callbacks, - allowMainThreadQueries = allowMainThreadQueries, - journalMode = journalMode, - queryExecutor = queryExecutor, - transactionExecutor = transactionExecutor, - multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent( - context, - MultiInstanceInvalidationService::class.java - ) else null, - allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade, - requireMigration = requireMigration, - migrationNotRequiredFrom = migrationNotRequiredFrom, - copyFromAssetPath = copyFromAssetPath, - copyFromFile = copyFromFile, - prepackagedDatabaseCallback = null, - copyFromInputStream = copyFromInputStream, - typeConverters = emptyList(), - autoMigrationSpecs = emptyList() - ) - - /** - * Creates a database configuration with the given values. - * - * @param context The application context. - * @param name Name of the database, can be null if it is in memory. - * @param sqliteOpenHelperFactory The open helper factory to use. - * @param migrationContainer The migration container for migrations. - * @param callbacks The list of callbacks for database events. - * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. - * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. - * @param queryExecutor The Executor used to execute asynchronous queries. - * @param transactionExecutor The Executor used to execute asynchronous transactions. - * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. - * @param requireMigration True if Room should require a valid migration if version changes, - * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no - * migration is supplied during a downgrade. - * @param migrationNotRequiredFrom The collection of schema versions from which migrations - * aren't required. - * @param copyFromAssetPath The assets path to the pre-packaged database. - * @param copyFromFile The pre-packaged database file. - * @param copyFromInputStream The callable to get the input stream from which a - * pre-package database file will be copied from. - * @param prepackagedDatabaseCallback The pre-packaged callback. - * - */ - @SuppressLint("LambdaLast") - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated( - "This constructor is deprecated.", - ReplaceWith("DatabaseConfiguration(Context, String, " + - "SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " + - "List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " + - "boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " + - "List, List)") - ) - constructor( - context: Context, - name: String?, - sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory, - migrationContainer: RoomDatabase.MigrationContainer, - callbacks: List?, - allowMainThreadQueries: Boolean, - journalMode: RoomDatabase.JournalMode, - queryExecutor: Executor, - transactionExecutor: Executor, - multiInstanceInvalidation: Boolean, - requireMigration: Boolean, - allowDestructiveMigrationOnDowngrade: Boolean, - migrationNotRequiredFrom: Set?, - copyFromAssetPath: String?, - copyFromFile: File?, - copyFromInputStream: Callable?, - prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback? - ) : this( - context = context, - name = name, - sqliteOpenHelperFactory = sqliteOpenHelperFactory, - migrationContainer = migrationContainer, - callbacks = callbacks, - allowMainThreadQueries = allowMainThreadQueries, - journalMode = journalMode, - queryExecutor = queryExecutor, - transactionExecutor = transactionExecutor, - multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent( - context, - MultiInstanceInvalidationService::class.java - ) else null, - allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade, - requireMigration = requireMigration, - migrationNotRequiredFrom = migrationNotRequiredFrom, - copyFromAssetPath = copyFromAssetPath, - copyFromFile = copyFromFile, - prepackagedDatabaseCallback = prepackagedDatabaseCallback, - copyFromInputStream = copyFromInputStream, - typeConverters = emptyList(), - autoMigrationSpecs = emptyList() - ) - - /** - * Creates a database configuration with the given values. - * - * @param context The application context. - * @param name Name of the database, can be null if it is in memory. - * @param sqliteOpenHelperFactory The open helper factory to use. - * @param migrationContainer The migration container for migrations. - * @param callbacks The list of callbacks for database events. - * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. - * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. - * @param queryExecutor The Executor used to execute asynchronous queries. - * @param transactionExecutor The Executor used to execute asynchronous transactions. - * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. - * @param requireMigration True if Room should require a valid migration if version changes, - * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no - * migration is supplied during a downgrade. - * @param migrationNotRequiredFrom The collection of schema versions from which migrations - * aren't required. - * @param copyFromAssetPath The assets path to the pre-packaged database. - * @param copyFromFile The pre-packaged database file. - * @param copyFromInputStream The callable to get the input stream from which a - * pre-package database file will be copied from. - * @param prepackagedDatabaseCallback The pre-packaged callback. - * @param typeConverters The type converters. - * - */ - @SuppressLint("LambdaLast") - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated( - "This constructor is deprecated.", - ReplaceWith("DatabaseConfiguration(Context, String, " + - "SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " + - "List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " + - "boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " + - "List, List)") - ) - constructor( - context: Context, - name: String?, - sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory, - migrationContainer: RoomDatabase.MigrationContainer, - callbacks: List?, - allowMainThreadQueries: Boolean, - journalMode: RoomDatabase.JournalMode, - queryExecutor: Executor, - transactionExecutor: Executor, - multiInstanceInvalidation: Boolean, - requireMigration: Boolean, - allowDestructiveMigrationOnDowngrade: Boolean, - migrationNotRequiredFrom: Set?, - copyFromAssetPath: String?, - copyFromFile: File?, - copyFromInputStream: Callable?, - prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?, - typeConverters: List - ) : this( - context = context, - name = name, - sqliteOpenHelperFactory = sqliteOpenHelperFactory, - migrationContainer = migrationContainer, - callbacks = callbacks, - allowMainThreadQueries = allowMainThreadQueries, - journalMode = journalMode, - queryExecutor = queryExecutor, - transactionExecutor = transactionExecutor, - multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent( - context, - MultiInstanceInvalidationService::class.java - ) else null, - allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade, - requireMigration = requireMigration, - migrationNotRequiredFrom = migrationNotRequiredFrom, - copyFromAssetPath = copyFromAssetPath, - copyFromFile = copyFromFile, - prepackagedDatabaseCallback = prepackagedDatabaseCallback, - copyFromInputStream = copyFromInputStream, - typeConverters = typeConverters, - autoMigrationSpecs = emptyList() - ) - - /** - * Creates a database configuration with the given values. - * - * @param context The application context. - * @param name Name of the database, can be null if it is in memory. - * @param sqliteOpenHelperFactory The open helper factory to use. - * @param migrationContainer The migration container for migrations. - * @param callbacks The list of callbacks for database events. - * @param allowMainThreadQueries Whether to allow main thread reads/writes or not. - * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING. - * @param queryExecutor The Executor used to execute asynchronous queries. - * @param transactionExecutor The Executor used to execute asynchronous transactions. - * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation. - * @param requireMigration True if Room should require a valid migration if version changes, - * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no - * migration is supplied during a downgrade. - * @param migrationNotRequiredFrom The collection of schema versions from which migrations - * aren't required. - * @param copyFromAssetPath The assets path to the pre-packaged database. - * @param copyFromFile The pre-packaged database file. - * @param copyFromInputStream The callable to get the input stream from which a - * pre-package database file will be copied from. - * @param prepackagedDatabaseCallback The pre-packaged callback. - * @param typeConverters The type converters. - * @param autoMigrationSpecs The auto migration specs. - * - */ - @SuppressLint("LambdaLast") - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated( - "This constructor is deprecated.", - ReplaceWith("DatabaseConfiguration(Context, String, " + - "SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " + - "List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " + - "boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " + - "List, List)") - ) - constructor( - context: Context, - name: String?, - sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory, - migrationContainer: RoomDatabase.MigrationContainer, - callbacks: List?, - allowMainThreadQueries: Boolean, - journalMode: RoomDatabase.JournalMode, - queryExecutor: Executor, - transactionExecutor: Executor, - multiInstanceInvalidation: Boolean, - requireMigration: Boolean, - allowDestructiveMigrationOnDowngrade: Boolean, - migrationNotRequiredFrom: Set?, - copyFromAssetPath: String?, - copyFromFile: File?, - copyFromInputStream: Callable?, - prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?, - typeConverters: List, - autoMigrationSpecs: List - ) : this( - context = context, - name = name, - sqliteOpenHelperFactory = sqliteOpenHelperFactory, - migrationContainer = migrationContainer, - callbacks = callbacks, - allowMainThreadQueries = allowMainThreadQueries, - journalMode = journalMode, - queryExecutor = queryExecutor, - transactionExecutor = transactionExecutor, - multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent( - context, - MultiInstanceInvalidationService::class.java - ) else null, - allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade, - requireMigration = requireMigration, - migrationNotRequiredFrom = migrationNotRequiredFrom, - copyFromAssetPath = copyFromAssetPath, - copyFromFile = copyFromFile, - prepackagedDatabaseCallback = null, - copyFromInputStream = copyFromInputStream, - typeConverters = typeConverters, - autoMigrationSpecs = autoMigrationSpecs - ) - - /** - * Returns whether a migration is required from the specified version. - * - * @param version The schema version. - * @return True if a valid migration is required, false otherwise. - * - */ - @Deprecated( - """Use [isMigrationRequired(int, int)] which takes - [allowDestructiveMigrationOnDowngrade] into account.""", - ReplaceWith("isMigrationRequired(version, version + 1)") - ) - open fun isMigrationRequiredFrom(version: Int): Boolean { - return isMigrationRequired(version, version + 1) - } - - /** - * Returns whether a migration is required between two versions. - * - * @param fromVersion The old schema version. - * @param toVersion The new schema version. - * @return True if a valid migration is required, false otherwise. - */ - open fun isMigrationRequired(fromVersion: Int, toVersion: Int): Boolean { - // Migrations are not required if its a downgrade AND destructive migration during downgrade - // has been allowed. - val isDowngrade = fromVersion > toVersion - if (isDowngrade && allowDestructiveMigrationOnDowngrade) { - return false - } else { - // Migrations are required between the two versions if we generally require migrations - // AND EITHER there are no exceptions OR the supplied fromVersion is not one of the - // exceptions. - return requireMigration && (migrationNotRequiredFrom == null || - !migrationNotRequiredFrom.contains(fromVersion)) - } - } -} diff --git a/app/src/main/java/androidx/room/DelegatingOpenHelper.kt b/app/src/main/java/androidx/room/DelegatingOpenHelper.java similarity index 65% rename from app/src/main/java/androidx/room/DelegatingOpenHelper.kt rename to app/src/main/java/androidx/room/DelegatingOpenHelper.java index b3eac5e5c7..7c84fc5487 100644 --- a/app/src/main/java/androidx/room/DelegatingOpenHelper.kt +++ b/app/src/main/java/androidx/room/DelegatingOpenHelper.java @@ -13,19 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.room +package androidx.room; -import androidx.sqlite.db.SupportSQLiteOpenHelper +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteOpenHelper; /** - * Internal interface for OpenHelpers which delegate to other open helpers. + * Package private interface for OpenHelpers which delegate to other open helpers. * * TODO(b/175612939): delete this interface once implementations are merged. */ -internal interface DelegatingOpenHelper { +interface DelegatingOpenHelper { + /** - * The delegate open helper (which may itself be a DelegatingOpenHelper) so + * Returns the delegate open helper (which may itself be a DelegatingOpenHelper) so * configurations on specific instances can be applied. + * + * @return the delegate */ - val delegate: SupportSQLiteOpenHelper + @NonNull + SupportSQLiteOpenHelper getDelegate(); } diff --git a/app/src/main/java/androidx/room/DeleteMe.kt b/app/src/main/java/androidx/room/DeleteMe.kt new file mode 100644 index 0000000000..4474168c82 --- /dev/null +++ b/app/src/main/java/androidx/room/DeleteMe.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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. + */ + +// This file exists to trick AGP/lint to work around b/234865137 \ No newline at end of file diff --git a/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt b/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java similarity index 53% rename from app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt rename to app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java index 9bfd1d2d82..154103cfcb 100644 --- a/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt +++ b/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java @@ -13,40 +13,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.room -import androidx.annotation.RestrictTo -import androidx.sqlite.db.SupportSQLiteStatement +package androidx.room; + +import androidx.annotation.RestrictTo; +import androidx.sqlite.db.SupportSQLiteStatement; /** * Implementations of this class knows how to delete or update a particular entity. - * + *

    * This is an internal library class and all of its implementations are auto-generated. * - * @constructor Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the - * given database. - * - * @param T The type parameter of the entity to be deleted -*/ + * @param The type parameter of the entity to be deleted + * @hide + */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -abstract class EntityDeletionOrUpdateAdapter ( - database: RoomDatabase -) : SharedSQLiteStatement(database) { +@SuppressWarnings({"WeakerAccess", "unused"}) +public abstract class EntityDeletionOrUpdateAdapter extends SharedSQLiteStatement { + /** + * Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given + * database. + * + * @param database The database to delete / update the item in. + */ + public EntityDeletionOrUpdateAdapter(RoomDatabase database) { + super(database); + } + /** * Create the deletion or update query * * @return An SQL query that can delete or update instances of T. */ - abstract override fun createQuery(): String + @Override + protected abstract String createQuery(); /** * Binds the entity into the given statement. * * @param statement The SQLite statement that prepared for the query returned from - * createQuery. + * createQuery. * @param entity The entity of type T. */ - protected abstract fun bind(statement: SupportSQLiteStatement, entity: T) + protected abstract void bind(SupportSQLiteStatement statement, T entity); /** * Deletes or updates the given entities in the database and returns the affected row count. @@ -54,13 +63,13 @@ abstract class EntityDeletionOrUpdateAdapter ( * @param entity The entity to delete or update * @return The number of affected rows */ - fun handle(entity: T): Int { - val stmt: SupportSQLiteStatement = acquire() - return try { - bind(stmt, entity) - stmt.executeUpdateDelete() + public final int handle(T entity) { + final SupportSQLiteStatement stmt = acquire(); + try { + bind(stmt, entity); + return stmt.executeUpdateDelete(); } finally { - release(stmt) + release(stmt); } } @@ -70,17 +79,17 @@ abstract class EntityDeletionOrUpdateAdapter ( * @param entities Entities to delete or update * @return The number of affected rows */ - fun handleMultiple(entities: Iterable): Int { - val stmt: SupportSQLiteStatement = acquire() - return try { - var total = 0 - entities.forEach { entity -> - bind(stmt, entity) - total += stmt.executeUpdateDelete() + public final int handleMultiple(Iterable entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + int total = 0; + for (T entity : entities) { + bind(stmt, entity); + total += stmt.executeUpdateDelete(); } - total + return total; } finally { - release(stmt) + release(stmt); } } @@ -90,17 +99,17 @@ abstract class EntityDeletionOrUpdateAdapter ( * @param entities Entities to delete or update * @return The number of affected rows */ - fun handleMultiple(entities: Array): Int { - val stmt: SupportSQLiteStatement = acquire() - return try { - var total = 0 - entities.forEach { entity -> - bind(stmt, entity) - total += stmt.executeUpdateDelete() + public final int handleMultiple(T[] entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + int total = 0; + for (T entity : entities) { + bind(stmt, entity); + total += stmt.executeUpdateDelete(); } - total + return total; } finally { - release(stmt) + release(stmt); } } } diff --git a/app/src/main/java/androidx/room/EntityInsertionAdapter.java b/app/src/main/java/androidx/room/EntityInsertionAdapter.java new file mode 100644 index 0000000000..3046d6cd0b --- /dev/null +++ b/app/src/main/java/androidx/room/EntityInsertionAdapter.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2016 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 androidx.room; + +import androidx.annotation.RestrictTo; +import androidx.sqlite.db.SupportSQLiteStatement; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Implementations of this class knows how to insert a particular entity. + *

    + * This is an internal library class and all of its implementations are auto-generated. + * + * @param The type parameter of the entity to be inserted + * @hide + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public abstract class EntityInsertionAdapter extends SharedSQLiteStatement { + /** + * Creates an InsertionAdapter that can insert the entity type T into the given database. + * + * @param database The database to insert into. + */ + public EntityInsertionAdapter(RoomDatabase database) { + super(database); + } + + /** + * Binds the entity into the given statement. + * + * @param statement The SQLite statement that prepared for the query returned from + * createInsertQuery. + * @param entity The entity of type T. + */ + protected abstract void bind(SupportSQLiteStatement statement, T entity); + + /** + * Inserts the entity into the database. + * + * @param entity The entity to insert + */ + public final void insert(T entity) { + final SupportSQLiteStatement stmt = acquire(); + try { + bind(stmt, entity); + stmt.executeInsert(); + } finally { + release(stmt); + } + } + + /** + * Inserts the given entities into the database. + * + * @param entities Entities to insert + */ + public final void insert(T[] entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + for (T entity : entities) { + bind(stmt, entity); + stmt.executeInsert(); + } + } finally { + release(stmt); + } + } + + /** + * Inserts the given entities into the database. + * + * @param entities Entities to insert + */ + public final void insert(Iterable entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + for (T entity : entities) { + bind(stmt, entity); + stmt.executeInsert(); + } + } finally { + release(stmt); + } + } + + /** + * Inserts the given entity into the database and returns the row id. + * + * @param entity The entity to insert + * @return The SQLite row id or -1 if no row is inserted + */ + public final long insertAndReturnId(T entity) { + final SupportSQLiteStatement stmt = acquire(); + try { + bind(stmt, entity); + return stmt.executeInsert(); + } finally { + release(stmt); + } + } + + /** + * Inserts the given entities into the database and returns the row ids. + * + * @param entities Entities to insert + * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 + */ + public final long[] insertAndReturnIdsArray(Collection entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + final long[] result = new long[entities.size()]; + int index = 0; + for (T entity : entities) { + bind(stmt, entity); + result[index] = stmt.executeInsert(); + index++; + } + return result; + } finally { + release(stmt); + } + } + + /** + * Inserts the given entities into the database and returns the row ids. + * + * @param entities Entities to insert + * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 + */ + public final long[] insertAndReturnIdsArray(T[] entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + final long[] result = new long[entities.length]; + int index = 0; + for (T entity : entities) { + bind(stmt, entity); + result[index] = stmt.executeInsert(); + index++; + } + return result; + } finally { + release(stmt); + } + } + + /** + * Inserts the given entities into the database and returns the row ids. + * + * @param entities Entities to insert + * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 + */ + public final Long[] insertAndReturnIdsArrayBox(Collection entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + final Long[] result = new Long[entities.size()]; + int index = 0; + for (T entity : entities) { + bind(stmt, entity); + result[index] = stmt.executeInsert(); + index++; + } + return result; + } finally { + release(stmt); + } + } + + /** + * Inserts the given entities into the database and returns the row ids. + * + * @param entities Entities to insert + * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 + */ + public final Long[] insertAndReturnIdsArrayBox(T[] entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + final Long[] result = new Long[entities.length]; + int index = 0; + for (T entity : entities) { + bind(stmt, entity); + result[index] = stmt.executeInsert(); + index++; + } + return result; + } finally { + release(stmt); + } + } + + /** + * Inserts the given entities into the database and returns the row ids. + * + * @param entities Entities to insert + * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 + */ + public final List insertAndReturnIdsList(T[] entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + final List result = new ArrayList<>(entities.length); + int index = 0; + for (T entity : entities) { + bind(stmt, entity); + result.add(index, stmt.executeInsert()); + index++; + } + return result; + } finally { + release(stmt); + } + } + + /** + * Inserts the given entities into the database and returns the row ids. + * + * @param entities Entities to insert + * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 + */ + public final List insertAndReturnIdsList(Collection entities) { + final SupportSQLiteStatement stmt = acquire(); + try { + final List result = new ArrayList<>(entities.size()); + int index = 0; + for (T entity : entities) { + bind(stmt, entity); + result.add(index, stmt.executeInsert()); + index++; + } + return result; + } finally { + release(stmt); + } + } +} diff --git a/app/src/main/java/androidx/room/EntityInsertionAdapter.kt b/app/src/main/java/androidx/room/EntityInsertionAdapter.kt deleted file mode 100644 index bee98b8330..0000000000 --- a/app/src/main/java/androidx/room/EntityInsertionAdapter.kt +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.room - -import androidx.annotation.RestrictTo -import androidx.sqlite.db.SupportSQLiteStatement - -/** - * Implementations of this class knows how to insert a particular entity. - * - * This is an internal library class and all of its implementations are auto-generated. - * - * @constructor Creates an InsertionAdapter that can insert the entity type T into the given - * database. - * - * @param T The type parameter of the entity to be inserted -*/ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -abstract class EntityInsertionAdapter(database: RoomDatabase) : SharedSQLiteStatement(database) { - /** - * Binds the entity into the given statement. - * - * @param statement The SQLite statement that prepared for the query returned from - * createInsertQuery. - * @param entity The entity of type T. - */ - protected abstract fun bind(statement: SupportSQLiteStatement, entity: T) - - /** - * Inserts the entity into the database. - * - * @param entity The entity to insert - */ - fun insert(entity: T) { - val stmt: SupportSQLiteStatement = acquire() - try { - bind(stmt, entity) - stmt.executeInsert() - } finally { - release(stmt) - } - } - - /** - * Inserts the given entities into the database. - * - * @param entities Entities to insert - */ - fun insert(entities: Array) { - val stmt: SupportSQLiteStatement = acquire() - try { - entities.forEach { entity -> - bind(stmt, entity) - stmt.executeInsert() - } - } finally { - release(stmt) - } - } - - /** - * Inserts the given entities into the database. - * - * @param entities Entities to insert - */ - fun insert(entities: Iterable) { - val stmt: SupportSQLiteStatement = acquire() - try { - entities.forEach { entity -> - bind(stmt, entity) - stmt.executeInsert() - } - } finally { - release(stmt) - } - } - - /** - * Inserts the given entity into the database and returns the row id. - * - * @param entity The entity to insert - * @return The SQLite row id or -1 if no row is inserted - */ - fun insertAndReturnId(entity: T): Long { - val stmt: SupportSQLiteStatement = acquire() - return try { - bind(stmt, entity) - stmt.executeInsert() - } finally { - release(stmt) - } - } - - /** - * Inserts the given entities into the database and returns the row ids. - * - * @param entities Entities to insert - * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 - */ - fun insertAndReturnIdsArray(entities: Collection): LongArray { - val stmt: SupportSQLiteStatement = acquire() - return try { - val result = LongArray(entities.size) - entities.forEachIndexed { index, entity -> - bind(stmt, entity) - result[index] = stmt.executeInsert() - } - result - } finally { - release(stmt) - } - } - - /** - * Inserts the given entities into the database and returns the row ids. - * - * @param entities Entities to insert - * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 - */ - fun insertAndReturnIdsArray(entities: Array): LongArray { - val stmt: SupportSQLiteStatement = acquire() - return try { - val result = LongArray(entities.size) - entities.forEachIndexed { index, entity -> - bind(stmt, entity) - result[index] = stmt.executeInsert() - } - result - } finally { - release(stmt) - } - } - - /** - * Inserts the given entities into the database and returns the row ids. - * - * @param entities Entities to insert - * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 - */ - fun insertAndReturnIdsArrayBox(entities: Collection): Array { - val stmt: SupportSQLiteStatement = acquire() - val iterator = entities.iterator() - return try { - val result = Array(entities.size) { - val entity = iterator.next() - bind(stmt, entity) - stmt.executeInsert() - } - result - } finally { - release(stmt) - } - } - - /** - * Inserts the given entities into the database and returns the row ids. - * - * @param entities Entities to insert - * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 - */ - fun insertAndReturnIdsArrayBox(entities: Array): Array { - val stmt: SupportSQLiteStatement = acquire() - val iterator = entities.iterator() - return try { - val result = Array(entities.size) { - val entity = iterator.next() - bind(stmt, entity) - stmt.executeInsert() - } - result - } finally { - release(stmt) - } - } - - /** - * Inserts the given entities into the database and returns the row ids. - * - * @param entities Entities to insert - * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 - */ - fun insertAndReturnIdsList(entities: Array): List { - val stmt: SupportSQLiteStatement = acquire() - return try { - buildList { - entities.forEach { entity -> - bind(stmt, entity) - add(stmt.executeInsert()) - } - } - } finally { - release(stmt) - } - } - - /** - * Inserts the given entities into the database and returns the row ids. - * - * @param entities Entities to insert - * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 - */ - fun insertAndReturnIdsList(entities: Collection): List { - val stmt: SupportSQLiteStatement = acquire() - return try { - buildList { - entities.forEach { entity -> - bind(stmt, entity) - add(stmt.executeInsert()) - } - } - } finally { - release(stmt) - } - } -} diff --git a/app/src/main/java/androidx/room/EntityUpsertionAdapter.kt b/app/src/main/java/androidx/room/EntityUpsertionAdapter.kt deleted file mode 100644 index db31e112f0..0000000000 --- a/app/src/main/java/androidx/room/EntityUpsertionAdapter.kt +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright 2022 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 androidx.room - -import android.database.sqlite.SQLiteConstraintException -import androidx.annotation.RestrictTo - -/** - * The error code defined by SQLite Library for SQLITE_CONSTRAINT_PRIMARYKEY error - * Only used by android of version newer than 19. - */ -private const val SQLITE_CONSTRAINT_PRIMARYKEY = "1555" - -/** - * The error code defined by SQLite Library for SQLITE_CONSTRAINT_UNIQUE error. - */ -private const val SQLITE_CONSTRAINT_UNIQUE = "2067" - -/** - * For android of version below and including 19, use error message instead of - * error code to check - */ -private const val ErrorMsg = "unique" - -/** - * This class knows how to insert an entity. When the insertion fails - * due to a unique constraint conflict (i.e. primary key conflict), - * it will perform an update. - * - * @constructor Creates an EntityUpsertionAdapter that can upsert entity of type T - * into the database using the given insertionAdapter to perform insertion and - * updateAdapter to perform update when the insertion fails - * - * @param T the type param of the entity to be upserted - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -class EntityUpsertionAdapter( - private val insertionAdapter: EntityInsertionAdapter, - private val updateAdapter: EntityDeletionOrUpdateAdapter -) { - /** - * Inserts the entity into the database. If a constraint exception is thrown - * i.e. a primary key conflict, update the existing entity. - * - * @param entity The entity to insert - */ - fun upsert(entity: T) { - try { - insertionAdapter.insert(entity) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entity) - } - } - - /** - * Upserts (insert or update) the given entities into the database. - * For each entity, insert if it is not already in the database - * update if there is a constraint conflict. - * - * @param entities array of entities to upsert - */ - fun upsert(entities: Array) { - entities.forEach { entity -> - try { - insertionAdapter.insert(entity) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entity) - } - } - } - - fun upsert(entities: Iterable) { - entities.forEach { entity -> - try { - insertionAdapter.insert(entity) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entity) - } - } - } - - /** - * Upserts the given entity into the database and returns the row id. - * If the insertion failed, update the existing entity and return -1. - * - * @param entity The entity to upsert - * @return The SQLite row id or -1 if the insertion failed and update - * is performed - */ - fun upsertAndReturnId(entity: T): Long { - return try { - insertionAdapter.insertAndReturnId(entity) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entity) - -1 - } - } - - /** - * Upserts the given entities into the database and returns the row ids. - * - * @param entities Entities to upsert - * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1 - */ - fun upsertAndReturnIdsArray(entities: Array): LongArray { - return LongArray(entities.size) { index -> - try { - insertionAdapter.insertAndReturnId(entities[index]) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entities[index]) - -1 - } - } - } - - fun upsertAndReturnIdsArray(entities: Collection): LongArray { - val iterator = entities.iterator() - return LongArray(entities.size) { - val entity = iterator.next() - try { - insertionAdapter.insertAndReturnId(entity) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entity) - -1 - } - } - } - - fun upsertAndReturnIdsList(entities: Array): List { - return buildList { - entities.forEach { entity -> - try { - add(insertionAdapter.insertAndReturnId(entity)) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entity) - add(-1) - } - } - } - } - - fun upsertAndReturnIdsList(entities: Collection): List { - return buildList { - entities.forEach { entity -> - try { - add(insertionAdapter.insertAndReturnId(entity)) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entity) - add(-1) - } - } - } - } - - fun upsertAndReturnIdsArrayBox(entities: Array): Array { - return Array(entities.size) { index -> - try { - insertionAdapter.insertAndReturnId(entities[index]) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entities[index]) - -1 - } - } - } - - fun upsertAndReturnIdsArrayBox(entities: Collection): Array { - val iterator = entities.iterator() - return Array(entities.size) { - val entity = iterator.next() - try { - insertionAdapter.insertAndReturnId(entity) - } catch (ex: SQLiteConstraintException) { - checkUniquenessException(ex) - updateAdapter.handle(entity) - -1 - } - } - } - - /** - * Verify if the exception is caused by Uniqueness constraint (Primary Key Conflict). - * If yes, upsert should update the existing one. If not, upsert should re-throw the - * exception. - * For android of version newer than KITKAT(19), SQLite supports ErrorCode. Otherwise, - * check with Error Message. - * - * @param ex the exception thrown by the insert attempt - */ - private fun checkUniquenessException(ex: SQLiteConstraintException) { - val message = ex.message ?: throw ex - val hasUniqueConstraintEx = - message.contains(ErrorMsg, ignoreCase = true) || - message.contains(SQLITE_CONSTRAINT_UNIQUE) || - message.contains(SQLITE_CONSTRAINT_PRIMARYKEY) - - if (!hasUniqueConstraintEx) { - throw ex - } - } -} diff --git a/app/src/main/java/androidx/room/ExperimentalRoomApi.kt b/app/src/main/java/androidx/room/ExperimentalRoomApi.java similarity index 73% rename from app/src/main/java/androidx/room/ExperimentalRoomApi.kt rename to app/src/main/java/androidx/room/ExperimentalRoomApi.java index 20ed03b72f..14896bfadb 100644 --- a/app/src/main/java/androidx/room/ExperimentalRoomApi.kt +++ b/app/src/main/java/androidx/room/ExperimentalRoomApi.java @@ -13,18 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.room +package androidx.room; + +import androidx.annotation.RequiresOptIn; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; -import androidx.annotation.RequiresOptIn /** * APIs marked with ExperimentalRoomApi are experimental and may change. */ -@Target( - AnnotationTarget.CLASS, - AnnotationTarget.FUNCTION -) -@Suppress("UnsafeOptInUsageError") -@RequiresOptIn -@Retention(AnnotationRetention.BINARY) -annotation class ExperimentalRoomApi +@Target({ElementType.TYPE, ElementType.METHOD}) +@RequiresOptIn() +public @interface ExperimentalRoomApi {} diff --git a/app/src/main/java/androidx/room/FileUtil.java b/app/src/main/java/androidx/room/FileUtil.java new file mode 100644 index 0000000000..a221ff741a --- /dev/null +++ b/app/src/main/java/androidx/room/FileUtil.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 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 androidx.room.util; + +import android.annotation.SuppressLint; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; + +/** + * File utilities for Room + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public class FileUtil { + + /** + * Copies data from the input channel to the output file channel. + * + * @param input the input channel to copy. + * @param output the output channel to copy. + * @throws IOException if there is an I/O error. + */ + @SuppressLint("LambdaLast") + public static void copy(@NonNull ReadableByteChannel input, @NonNull FileChannel output) + throws IOException { + try { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { + output.transferFrom(input, 0, Long.MAX_VALUE); + } else { + InputStream inputStream = Channels.newInputStream(input); + OutputStream outputStream = Channels.newOutputStream(output); + int length; + byte[] buffer = new byte[1024 * 4]; + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + } + output.force(false); + } finally { + input.close(); + output.close(); + } + } + + private FileUtil() { + } +} diff --git a/app/src/main/java/androidx/room/FtsTableInfo.java b/app/src/main/java/androidx/room/FtsTableInfo.java new file mode 100644 index 0000000000..aa7305f0ca --- /dev/null +++ b/app/src/main/java/androidx/room/FtsTableInfo.java @@ -0,0 +1,220 @@ +/* + * Copyright 2018 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 androidx.room.util; + +import android.database.Cursor; + +import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A data class that holds the information about an FTS table. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public final class FtsTableInfo { + + // A set of valid FTS Options + private static final String[] FTS_OPTIONS = new String[] { + "tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=", + "order=", "prefix=", "uncompress="}; + + /** + * The table name + */ + public final String name; + + /** + * The column names + */ + public final Set columns; + + /** + * The set of options. Each value in the set contains the option in the following format: + * <key>=<value>. + */ + public final Set options; + + public FtsTableInfo(String name, Set columns, Set options) { + this.name = name; + this.columns = columns; + this.options = options; + } + + public FtsTableInfo(String name, Set columns, String createSql) { + this.name = name; + this.columns = columns; + this.options = parseOptions(createSql); + } + + /** + * Reads the table information from the given database. + * + * @param database The database to read the information from. + * @param tableName The table name. + * @return A FtsTableInfo containing the columns and options for the provided table name. + */ + public static FtsTableInfo read(SupportSQLiteDatabase database, String tableName) { + Set columns = readColumns(database, tableName); + Set options = readOptions(database, tableName); + + return new FtsTableInfo(tableName, columns, options); + } + + @SuppressWarnings("TryFinallyCanBeTryWithResources") + private static Set readColumns(SupportSQLiteDatabase database, String tableName) { + Cursor cursor = database.query("PRAGMA table_info(`" + tableName + "`)"); + Set columns = new HashSet<>(); + try { + if (cursor.getColumnCount() > 0) { + int nameIndex = cursor.getColumnIndex("name"); + while (cursor.moveToNext()) { + columns.add(cursor.getString(nameIndex)); + } + } + } finally { + cursor.close(); + } + return columns; + } + + @SuppressWarnings("TryFinallyCanBeTryWithResources") + private static Set readOptions(SupportSQLiteDatabase database, String tableName) { + String sql = ""; + Cursor cursor = database.query( + "SELECT * FROM sqlite_master WHERE `name` = '" + tableName + "'"); + try { + if (cursor.moveToFirst()) { + sql = cursor.getString(cursor.getColumnIndexOrThrow("sql")); + } + } finally { + cursor.close(); + } + return parseOptions(sql); + } + + /** + * Parses FTS options from the create statement of an FTS table. + * + * This method assumes the given create statement is a valid well-formed SQLite statement as + * defined in the CREATE VIRTUAL TABLE + * syntax diagram. + * + * @param createStatement the "CREATE VIRTUAL TABLE" statement. + * @return the set of FTS option key and values in the create statement. + */ + @VisibleForTesting + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static Set parseOptions(String createStatement) { + if (createStatement.isEmpty()) { + return new HashSet<>(); + } + + // Module arguments are within the parenthesis followed by the module name. + String argsString = createStatement.substring( + createStatement.indexOf('(') + 1, + createStatement.lastIndexOf(')')); + + // Split the module argument string by the comma delimiter, keeping track of quotation so + // so that if the delimiter is found within a string literal we don't substring at the wrong + // index. SQLite supports four ways of quoting keywords, see: + // https://www.sqlite.org/lang_keywords.html + List args = new ArrayList<>(); + ArrayDeque quoteStack = new ArrayDeque<>(); + int lastDelimiterIndex = -1; + for (int i = 0; i < argsString.length(); i++) { + char c = argsString.charAt(i); + switch (c) { + case '\'': + case '"': + case '`': + if (quoteStack.isEmpty()) { + quoteStack.push(c); + } else if (quoteStack.peek() == c) { + quoteStack.pop(); + } + break; + case '[': + if (quoteStack.isEmpty()) { + quoteStack.push(c); + } + break; + case ']': + if (!quoteStack.isEmpty() && quoteStack.peek() == '[') { + quoteStack.pop(); + } + break; + case ',': + if (quoteStack.isEmpty()) { + args.add(argsString.substring(lastDelimiterIndex + 1, i).trim()); + lastDelimiterIndex = i; + } + break; + } + } + args.add(argsString.substring(lastDelimiterIndex + 1).trim()); // Add final argument. + + // Match args against valid options, otherwise they are column definitions. + HashSet options = new HashSet<>(); + for (String arg : args) { + for (String validOption : FTS_OPTIONS) { + if (arg.startsWith(validOption)) { + options.add(arg); + } + } + } + + return options; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FtsTableInfo)) return false; + + FtsTableInfo that = (FtsTableInfo) o; + + if (name != null ? !name.equals(that.name) : that.name != null) return false; + if (columns != null ? !columns.equals(that.columns) : that.columns != null) return false; + return options != null ? options.equals(that.options) : that.options == null; + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (columns != null ? columns.hashCode() : 0); + result = 31 * result + (options != null ? options.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "FtsTableInfo{" + + "name='" + name + '\'' + + ", columns=" + columns + + ", options=" + options + + '}'; + } +} diff --git a/app/src/main/java/androidx/room/InvalidationLiveDataContainer.java b/app/src/main/java/androidx/room/InvalidationLiveDataContainer.java new file mode 100644 index 0000000000..e1e8155f8a --- /dev/null +++ b/app/src/main/java/androidx/room/InvalidationLiveDataContainer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 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 androidx.room; + +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LiveData; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; +import java.util.concurrent.Callable; + +/** + * A helper class that maintains {@link RoomTrackingLiveData} instances for an + * {@link InvalidationTracker}. + *

    + * We keep a strong reference to active LiveData instances to avoid garbage collection in case + * developer does not hold onto the returned LiveData. + */ +class InvalidationLiveDataContainer { + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + final Set mLiveDataSet = Collections.newSetFromMap( + new IdentityHashMap() + ); + private final RoomDatabase mDatabase; + + InvalidationLiveDataContainer(RoomDatabase database) { + mDatabase = database; + } + + LiveData create(String[] tableNames, boolean inTransaction, + Callable computeFunction) { + return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction, + tableNames); + } + + void onActive(LiveData liveData) { + mLiveDataSet.add(liveData); + } + + void onInactive(LiveData liveData) { + mLiveDataSet.remove(liveData); + } +} diff --git a/app/src/main/java/androidx/room/InvalidationLiveDataContainer.kt b/app/src/main/java/androidx/room/InvalidationLiveDataContainer.kt deleted file mode 100644 index de7ae79b5b..0000000000 --- a/app/src/main/java/androidx/room/InvalidationLiveDataContainer.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2018 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 androidx.room - -import androidx.lifecycle.LiveData -import java.util.Collections -import java.util.IdentityHashMap -import java.util.concurrent.Callable - -/** - * A helper class that maintains [RoomTrackingLiveData] instances for an - * [InvalidationTracker]. - * - * We keep a strong reference to active LiveData instances to avoid garbage collection in case - * developer does not hold onto the returned LiveData. - */ -internal class InvalidationLiveDataContainer(private val database: RoomDatabase) { - internal val liveDataSet: MutableSet> = Collections.newSetFromMap(IdentityHashMap()) - - fun create( - tableNames: Array, - inTransaction: Boolean, - computeFunction: Callable - ): LiveData { - return RoomTrackingLiveData( - database, - this, - inTransaction, - computeFunction, - tableNames - ) - } - - fun onActive(liveData: LiveData<*>) { - liveDataSet.add(liveData) - } - - fun onInactive(liveData: LiveData<*>) { - liveDataSet.remove(liveData) - } -} diff --git a/app/src/main/java/androidx/room/InvalidationTracker.java b/app/src/main/java/androidx/room/InvalidationTracker.java new file mode 100644 index 0000000000..99e69154b1 --- /dev/null +++ b/app/src/main/java/androidx/room/InvalidationTracker.java @@ -0,0 +1,902 @@ +/* + * 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 androidx.room; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import androidx.arch.core.internal.SafeIterableMap; +import androidx.lifecycle.LiveData; +import androidx.sqlite.db.SimpleSQLiteQuery; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteStatement; + +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; + +/** + * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about + * these tables. + */ +// Some details on how the InvalidationTracker works: +// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from +// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated. +// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for). +// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states. +// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated +// tables. +// * Each update (write operation) on one of the observed tables triggers an update into the +// memory table table, flipping the invalidated flag ON. +// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created. +// It works as an Observer, and notifies other instances of table invalidation. +public class InvalidationTracker { + + private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"}; + + private static final String UPDATE_TABLE_NAME = "room_table_modification_log"; + + private static final String TABLE_ID_COLUMN_NAME = "table_id"; + + private static final String INVALIDATED_COLUMN_NAME = "invalidated"; + + private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME + + "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, " + + INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)"; + + @VisibleForTesting + static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME + + " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 "; + + @VisibleForTesting + static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME + + " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;"; + + @NonNull + final HashMap mTableIdLookup; + final String[] mTableNames; + + @NonNull + private Map> mViewTables; + + @Nullable + AutoCloser mAutoCloser = null; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + final RoomDatabase mDatabase; + + AtomicBoolean mPendingRefresh = new AtomicBoolean(false); + + private volatile boolean mInitialized = false; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + volatile SupportSQLiteStatement mCleanupStatement; + + private final ObservedTableTracker mObservedTableTracker; + + private final InvalidationLiveDataContainer mInvalidationLiveDataContainer; + + // should be accessed with synchronization only. + @VisibleForTesting + @SuppressLint("RestrictedApi") + final SafeIterableMap mObserverMap = new SafeIterableMap<>(); + + private MultiInstanceInvalidationClient mMultiInstanceInvalidationClient; + + private final Object mSyncTriggersLock = new Object(); + + /** + * Used by the generated code. + * + * @hide + */ + @SuppressWarnings("WeakerAccess") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public InvalidationTracker(RoomDatabase database, String... tableNames) { + this(database, new HashMap(), Collections.>emptyMap(), + tableNames); + } + + /** + * Used by the generated code. + * + * @hide + */ + @SuppressWarnings("WeakerAccess") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public InvalidationTracker(RoomDatabase database, Map shadowTablesMap, + Map> viewTables, String... tableNames) { + mDatabase = database; + mObservedTableTracker = new ObservedTableTracker(tableNames.length); + mTableIdLookup = new HashMap<>(); + mViewTables = viewTables; + mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase); + final int size = tableNames.length; + mTableNames = new String[size]; + for (int id = 0; id < size; id++) { + final String tableName = tableNames[id].toLowerCase(Locale.US); + mTableIdLookup.put(tableName, id); + String shadowTableName = shadowTablesMap.get(tableNames[id]); + if (shadowTableName != null) { + mTableNames[id] = shadowTableName.toLowerCase(Locale.US); + } else { + mTableNames[id] = tableName; + } + } + // Adjust table id lookup for those tables whose shadow table is another already mapped + // table (e.g. external content fts tables). + for (Map.Entry shadowTableEntry : shadowTablesMap.entrySet()) { + String shadowTableName = shadowTableEntry.getValue().toLowerCase(Locale.US); + if (mTableIdLookup.containsKey(shadowTableName)) { + String tableName = shadowTableEntry.getKey().toLowerCase(Locale.US); + mTableIdLookup.put(tableName, mTableIdLookup.get(shadowTableName)); + } + } + } + + /** + * Sets the auto closer for this invalidation tracker so that the invalidation tracker can + * ensure that the database is not closed if there are pending invalidations that haven't yet + * been flushed. + * + * This also adds a callback to the autocloser to ensure that the InvalidationTracker is in + * an ok state once the table is invalidated. + * + * This must be called before the database is used. + * + * @param autoCloser the autocloser associated with the db + */ + void setAutoCloser(AutoCloser autoCloser) { + this.mAutoCloser = autoCloser; + mAutoCloser.setAutoCloseCallback(this::onAutoCloseCallback); + } + + /** + * Internal method to initialize table tracking. + *

    + * You should never call this method, it is called by the generated code. + */ + void internalInit(SupportSQLiteDatabase database) { + synchronized (this) { + if (mInitialized) { + Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/."); + return; + } + + // These actions are not in a transaction because temp_store is not allowed to be + // performed on a transaction, and recursive_triggers is not affected by transactions. + database.execSQL("PRAGMA temp_store = MEMORY;"); + database.execSQL("PRAGMA recursive_triggers='ON';"); + database.execSQL(CREATE_TRACKING_TABLE_SQL); + syncTriggers(database); + mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL); + mInitialized = true; + } + } + + void onAutoCloseCallback() { + synchronized (this) { + mInitialized = false; + mObservedTableTracker.resetTriggerState(); + } + } + + void startMultiInstanceInvalidation(Context context, String name, Intent serviceIntent) { + mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, + serviceIntent, this, mDatabase.getQueryExecutor()); + } + + void stopMultiInstanceInvalidation() { + if (mMultiInstanceInvalidationClient != null) { + mMultiInstanceInvalidationClient.stop(); + mMultiInstanceInvalidationClient = null; + } + } + + private static void appendTriggerName(StringBuilder builder, String tableName, + String triggerType) { + builder.append("`") + .append("room_table_modification_trigger_") + .append(tableName) + .append("_") + .append(triggerType) + .append("`"); + } + + private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) { + final String tableName = mTableNames[tableId]; + StringBuilder stringBuilder = new StringBuilder(); + for (String trigger : TRIGGERS) { + stringBuilder.setLength(0); + stringBuilder.append("DROP TRIGGER IF EXISTS "); + appendTriggerName(stringBuilder, tableName, trigger); + writableDb.execSQL(stringBuilder.toString()); + } + } + + private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) { + writableDb.execSQL( + "INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)"); + final String tableName = mTableNames[tableId]; + StringBuilder stringBuilder = new StringBuilder(); + for (String trigger : TRIGGERS) { + stringBuilder.setLength(0); + stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS "); + appendTriggerName(stringBuilder, tableName, trigger); + stringBuilder.append(" AFTER ") + .append(trigger) + .append(" ON `") + .append(tableName) + .append("` BEGIN UPDATE ") + .append(UPDATE_TABLE_NAME) + .append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1") + .append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId) + .append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0") + .append("; END"); + writableDb.execSQL(stringBuilder.toString()); + } + } + + /** + * Adds the given observer to the observers list and it will be notified if any table it + * observes changes. + *

    + * Database changes are pulled on another thread so in some race conditions, the observer might + * be invoked for changes that were done before it is added. + *

    + * If the observer already exists, this is a no-op call. + *

    + * If one of the tables in the Observer does not exist in the database, this method throws an + * {@link IllegalArgumentException}. + *

    + * This method should be called on a background/worker thread as it performs database + * operations. + * + * @param observer The observer which listens the database for changes. + */ + @SuppressLint("RestrictedApi") + @WorkerThread + public void addObserver(@NonNull Observer observer) { + final String[] tableNames = resolveViews(observer.mTables); + int[] tableIds = new int[tableNames.length]; + final int size = tableNames.length; + + for (int i = 0; i < size; i++) { + Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US)); + if (tableId == null) { + throw new IllegalArgumentException("There is no table with name " + tableNames[i]); + } + tableIds[i] = tableId; + } + ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames); + ObserverWrapper currentObserver; + synchronized (mObserverMap) { + currentObserver = mObserverMap.putIfAbsent(observer, wrapper); + } + if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) { + syncTriggers(); + } + } + + private String[] validateAndResolveTableNames(String[] tableNames) { + String[] resolved = resolveViews(tableNames); + for (String tableName : resolved) { + if (!mTableIdLookup.containsKey(tableName.toLowerCase(Locale.US))) { + throw new IllegalArgumentException("There is no table with name " + tableName); + } + } + return resolved; + } + + /** + * Resolves the list of tables and views into a list of unique tables that are underlying them. + * + * @param names The names of tables or views. + * @return The names of the underlying tables. + */ + private String[] resolveViews(String[] names) { + Set tables = new HashSet<>(); + for (String name : names) { + final String lowercase = name.toLowerCase(Locale.US); + if (mViewTables.containsKey(lowercase)) { + tables.addAll(mViewTables.get(lowercase)); + } else { + tables.add(name); + } + } + return tables.toArray(new String[tables.size()]); + } + + private static void beginTransactionInternal(SupportSQLiteDatabase database) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && database.isWriteAheadLoggingEnabled()) { + database.beginTransactionNonExclusive(); + } else { + database.beginTransaction(); + } + } + + /** + * Adds an observer but keeps a weak reference back to it. + *

    + * Note that you cannot remove this observer once added. It will be automatically removed + * when the observer is GC'ed. + * + * @param observer The observer to which InvalidationTracker will keep a weak reference. + * @hide + */ + @SuppressWarnings("unused") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public void addWeakObserver(Observer observer) { + addObserver(new WeakObserver(this, observer)); + } + + /** + * Removes the observer from the observers list. + *

    + * This method should be called on a background/worker thread as it performs database + * operations. + * + * @param observer The observer to remove. + */ + @SuppressLint("RestrictedApi") + @SuppressWarnings("WeakerAccess") + @WorkerThread + public void removeObserver(@NonNull final Observer observer) { + ObserverWrapper wrapper; + synchronized (mObserverMap) { + wrapper = mObserverMap.remove(observer); + } + if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) { + syncTriggers(); + } + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + boolean ensureInitialization() { + if (!mDatabase.isOpen()) { + return false; + } + if (!mInitialized) { + // trigger initialization + mDatabase.getOpenHelper().getWritableDatabase(); + } + if (!mInitialized) { + Log.e(Room.LOG_TAG, "database is not initialized even though it is open"); + return false; + } + return true; + } + + @VisibleForTesting + Runnable mRefreshRunnable = new Runnable() { + @Override + public void run() { + final Lock closeLock = mDatabase.getCloseLock(); + Set invalidatedTableIds = null; + closeLock.lock(); + try { + + if (!ensureInitialization()) { + return; + } + + if (!mPendingRefresh.compareAndSet(true, false)) { + // no pending refresh + return; + } + + if (mDatabase.inTransaction()) { + // current thread is in a transaction. when it ends, it will invoke + // refreshRunnable again. mPendingRefresh is left as false on purpose + // so that the last transaction can flip it on again. + return; + } + + // This transaction has to be on the underlying DB rather than the RoomDatabase + // in order to avoid a recursive loop after endTransaction. + SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + invalidatedTableIds = checkUpdatedTable(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } catch (IllegalStateException | SQLiteException exception) { + // may happen if db is closed. just log. + Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", + exception); + } finally { + closeLock.unlock(); + + if (mAutoCloser != null) { + mAutoCloser.decrementCountAndScheduleClose(); + } + } + if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) { + synchronized (mObserverMap) { + for (Map.Entry entry : mObserverMap) { + entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds); + } + } + } + } + + private Set checkUpdatedTable() { + HashSet invalidatedTableIds = new HashSet<>(); + Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)); + //noinspection TryFinallyCanBeTryWithResources + try { + while (cursor.moveToNext()) { + final int tableId = cursor.getInt(0); + invalidatedTableIds.add(tableId); + } + } catch (Throwable ex) { + eu.faircode.email.Log.w(ex); + } finally { + cursor.close(); + } + if (!invalidatedTableIds.isEmpty()) { + mCleanupStatement.executeUpdateDelete(); + } + return invalidatedTableIds; + } + }; + + /** + * Enqueues a task to refresh the list of updated tables. + *

    + * This method is automatically called when {@link RoomDatabase#endTransaction()} is called but + * if you have another connection to the database or directly use {@link + * SupportSQLiteDatabase}, you may need to call this manually. + */ + @SuppressWarnings("WeakerAccess") + public void refreshVersionsAsync() { + // TODO we should consider doing this sync instead of async. + if (mPendingRefresh.compareAndSet(false, true)) { + if (mAutoCloser != null) { + // refreshVersionsAsync is called with the ref count incremented from + // RoomDatabase, so the db can't be closed here, but we need to be sure that our + // db isn't closed until refresh is completed. This increment call must be + // matched with a corresponding call in mRefreshRunnable. + mAutoCloser.incrementCountAndEnsureDbIsOpen(); + } + mDatabase.getQueryExecutor().execute(mRefreshRunnable); + } + } + + /** + * Check versions for tables, and run observers synchronously if tables have been updated. + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + @WorkerThread + public void refreshVersionsSync() { + if (mAutoCloser != null) { + // This increment call must be matched with a corresponding call in mRefreshRunnable. + mAutoCloser.incrementCountAndEnsureDbIsOpen(); + } + syncTriggers(); + mRefreshRunnable.run(); + } + + /** + * Notifies all the registered {@link Observer}s of table changes. + *

    + * This can be used for notifying invalidation that cannot be detected by this + * {@link InvalidationTracker}, for example, invalidation from another process. + * + * @param tables The invalidated tables. + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY) + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + public void notifyObserversByTableNames(String... tables) { + synchronized (mObserverMap) { + for (Map.Entry entry : mObserverMap) { + if (!entry.getKey().isRemote()) { + entry.getValue().notifyByTableNames(tables); + } + } + } + } + + void syncTriggers(SupportSQLiteDatabase database) { + if (database.inTransaction()) { + // we won't run this inside another transaction. + return; + } + try { + Lock closeLock = mDatabase.getCloseLock(); + closeLock.lock(); + try { + // Serialize adding and removing table trackers, this is specifically important + // to avoid missing invalidation before a transaction starts but there are + // pending (possibly concurrent) observer changes. + synchronized (mSyncTriggersLock) { + final int[] tablesToSync = mObservedTableTracker.getTablesToSync(); + if (tablesToSync == null) { + return; + } + final int limit = tablesToSync.length; + beginTransactionInternal(database); + try { + for (int tableId = 0; tableId < limit; tableId++) { + switch (tablesToSync[tableId]) { + case ObservedTableTracker.ADD: + startTrackingTable(database, tableId); + break; + case ObservedTableTracker.REMOVE: + stopTrackingTable(database, tableId); + break; + } + } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + } + } finally { + closeLock.unlock(); + } + } catch (IllegalStateException | SQLiteException exception) { + // may happen if db is closed. just log. + Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", + exception); + } + } + + /** + * Called by RoomDatabase before each beginTransaction call. + *

    + * It is important that pending trigger changes are applied to the database before any query + * runs. Otherwise, we may miss some changes. + *

    + * This api should eventually be public. + */ + void syncTriggers() { + if (!mDatabase.isOpen()) { + return; + } + syncTriggers(mDatabase.getOpenHelper().getWritableDatabase()); + } + + /** + * Creates a LiveData that computes the given function once and for every other invalidation + * of the database. + *

    + * Holds a strong reference to the created LiveData as long as it is active. + * + * @deprecated Use {@link #createLiveData(String[], boolean, Callable)} + * + * @param computeFunction The function that calculates the value + * @param tableNames The list of tables to observe + * @param The return type + * @return A new LiveData that computes the given function when the given list of tables + * invalidates. + * @hide + */ + @Deprecated + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public LiveData createLiveData(String[] tableNames, Callable computeFunction) { + return createLiveData(tableNames, false, computeFunction); + } + + /** + * Creates a LiveData that computes the given function once and for every other invalidation + * of the database. + *

    + * Holds a strong reference to the created LiveData as long as it is active. + * + * @param tableNames The list of tables to observe + * @param inTransaction True if the computeFunction will be done in a transaction, false + * otherwise. + * @param computeFunction The function that calculates the value + * @param The return type + * @return A new LiveData that computes the given function when the given list of tables + * invalidates. + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public LiveData createLiveData(String[] tableNames, boolean inTransaction, + Callable computeFunction) { + return mInvalidationLiveDataContainer.create( + validateAndResolveTableNames(tableNames), inTransaction, computeFunction); + } + + /** + * Wraps an observer and keeps the table information. + *

    + * Internally table ids are used which may change from database to database so the table + * related information is kept here rather than in the Observer. + */ + @SuppressWarnings("WeakerAccess") + static class ObserverWrapper { + final int[] mTableIds; + private final String[] mTableNames; + final Observer mObserver; + private final Set mSingleTableSet; + + ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) { + mObserver = observer; + mTableIds = tableIds; + mTableNames = tableNames; + if (tableIds.length == 1) { + HashSet set = new HashSet<>(); + set.add(mTableNames[0]); + mSingleTableSet = Collections.unmodifiableSet(set); + } else { + mSingleTableSet = null; + } + } + + /** + * Notifies the underlying {@link #mObserver} if any of the observed tables are invalidated + * based on the given invalid status set. + * + * @param invalidatedTablesIds The table ids of the tables that are invalidated. + */ + void notifyByTableInvalidStatus(Set invalidatedTablesIds) { + Set invalidatedTables = null; + final int size = mTableIds.length; + for (int index = 0; index < size; index++) { + final int tableId = mTableIds[index]; + if (invalidatedTablesIds.contains(tableId)) { + if (size == 1) { + // Optimization for a single-table observer + invalidatedTables = mSingleTableSet; + } else { + if (invalidatedTables == null) { + invalidatedTables = new HashSet<>(size); + } + invalidatedTables.add(mTableNames[index]); + } + } + } + if (invalidatedTables != null) { + mObserver.onInvalidated(invalidatedTables); + } + } + + /** + * Notifies the underlying {@link #mObserver} if it observes any of the specified + * {@code tables}. + * + * @param tables The invalidated table names. + */ + void notifyByTableNames(String[] tables) { + Set invalidatedTables = null; + if (mTableNames.length == 1) { + for (String table : tables) { + if (table.equalsIgnoreCase(mTableNames[0])) { + // Optimization for a single-table observer + invalidatedTables = mSingleTableSet; + break; + } + } + } else { + HashSet set = new HashSet<>(); + for (String table : tables) { + for (String ourTable : mTableNames) { + if (ourTable.equalsIgnoreCase(table)) { + set.add(ourTable); + break; + } + } + } + if (set.size() > 0) { + invalidatedTables = set; + } + } + if (invalidatedTables != null) { + mObserver.onInvalidated(invalidatedTables); + } + } + } + + /** + * An observer that can listen for changes in the database. + */ + public abstract static class Observer { + final String[] mTables; + + /** + * Observes the given list of tables and views. + * + * @param firstTable The name of the table or view. + * @param rest More names of tables or views. + */ + @SuppressWarnings("unused") + protected Observer(@NonNull String firstTable, String... rest) { + mTables = Arrays.copyOf(rest, rest.length + 1); + mTables[rest.length] = firstTable; + } + + /** + * Observes the given list of tables and views. + * + * @param tables The list of tables or views to observe for changes. + */ + public Observer(@NonNull String[] tables) { + // copy tables in case user modifies them afterwards + mTables = Arrays.copyOf(tables, tables.length); + } + + /** + * Called when one of the observed tables is invalidated in the database. + * + * @param tables A set of invalidated tables. This is useful when the observer targets + * multiple tables and you want to know which table is invalidated. This will + * be names of underlying tables when you are observing views. + */ + public abstract void onInvalidated(@NonNull Set tables); + + boolean isRemote() { + return false; + } + } + + /** + * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/ + * triggers in the database. + *

    + * This class is thread safe + */ + static class ObservedTableTracker { + static final int NO_OP = 0; // don't change trigger state for this table + static final int ADD = 1; // add triggers for this table + static final int REMOVE = 2; // remove triggers for this table + + // number of observers per table + final long[] mTableObservers; + // trigger state for each table at last sync + // this field is updated when syncAndGet is called. + final boolean[] mTriggerStates; + // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP + final int[] mTriggerStateChanges; + + boolean mNeedsSync; + + ObservedTableTracker(int tableCount) { + mTableObservers = new long[tableCount]; + mTriggerStates = new boolean[tableCount]; + mTriggerStateChanges = new int[tableCount]; + Arrays.fill(mTableObservers, 0); + Arrays.fill(mTriggerStates, false); + } + + /** + * @return true if # of triggers is affected. + */ + boolean onAdded(int... tableIds) { + boolean needTriggerSync = false; + synchronized (this) { + for (int tableId : tableIds) { + final long prevObserverCount = mTableObservers[tableId]; + mTableObservers[tableId] = prevObserverCount + 1; + if (prevObserverCount == 0) { + mNeedsSync = true; + needTriggerSync = true; + } + } + } + return needTriggerSync; + } + + /** + * @return true if # of triggers is affected. + */ + boolean onRemoved(int... tableIds) { + boolean needTriggerSync = false; + synchronized (this) { + for (int tableId : tableIds) { + final long prevObserverCount = mTableObservers[tableId]; + mTableObservers[tableId] = prevObserverCount - 1; + if (prevObserverCount == 1) { + mNeedsSync = true; + needTriggerSync = true; + } + } + } + return needTriggerSync; + } + + /** + * If we are re-opening the db we'll need to add all the triggers that we need so change + * the current state to false for all. + */ + void resetTriggerState() { + synchronized (this) { + Arrays.fill(mTriggerStates, false); + mNeedsSync = true; + } + } + + /** + * If this returns non-null there are no pending sync operations. + * + * @return int[] An int array where the index for each tableId has the action for that + * table. + */ + @Nullable + int[] getTablesToSync() { + synchronized (this) { + if (!mNeedsSync) { + return null; + } + final int tableCount = mTableObservers.length; + for (int i = 0; i < tableCount; i++) { + final boolean newState = mTableObservers[i] > 0; + if (newState != mTriggerStates[i]) { + mTriggerStateChanges[i] = newState ? ADD : REMOVE; + } else { + mTriggerStateChanges[i] = NO_OP; + } + mTriggerStates[i] = newState; + } + mNeedsSync = false; + return mTriggerStateChanges.clone(); + } + } + } + + /** + * An Observer wrapper that keeps a weak reference to the given object. + *

    + * This class will automatically unsubscribe when the wrapped observer goes out of memory. + */ + static class WeakObserver extends Observer { + final InvalidationTracker mTracker; + final WeakReference mDelegateRef; + + WeakObserver(InvalidationTracker tracker, Observer delegate) { + super(delegate.mTables); + mTracker = tracker; + mDelegateRef = new WeakReference<>(delegate); + } + + @Override + public void onInvalidated(@NonNull Set tables) { + final Observer observer = mDelegateRef.get(); + if (observer == null) { + mTracker.removeObserver(this); + } else { + observer.onInvalidated(tables); + } + } + } +} diff --git a/app/src/main/java/androidx/room/InvalidationTracker.kt b/app/src/main/java/androidx/room/InvalidationTracker.kt deleted file mode 100644 index 38067b702f..0000000000 --- a/app/src/main/java/androidx/room/InvalidationTracker.kt +++ /dev/null @@ -1,839 +0,0 @@ -/* - * 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 androidx.room - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.database.sqlite.SQLiteException -import android.os.Build -import android.util.Log -import androidx.annotation.GuardedBy -import androidx.annotation.RestrictTo -import androidx.annotation.VisibleForTesting -import androidx.annotation.WorkerThread -import androidx.arch.core.internal.SafeIterableMap -import androidx.lifecycle.LiveData -import androidx.room.Room.LOG_TAG -import androidx.room.util.useCursor -import androidx.sqlite.db.SimpleSQLiteQuery -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteStatement -import java.lang.ref.WeakReference -import java.util.Arrays -import java.util.Locale -import java.util.concurrent.Callable -import java.util.concurrent.atomic.AtomicBoolean - -/** - * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about - * these tables. - */ -// Some details on how the InvalidationTracker works: -// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from -// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated. -// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for). -// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states. -// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated -// tables. -// * Each update (write operation) on one of the observed tables triggers an update into the -// memory table table, flipping the invalidated flag ON. -// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created. -// It works as an Observer, and notifies other instances of table invalidation. -open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) constructor( - internal val database: RoomDatabase, - private val shadowTablesMap: Map, - private val viewTables: Map>, - vararg tableNames: String -) { - internal val tableIdLookup: Map - internal val tablesNames: Array - - private var autoCloser: AutoCloser? = null - - @get:RestrictTo(RestrictTo.Scope.LIBRARY) - @field:RestrictTo(RestrictTo.Scope.LIBRARY) - val pendingRefresh = AtomicBoolean(false) - - @Volatile - private var initialized = false - - @Volatile - internal var cleanupStatement: SupportSQLiteStatement? = null - - private val observedTableTracker: ObservedTableTracker = ObservedTableTracker(tableNames.size) - - private val invalidationLiveDataContainer: InvalidationLiveDataContainer = - InvalidationLiveDataContainer(database) - - @GuardedBy("observerMap") - internal val observerMap = SafeIterableMap() - - private var multiInstanceInvalidationClient: MultiInstanceInvalidationClient? = null - - private val syncTriggersLock = Any() - - private val trackerLock = Any() - - init { - tableIdLookup = mutableMapOf() - tablesNames = Array(tableNames.size) { id -> - val tableName = tableNames[id].lowercase(Locale.US) - tableIdLookup[tableName] = id - val shadowTableName = shadowTablesMap[tableNames[id]]?.lowercase(Locale.US) - shadowTableName ?: tableName - } - - // Adjust table id lookup for those tables whose shadow table is another already mapped - // table (e.g. external content fts tables). - shadowTablesMap.forEach { entry -> - val shadowTableName = entry.value.lowercase(Locale.US) - if (tableIdLookup.containsKey(shadowTableName)) { - val tableName = entry.key.lowercase(Locale.US) - tableIdLookup[tableName] = tableIdLookup.getValue(shadowTableName) - } - } - } - - /** - * Used by the generated code. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - constructor(database: RoomDatabase, vararg tableNames: String) : - this( - database = database, - shadowTablesMap = emptyMap(), - viewTables = emptyMap(), - tableNames = tableNames - ) - - /** - * Sets the auto closer for this invalidation tracker so that the invalidation tracker can - * ensure that the database is not closed if there are pending invalidations that haven't yet - * been flushed. - * - * This also adds a callback to the autocloser to ensure that the InvalidationTracker is in - * an ok state once the table is invalidated. - * - * This must be called before the database is used. - * - * @param autoCloser the autocloser associated with the db - */ - internal fun setAutoCloser(autoCloser: AutoCloser) { - this.autoCloser = autoCloser - autoCloser.setAutoCloseCallback(::onAutoCloseCallback) - } - - /** - * Internal method to initialize table tracking. - * - * You should never call this method, it is called by the generated code. - */ - internal fun internalInit(database: SupportSQLiteDatabase) { - synchronized(trackerLock) { - if (initialized) { - Log.e(LOG_TAG, "Invalidation tracker is initialized twice :/.") - return - } - - // These actions are not in a transaction because temp_store is not allowed to be - // performed on a transaction, and recursive_triggers is not affected by transactions. - database.execSQL("PRAGMA temp_store = MEMORY;") - database.execSQL("PRAGMA recursive_triggers='ON';") - database.execSQL(CREATE_TRACKING_TABLE_SQL) - syncTriggers(database) - cleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL) - initialized = true - } - } - - private fun onAutoCloseCallback() { - synchronized(trackerLock) { - initialized = false - observedTableTracker.resetTriggerState() - cleanupStatement?.close() - } - } - - internal fun startMultiInstanceInvalidation( - context: Context, - name: String, - serviceIntent: Intent - ) { - multiInstanceInvalidationClient = MultiInstanceInvalidationClient( - context = context, - name = name, - serviceIntent = serviceIntent, - invalidationTracker = this, - executor = database.queryExecutor - ) - } - - internal fun stopMultiInstanceInvalidation() { - multiInstanceInvalidationClient?.stop() - multiInstanceInvalidationClient = null - } - - private fun stopTrackingTable(db: SupportSQLiteDatabase, tableId: Int) { - val tableName = tablesNames[tableId] - for (trigger in TRIGGERS) { - val sql = buildString { - append("DROP TRIGGER IF EXISTS ") - append(getTriggerName(tableName, trigger)) - } - db.execSQL(sql) - } - } - - private fun startTrackingTable(db: SupportSQLiteDatabase, tableId: Int) { - db.execSQL( - "INSERT OR IGNORE INTO $UPDATE_TABLE_NAME VALUES($tableId, 0)" - ) - val tableName = tablesNames[tableId] - for (trigger in TRIGGERS) { - val sql = buildString { - append("CREATE TEMP TRIGGER IF NOT EXISTS ") - append(getTriggerName(tableName, trigger)) - append(" AFTER ") - append(trigger) - append(" ON `") - append(tableName) - append("` BEGIN UPDATE ") - append(UPDATE_TABLE_NAME) - append(" SET ").append(INVALIDATED_COLUMN_NAME) - append(" = 1") - append(" WHERE ").append(TABLE_ID_COLUMN_NAME) - append(" = ").append(tableId) - append(" AND ").append(INVALIDATED_COLUMN_NAME) - append(" = 0") - append("; END") - } - db.execSQL(sql) - } - } - - /** - * Adds the given observer to the observers list and it will be notified if any table it - * observes changes. - * - * Database changes are pulled on another thread so in some race conditions, the observer might - * be invoked for changes that were done before it is added. - * - * If the observer already exists, this is a no-op call. - * - * If one of the tables in the Observer does not exist in the database, this method throws an - * [IllegalArgumentException]. - * - * This method should be called on a background/worker thread as it performs database - * operations. - * - * @param observer The observer which listens the database for changes. - */ - @SuppressLint("RestrictedApi") - @WorkerThread - open fun addObserver(observer: Observer) { - val tableNames = resolveViews(observer.tables) - val tableIds = tableNames.map { tableName -> - tableIdLookup[tableName.lowercase(Locale.US)] - ?: throw IllegalArgumentException("There is no table with name $tableName") - }.toIntArray() - - val wrapper = ObserverWrapper( - observer = observer, - tableIds = tableIds, - tableNames = tableNames - ) - - val currentObserver = synchronized(observerMap) { - observerMap.putIfAbsent(observer, wrapper) - } - if (currentObserver == null && observedTableTracker.onAdded(*tableIds)) { - syncTriggers() - } - } - - private fun validateAndResolveTableNames(tableNames: Array): Array { - val resolved = resolveViews(tableNames) - resolved.forEach { tableName -> - require(tableIdLookup.containsKey(tableName.lowercase(Locale.US))) { - "There is no table with name $tableName" - } - } - return resolved - } - - /** - * Resolves the list of tables and views into a list of unique tables that are underlying them. - * - * @param names The names of tables or views. - * @return The names of the underlying tables. - */ - private fun resolveViews(names: Array): Array { - return buildSet { - names.forEach { name -> - if (viewTables.containsKey(name.lowercase(Locale.US))) { - addAll(viewTables[name.lowercase(Locale.US)]!!) - } else { - add(name) - } - } - }.toTypedArray() - } - - /** - * Adds an observer but keeps a weak reference back to it. - * - * Note that you cannot remove this observer once added. It will be automatically removed - * when the observer is GC'ed. - * - * @param observer The observer to which InvalidationTracker will keep a weak reference. - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - open fun addWeakObserver(observer: Observer) { - addObserver(WeakObserver(this, observer)) - } - - /** - * Removes the observer from the observers list. - * - * This method should be called on a background/worker thread as it performs database - * operations. - * - * @param observer The observer to remove. - */ - @SuppressLint("RestrictedApi") - @WorkerThread - open fun removeObserver(observer: Observer) { - val wrapper = synchronized(observerMap) { - observerMap.remove(observer) - } - if (wrapper != null && observedTableTracker.onRemoved(tableIds = wrapper.tableIds)) { - syncTriggers() - } - } - - internal fun ensureInitialization(): Boolean { - if (!database.isOpenInternal) { - return false - } - if (!initialized) { - // trigger initialization - database.openHelper.writableDatabase - } - if (!initialized) { - Log.e(LOG_TAG, "database is not initialized even though it is open") - return false - } - return true - } - - @VisibleForTesting - @JvmField - @RestrictTo(RestrictTo.Scope.LIBRARY) - val refreshRunnable: Runnable = object : Runnable { - override fun run() { - val closeLock = database.getCloseLock() - closeLock.lock() - val invalidatedTableIds: Set = - try { - if (!ensureInitialization()) { - return - } - if (!pendingRefresh.compareAndSet(true, false)) { - // no pending refresh - return - } - if (database.inTransaction()) { - // current thread is in a transaction. when it ends, it will invoke - // refreshRunnable again. pendingRefresh is left as false on purpose - // so that the last transaction can flip it on again. - return - } - - // This transaction has to be on the underlying DB rather than the RoomDatabase - // in order to avoid a recursive loop after endTransaction. - val db = database.openHelper.writableDatabase - db.beginTransactionNonExclusive() - val invalidatedTableIds: Set - try { - invalidatedTableIds = checkUpdatedTable() - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } - invalidatedTableIds - } catch (ex: IllegalStateException) { - // may happen if db is closed. just log. - Log.e( - LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", - ex - ) - emptySet() - } catch (ex: SQLiteException) { - Log.e( - LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", - ex - ) - emptySet() - } finally { - closeLock.unlock() - autoCloser?.decrementCountAndScheduleClose() - } - - if (invalidatedTableIds.isNotEmpty()) { - synchronized(observerMap) { - observerMap.forEach { - it.value.notifyByTableInvalidStatus(invalidatedTableIds) - } - } - } - } - - private fun checkUpdatedTable(): Set { - val invalidatedTableIds = buildSet { - database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor -> - while (cursor.moveToNext()) { - add(cursor.getInt(0)) - } - } - } - if (invalidatedTableIds.isNotEmpty()) { - checkNotNull(cleanupStatement) - val statement = cleanupStatement - requireNotNull(statement) - statement.executeUpdateDelete() - } - return invalidatedTableIds - } - } - - /** - * Enqueues a task to refresh the list of updated tables. - * - * This method is automatically called when [RoomDatabase.endTransaction] is called but - * if you have another connection to the database or directly use [ ], you may need to call this - * manually. - */ - open fun refreshVersionsAsync() { - // TODO we should consider doing this sync instead of async. - if (pendingRefresh.compareAndSet(false, true)) { - // refreshVersionsAsync is called with the ref count incremented from - // RoomDatabase, so the db can't be closed here, but we need to be sure that our - // db isn't closed until refresh is completed. This increment call must be - // matched with a corresponding call in refreshRunnable. - autoCloser?.incrementCountAndEnsureDbIsOpen() - database.queryExecutor.execute(refreshRunnable) - } - } - - /** - * Check versions for tables, and run observers synchronously if tables have been updated. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @WorkerThread - open fun refreshVersionsSync() { - // This increment call must be matched with a corresponding call in refreshRunnable. - autoCloser?.incrementCountAndEnsureDbIsOpen() - syncTriggers() - refreshRunnable.run() - } - - /** - * Notifies all the registered [Observer]s of table changes. - * - * This can be used for notifying invalidation that cannot be detected by this - * [InvalidationTracker], for example, invalidation from another process. - * - * @param tables The invalidated tables. - */ - @RestrictTo(RestrictTo.Scope.LIBRARY) - fun notifyObserversByTableNames(vararg tables: String) { - synchronized(observerMap) { - observerMap.forEach { (observer, wrapper) -> - if (!observer.isRemote) { - wrapper.notifyByTableNames(tables) - } - } - } - } - - internal fun syncTriggers(database: SupportSQLiteDatabase) { - if (database.inTransaction()) { - // we won't run this inside another transaction. - return - } - try { - val closeLock = this.database.getCloseLock() - closeLock.lock() - try { - // Serialize adding and removing table trackers, this is specifically important - // to avoid missing invalidation before a transaction starts but there are - // pending (possibly concurrent) observer changes. - synchronized(syncTriggersLock) { - val tablesToSync = observedTableTracker.getTablesToSync() ?: return - beginTransactionInternal(database) - try { - tablesToSync.forEachIndexed { tableId, syncState -> - when (syncState) { - ObservedTableTracker.ADD -> - startTrackingTable(database, tableId) - ObservedTableTracker.REMOVE -> - stopTrackingTable(database, tableId) - } - } - database.setTransactionSuccessful() - } finally { - database.endTransaction() - } - } - } finally { - closeLock.unlock() - } - } catch (ex: IllegalStateException) { - // may happen if db is closed. just log. - Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex) - } catch (ex: SQLiteException) { - Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex) - } - } - - /** - * Called by RoomDatabase before each beginTransaction call. - * - * It is important that pending trigger changes are applied to the database before any query - * runs. Otherwise, we may miss some changes. - * - * This api should eventually be public. - */ - internal fun syncTriggers() { - if (!database.isOpenInternal) { - return - } - syncTriggers(database.openHelper.writableDatabase) - } - - /** - * Creates a LiveData that computes the given function once and for every other invalidation - * of the database. - * - * Holds a strong reference to the created LiveData as long as it is active. - * - * @param computeFunction The function that calculates the value - * @param tableNames The list of tables to observe - * @param T The return type - * @return A new LiveData that computes the given function when the given list of tables - * invalidates. - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated("Use [createLiveData(String[], boolean, Callable)]") - open fun createLiveData( - tableNames: Array, - computeFunction: Callable - ): LiveData { - return createLiveData(tableNames, false, computeFunction) - } - - /** - * Creates a LiveData that computes the given function once and for every other invalidation - * of the database. - * - * Holds a strong reference to the created LiveData as long as it is active. - * - * @param tableNames The list of tables to observe - * @param inTransaction True if the computeFunction will be done in a transaction, false - * otherwise. - * @param computeFunction The function that calculates the value - * @param T The return type - * @return A new LiveData that computes the given function when the given list of tables - * invalidates. - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - open fun createLiveData( - tableNames: Array, - inTransaction: Boolean, - computeFunction: Callable - ): LiveData { - return invalidationLiveDataContainer.create( - validateAndResolveTableNames(tableNames), inTransaction, computeFunction - ) - } - - /** - * Wraps an observer and keeps the table information. - * - * Internally table ids are used which may change from database to database so the table - * related information is kept here rather than in the Observer. - */ - internal class ObserverWrapper( - internal val observer: Observer, - internal val tableIds: IntArray, - private val tableNames: Array - ) { - private val singleTableSet = if (tableNames.isNotEmpty()) { - setOf(tableNames[0]) - } else { - emptySet() - } - - init { - check(tableIds.size == tableNames.size) - } - - /** - * Notifies the underlying [.mObserver] if any of the observed tables are invalidated - * based on the given invalid status set. - * - * @param invalidatedTablesIds The table ids of the tables that are invalidated. - */ - internal fun notifyByTableInvalidStatus(invalidatedTablesIds: Set) { - val invalidatedTables = when (tableIds.size) { - 0 -> emptySet() - 1 -> if (invalidatedTablesIds.contains(tableIds[0])) { - singleTableSet // Optimization for a single-table observer - } else { - emptySet() - } - else -> buildSet { - tableIds.forEachIndexed { idx, tableId -> - if (invalidatedTablesIds.contains(tableId)) { - add(tableNames[idx]) - } - } - } - } - - if (invalidatedTables.isNotEmpty()) { - observer.onInvalidated(invalidatedTables) - } - } - - /** - * Notifies the underlying [.mObserver] if it observes any of the specified - * `tables`. - * - * @param tables The invalidated table names. - */ - internal fun notifyByTableNames(tables: Array) { - val invalidatedTables = when (tableNames.size) { - 0 -> emptySet() - 1 -> if (tables.any { it.equals(tableNames[0], ignoreCase = true) }) { - singleTableSet // Optimization for a single-table observer - } else { - emptySet() - } - else -> buildSet { - tables.forEach { table -> - tableNames.forEach ourTablesLoop@{ ourTable -> - if (ourTable.equals(table, ignoreCase = true)) { - add(ourTable) - return@ourTablesLoop - } - } - } - } - } - if (invalidatedTables.isNotEmpty()) { - observer.onInvalidated(invalidatedTables) - } - } - } - - /** - * An observer that can listen for changes in the database. - */ - abstract class Observer(internal val tables: Array) { - /** - * Observes the given list of tables and views. - * - * @param firstTable The name of the table or view. - * @param rest More names of tables or views. - */ - protected constructor(firstTable: String, vararg rest: String) : this( - buildList { - addAll(rest) - add(firstTable) - }.toTypedArray() - ) - - /** - * Called when one of the observed tables is invalidated in the database. - * - * @param tables A set of invalidated tables. This is useful when the observer targets - * multiple tables and you want to know which table is invalidated. This will - * be names of underlying tables when you are observing views. - */ - abstract fun onInvalidated(tables: Set) - - internal open val isRemote: Boolean - get() = false - } - - /** - * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/ - * triggers in the database. - * - * This class is thread safe - */ - internal class ObservedTableTracker(tableCount: Int) { - // number of observers per table - val tableObservers = LongArray(tableCount) - - // trigger state for each table at last sync - // this field is updated when syncAndGet is called. - private val triggerStates = BooleanArray(tableCount) - - // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP - private val triggerStateChanges = IntArray(tableCount) - - var needsSync = false - - /** - * @return true if # of triggers is affected. - */ - fun onAdded(vararg tableIds: Int): Boolean { - var needTriggerSync = false - synchronized(this) { - tableIds.forEach { tableId -> - val prevObserverCount = tableObservers[tableId] - tableObservers[tableId] = prevObserverCount + 1 - if (prevObserverCount == 0L) { - needsSync = true - needTriggerSync = true - } - } - } - return needTriggerSync - } - - /** - * @return true if # of triggers is affected. - */ - fun onRemoved(vararg tableIds: Int): Boolean { - var needTriggerSync = false - synchronized(this) { - tableIds.forEach { tableId -> - val prevObserverCount = tableObservers[tableId] - tableObservers[tableId] = prevObserverCount - 1 - if (prevObserverCount == 1L) { - needsSync = true - needTriggerSync = true - } - } - } - return needTriggerSync - } - - /** - * If we are re-opening the db we'll need to add all the triggers that we need so change - * the current state to false for all. - */ - fun resetTriggerState() { - synchronized(this) { - Arrays.fill(triggerStates, false) - needsSync = true - } - } - - /** - * If this returns non-null, you must call onSyncCompleted. - * - * @return int[] An int array where the index for each tableId has the action for that - * table. - */ - @VisibleForTesting - @JvmName("getTablesToSync") - fun getTablesToSync(): IntArray? { - synchronized(this) { - if (!needsSync) { - return null - } - tableObservers.forEachIndexed { i, observerCount -> - val newState = observerCount > 0 - if (newState != triggerStates[i]) { - triggerStateChanges[i] = if (newState) ADD else REMOVE - } else { - triggerStateChanges[i] = NO_OP - } - triggerStates[i] = newState - } - needsSync = false - return triggerStateChanges.clone() - } - } - - internal companion object { - const val NO_OP = 0 // don't change trigger state for this table - const val ADD = 1 // add triggers for this table - const val REMOVE = 2 // remove triggers for this table - } - } - - /** - * An Observer wrapper that keeps a weak reference to the given object. - * - * This class will automatically unsubscribe when the wrapped observer goes out of memory. - */ - internal class WeakObserver( - val tracker: InvalidationTracker, - delegate: Observer - ) : Observer(delegate.tables) { - val delegateRef: WeakReference = WeakReference(delegate) - override fun onInvalidated(tables: Set) { - val observer = delegateRef.get() - if (observer == null) { - tracker.removeObserver(this) - } else { - observer.onInvalidated(tables) - } - } - } - - companion object { - private val TRIGGERS = arrayOf("UPDATE", "DELETE", "INSERT") - private const val UPDATE_TABLE_NAME = "room_table_modification_log" - private const val TABLE_ID_COLUMN_NAME = "table_id" - private const val INVALIDATED_COLUMN_NAME = "invalidated" - private const val CREATE_TRACKING_TABLE_SQL = - "CREATE TEMP TABLE $UPDATE_TABLE_NAME ($TABLE_ID_COLUMN_NAME INTEGER PRIMARY KEY, " + - "$INVALIDATED_COLUMN_NAME INTEGER NOT NULL DEFAULT 0)" - - @VisibleForTesting - internal const val RESET_UPDATED_TABLES_SQL = - "UPDATE $UPDATE_TABLE_NAME SET $INVALIDATED_COLUMN_NAME = 0 " + - "WHERE $INVALIDATED_COLUMN_NAME = 1" - - @VisibleForTesting - internal const val SELECT_UPDATED_TABLES_SQL = - "SELECT * FROM $UPDATE_TABLE_NAME WHERE $INVALIDATED_COLUMN_NAME = 1;" - - internal fun getTriggerName( - tableName: String, - triggerType: String - ) = "`room_table_modification_trigger_${tableName}_$triggerType`" - - internal fun beginTransactionInternal(database: SupportSQLiteDatabase) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && - database.isWriteAheadLoggingEnabled - ) { - database.beginTransactionNonExclusive() - } else { - database.beginTransaction() - } - } - } -} diff --git a/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java b/app/src/main/java/androidx/room/LimitOffsetDataSource.java similarity index 98% rename from app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java rename to app/src/main/java/androidx/room/LimitOffsetDataSource.java index 2b5c391dbc..d3513473af 100644 --- a/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java +++ b/app/src/main/java/androidx/room/LimitOffsetDataSource.java @@ -46,6 +46,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * * @param Data type returned by the data source. * + * @hide */ @SuppressWarnings("deprecation") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @@ -116,8 +117,8 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona /** * Count number of rows query can return * + * @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY) @SuppressWarnings("WeakerAccess") public int countItems() { registerObserverIfNecessary(); @@ -153,7 +154,7 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona @NonNull LoadInitialCallback callback) { registerObserverIfNecessary(); List list = Collections.emptyList(); - int totalCount; + int totalCount = 0; int firstLoadPosition = 0; RoomSQLiteQuery sqLiteQuery = null; Cursor cursor = null; @@ -171,6 +172,8 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona mDb.setTransactionSuccessful(); list = rows; } + } catch (Throwable ex) { + eu.faircode.email.Log.w(ex); } finally { if (cursor != null) { cursor.close(); @@ -193,8 +196,8 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona /** * Return the rows from startPos to startPos + loadCount * + * @hide */ - @RestrictTo(RestrictTo.Scope.LIBRARY) @SuppressWarnings("deprecation") @NonNull public List loadRange(int startPosition, int loadCount) { diff --git a/app/src/main/java/androidx/room/migration/Migration.kt b/app/src/main/java/androidx/room/Migration.java similarity index 56% rename from app/src/main/java/androidx/room/migration/Migration.kt rename to app/src/main/java/androidx/room/Migration.java index 63b20d2241..4aa7a7e86a 100644 --- a/app/src/main/java/androidx/room/migration/Migration.kt +++ b/app/src/main/java/androidx/room/Migration.java @@ -13,42 +13,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.room.migration -import androidx.sqlite.db.SupportSQLiteDatabase +package androidx.room.migration; + +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteDatabase; /** * Base class for a database migration. - * - * Creates a new migration between [startVersion] and [endVersion]. - * - * Each migration can move between 2 versions that are defined by [startVersion] and - * [endVersion]. - * + *

    + * Each migration can move between 2 versions that are defined by {@link #startVersion} and + * {@link #endVersion}. + *

    * A migration can handle more than 1 version (e.g. if you have a faster path to choose when * going version 3 to 5 without going to version 4). If Room opens a database at version - * 3 and latest version is 5, Room will use the migration object that can migrate from + * 3 and latest version is >= 5, Room will use the migration object that can migrate from * 3 to 5 instead of 3 to 4 and 4 to 5. - * + *

    * If there are not enough migrations provided to move from the current version to the latest * version, Room will clear the database and recreate so even if you have no changes between 2 * versions, you should still provide a Migration object to the builder. */ -abstract class Migration( - @JvmField - val startVersion: Int, - @JvmField - val endVersion: Int -) { +public abstract class Migration { + public final int startVersion; + public final int endVersion; + + /** + * Creates a new migration between {@code startVersion} and {@code endVersion}. + * + * @param startVersion The start version of the database. + * @param endVersion The end version of the database after this migration is applied. + */ + public Migration(int startVersion, int endVersion) { + this.startVersion = startVersion; + this.endVersion = endVersion; + } + /** * Should run the necessary migrations. - * - * The Migration class cannot access any generated Dao in this method. - * + *

    + * This class cannot access any generated Dao in this method. + *

    * This method is already called inside a transaction and that transaction might actually be a - * composite transaction of all necessary `Migration`s. + * composite transaction of all necessary {@code Migration}s. * - * @param db The database instance + * @param database The database instance */ - abstract fun migrate(db: SupportSQLiteDatabase) + public abstract void migrate(@NonNull SupportSQLiteDatabase database); } diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java new file mode 100644 index 0000000000..cd847acc89 --- /dev/null +++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java @@ -0,0 +1,196 @@ +/* + * Copyright 2018 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 androidx.room; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to + * {@link MultiInstanceInvalidationService}. + */ +class MultiInstanceInvalidationClient { + + /** + * The application context. + */ + // synthetic access + @SuppressWarnings("WeakerAccess") + final Context mAppContext; + + /** + * The name of the database file. + */ + // synthetic access + @SuppressWarnings("WeakerAccess") + final String mName; + + /** + * The client ID assigned by {@link MultiInstanceInvalidationService}. + */ + // synthetic access + @SuppressWarnings("WeakerAccess") + int mClientId; + + // synthetic access + @SuppressWarnings("WeakerAccess") + final InvalidationTracker mInvalidationTracker; + + // synthetic access + @SuppressWarnings("WeakerAccess") + final InvalidationTracker.Observer mObserver; + + // synthetic access + @SuppressWarnings("WeakerAccess") + @Nullable + IMultiInstanceInvalidationService mService; + + // synthetic access + @SuppressWarnings("WeakerAccess") + final Executor mExecutor; + + // synthetic access + @SuppressWarnings("WeakerAccess") + final IMultiInstanceInvalidationCallback mCallback = + new IMultiInstanceInvalidationCallback.Stub() { + @Override + public void onInvalidation(final String[] tables) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mInvalidationTracker.notifyObserversByTableNames(tables); + } + }); + } + }; + + // synthetic access + @SuppressWarnings("WeakerAccess") + final AtomicBoolean mStopped = new AtomicBoolean(false); + + // synthetic access + @SuppressWarnings("WeakerAccess") + final ServiceConnection mServiceConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mService = IMultiInstanceInvalidationService.Stub.asInterface(service); + mExecutor.execute(mSetUpRunnable); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mExecutor.execute(mRemoveObserverRunnable); + mService = null; + } + + }; + + // synthetic access + @SuppressWarnings("WeakerAccess") + final Runnable mSetUpRunnable = new Runnable() { + @Override + public void run() { + try { + final IMultiInstanceInvalidationService service = mService; + if (service != null) { + mClientId = service.registerCallback(mCallback, mName); + mInvalidationTracker.addObserver(mObserver); + } + } catch (RemoteException e) { + Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e); + } + } + }; + + // synthetic access + @SuppressWarnings("WeakerAccess") + final Runnable mRemoveObserverRunnable = new Runnable() { + @Override + public void run() { + mInvalidationTracker.removeObserver(mObserver); + } + }; + + /** + * @param context The Context to be used for binding + * {@link IMultiInstanceInvalidationService}. + * @param name The name of the database file. + * @param serviceIntent The {@link Intent} used for binding + * {@link IMultiInstanceInvalidationService}. + * @param invalidationTracker The {@link InvalidationTracker} + * @param executor The background executor. + */ + MultiInstanceInvalidationClient(Context context, String name, Intent serviceIntent, + InvalidationTracker invalidationTracker, Executor executor) { + mAppContext = context.getApplicationContext(); + mName = name; + mInvalidationTracker = invalidationTracker; + mExecutor = executor; + // Use all tables names for observer. + final Set tableNames = invalidationTracker.mTableIdLookup.keySet(); + mObserver = new InvalidationTracker.Observer(tableNames.toArray(new String[0])) { + @Override + public void onInvalidated(@NonNull Set tables) { + if (mStopped.get()) { + return; + } + try { + final IMultiInstanceInvalidationService service = mService; + if (service != null) { + service.broadcastInvalidation(mClientId, tables.toArray(new String[0])); + } + } catch (RemoteException e) { + Log.w(Room.LOG_TAG, "Cannot broadcast invalidation", e); + } + } + + @Override + boolean isRemote() { + return true; + } + }; + mAppContext.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE); + } + + void stop() { + if (mStopped.compareAndSet(false, true)) { + mInvalidationTracker.removeObserver(mObserver); + try { + final IMultiInstanceInvalidationService service = mService; + if (service != null) { + service.unregisterCallback(mCallback, mClientId); + } + } catch (RemoteException e) { + Log.w(Room.LOG_TAG, "Cannot unregister multi-instance invalidation callback", e); + } + mAppContext.unbindService(mServiceConnection); + } + } +} diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt deleted file mode 100644 index fce2ec7676..0000000000 --- a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2018 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 androidx.room - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder -import android.os.RemoteException -import android.util.Log -import androidx.room.Room.LOG_TAG -import java.util.concurrent.Executor -import java.util.concurrent.atomic.AtomicBoolean - -/** - * Handles all the communication from [RoomDatabase] and [InvalidationTracker] to - * [MultiInstanceInvalidationService]. - * - * @param context The Context to be used for binding - * [IMultiInstanceInvalidationService]. - * @param name The name of the database file. - * @param serviceIntent The [Intent] used for binding - * [IMultiInstanceInvalidationService]. - * @param invalidationTracker The [InvalidationTracker] - * @param executor The background executor. - */ -internal class MultiInstanceInvalidationClient( - context: Context, - val name: String, - serviceIntent: Intent, - val invalidationTracker: InvalidationTracker, - val executor: Executor -) { - private val appContext = context.applicationContext - - /** - * The client ID assigned by [MultiInstanceInvalidationService]. - */ - var clientId = 0 - lateinit var observer: InvalidationTracker.Observer - var service: IMultiInstanceInvalidationService? = null - - val callback: IMultiInstanceInvalidationCallback = - object : IMultiInstanceInvalidationCallback.Stub() { - override fun onInvalidation(tables: Array) { - executor.execute { invalidationTracker.notifyObserversByTableNames(*tables) } - } - } - - val stopped = AtomicBoolean(false) - - val serviceConnection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, service: IBinder) { - this@MultiInstanceInvalidationClient.service = - IMultiInstanceInvalidationService.Stub.asInterface(service) - executor.execute(setUpRunnable) - } - - override fun onServiceDisconnected(name: ComponentName) { - executor.execute(removeObserverRunnable) - service = null - } - } - - val setUpRunnable = Runnable { - try { - service?.let { - clientId = it.registerCallback(callback, name) - invalidationTracker.addObserver(observer) - } - } catch (e: RemoteException) { - Log.w(LOG_TAG, "Cannot register multi-instance invalidation callback", e) - } - } - - val removeObserverRunnable = Runnable { invalidationTracker.removeObserver(observer) } - - init { - // Use all tables names for observer. - val tableNames: Set = invalidationTracker.tableIdLookup.keys - observer = object : InvalidationTracker.Observer(tableNames.toTypedArray()) { - override fun onInvalidated(tables: Set) { - if (stopped.get()) { - return - } - - try { - service?.broadcastInvalidation(clientId, tables.toTypedArray()) - } catch (e: RemoteException) { - Log.w(LOG_TAG, "Cannot broadcast invalidation", e) - } - } - - override val isRemote: Boolean - get() = true - } - appContext.bindService( - serviceIntent, - serviceConnection, - Context.BIND_AUTO_CREATE - ) - } - - fun stop() { - if (stopped.compareAndSet(false, true)) { - invalidationTracker.removeObserver(observer) - try { - service?.unregisterCallback(callback, clientId) - } catch (e: RemoteException) { - Log.w(LOG_TAG, "Cannot unregister multi-instance invalidation callback", e) - } - appContext.unbindService(serviceConnection) - } - } -} diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java b/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java new file mode 100644 index 0000000000..0c82f93fa0 --- /dev/null +++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java @@ -0,0 +1,137 @@ +/* + * Copyright 2018 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 androidx.room; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; + +/** + * A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances. + * This service runs in the main app process. All the instances of {@link InvalidationTracker} + * (potentially in other processes) has to connect to this service. + * + *

    The intent to launch it can be specified by + * {@link RoomDatabase.Builder#setMultiInstanceInvalidationServiceIntent}, although the service is + * defined in the manifest by default so there should be no need to override it in a normal + * situation. + */ +@ExperimentalRoomApi +public class MultiInstanceInvalidationService extends Service { + + // synthetic access + @SuppressWarnings("WeakerAccess") + int mMaxClientId = 0; + + // synthetic access + @SuppressWarnings("WeakerAccess") + final HashMap mClientNames = new HashMap<>(); + + // synthetic access + @SuppressWarnings("WeakerAccess") + final RemoteCallbackList mCallbackList = + new RemoteCallbackList() { + @Override + public void onCallbackDied(IMultiInstanceInvalidationCallback callback, + Object cookie) { + mClientNames.remove((int) cookie); + } + }; + + private final IMultiInstanceInvalidationService.Stub mBinder = + new IMultiInstanceInvalidationService.Stub() { + + // Assigns a client ID to the client. + @Override + public int registerCallback(IMultiInstanceInvalidationCallback callback, + String name) { + if (name == null) { + return 0; + } + synchronized (mCallbackList) { + int clientId = ++mMaxClientId; + // Use the client ID as the RemoteCallbackList cookie. + if (mCallbackList.register(callback, clientId)) { + mClientNames.put(clientId, name); + return clientId; + } else { + --mMaxClientId; + return 0; + } + } + } + + // Explicitly removes the client. + // The client can die without calling this. In that case, mCallbackList + // .onCallbackDied() can take care of removal. + @Override + public void unregisterCallback(IMultiInstanceInvalidationCallback callback, + int clientId) { + synchronized (mCallbackList) { + mCallbackList.unregister(callback); + mClientNames.remove(clientId); + } + } + + // Broadcasts table invalidation to other instances of the same database file. + // The broadcast is not sent to the caller itself. + @Override + public void broadcastInvalidation(int clientId, String[] tables) { + synchronized (mCallbackList) { + String name = mClientNames.get(clientId); + if (name == null) { + Log.w(Room.LOG_TAG, "Remote invalidation client ID not registered"); + return; + } + int count = mCallbackList.beginBroadcast(); + try { + for (int i = 0; i < count; i++) { + int targetClientId = (int) mCallbackList.getBroadcastCookie(i); + String targetName = mClientNames.get(targetClientId); + if (clientId == targetClientId // This is the caller itself. + || !name.equals(targetName)) { // Not the same file. + continue; + } + try { + IMultiInstanceInvalidationCallback callback = + mCallbackList.getBroadcastItem(i); + callback.onInvalidation(tables); + } catch (RemoteException e) { + Log.w(Room.LOG_TAG, "Error invoking a remote callback", e); + } + } + } finally { + mCallbackList.finishBroadcast(); + } + } + } + }; + + @Nullable + @Override + public IBinder onBind(@NonNull Intent intent) { + return mBinder; + } +} diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationService.kt b/app/src/main/java/androidx/room/MultiInstanceInvalidationService.kt deleted file mode 100644 index 3bac1ea5eb..0000000000 --- a/app/src/main/java/androidx/room/MultiInstanceInvalidationService.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2018 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 androidx.room - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import android.os.RemoteCallbackList -import android.os.RemoteException -import android.util.Log -import androidx.room.Room.LOG_TAG - -/** - * A [Service] for remote invalidation among multiple [InvalidationTracker] instances. - * This service runs in the main app process. All the instances of [InvalidationTracker] - * (potentially in other processes) has to connect to this service. - * - * The intent to launch it can be specified by - * [RoomDatabase.Builder.setMultiInstanceInvalidationServiceIntent], although the service is - * defined in the manifest by default so there should be no need to override it in a normal - * situation. - */ -@ExperimentalRoomApi -class MultiInstanceInvalidationService : Service() { - internal var maxClientId = 0 - internal val clientNames = mutableMapOf() - - internal val callbackList: RemoteCallbackList = - object : RemoteCallbackList() { - override fun onCallbackDied( - callback: IMultiInstanceInvalidationCallback, - cookie: Any - ) { - clientNames.remove(cookie as Int) - } - } - - private val binder: IMultiInstanceInvalidationService.Stub = - object : IMultiInstanceInvalidationService.Stub() { - // Assigns a client ID to the client. - override fun registerCallback( - callback: IMultiInstanceInvalidationCallback, - name: String? - ): Int { - if (name == null) { - return 0 - } - synchronized(callbackList) { - val clientId = ++maxClientId - // Use the client ID as the RemoteCallbackList cookie. - return if (callbackList.register(callback, clientId)) { - clientNames[clientId] = name - clientId - } else { - --maxClientId - 0 - } - } - } - - // Explicitly removes the client. - // The client can die without calling this. In that case, callbackList - // .onCallbackDied() can take care of removal. - override fun unregisterCallback( - callback: IMultiInstanceInvalidationCallback, - clientId: Int - ) { - synchronized(callbackList) { - callbackList.unregister(callback) - clientNames.remove(clientId) - } - } - - // Broadcasts table invalidation to other instances of the same database file. - // The broadcast is not sent to the caller itself. - override fun broadcastInvalidation(clientId: Int, tables: Array) { - synchronized(callbackList) { - val name = clientNames[clientId] - if (name == null) { - Log.w(LOG_TAG, "Remote invalidation client ID not registered") - return - } - val count = callbackList.beginBroadcast() - try { - for (i in 0 until count) { - val targetClientId = callbackList.getBroadcastCookie(i) as Int - val targetName = clientNames[targetClientId] - if (clientId == targetClientId || name != targetName) { - // Skip if this is the caller itself or broadcast is for another - // database. - continue - } - try { - callbackList.getBroadcastItem(i).onInvalidation(tables) - } catch (e: RemoteException) { - Log.w(LOG_TAG, "Error invoking a remote callback", e) - } - } - } finally { - callbackList.finishBroadcast() - } - } - } - } - - override fun onBind(intent: Intent): IBinder { - return binder - } -} diff --git a/app/src/main/java/androidx/room/QueryInterceptorDatabase.java b/app/src/main/java/androidx/room/QueryInterceptorDatabase.java new file mode 100644 index 0000000000..386b583587 --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorDatabase.java @@ -0,0 +1,312 @@ +/* + * Copyright 2020 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 androidx.room; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteTransactionListener; +import android.os.Build; +import android.os.CancellationSignal; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteQuery; +import androidx.sqlite.db.SupportSQLiteStatement; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.Executor; + + +/** + * Implements {@link SupportSQLiteDatabase} for SQLite queries. + */ +final class QueryInterceptorDatabase implements SupportSQLiteDatabase { + + private final SupportSQLiteDatabase mDelegate; + private final RoomDatabase.QueryCallback mQueryCallback; + private final Executor mQueryCallbackExecutor; + + QueryInterceptorDatabase(@NonNull SupportSQLiteDatabase supportSQLiteDatabase, + @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor + queryCallbackExecutor) { + mDelegate = supportSQLiteDatabase; + mQueryCallback = queryCallback; + mQueryCallbackExecutor = queryCallbackExecutor; + } + + @NonNull + @Override + public SupportSQLiteStatement compileStatement(@NonNull String sql) { + return new QueryInterceptorStatement(mDelegate.compileStatement(sql), + mQueryCallback, sql, mQueryCallbackExecutor); + } + + @Override + public void beginTransaction() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", + Collections.emptyList())); + mDelegate.beginTransaction(); + } + + @Override + public void beginTransactionNonExclusive() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION", + Collections.emptyList())); + mDelegate.beginTransactionNonExclusive(); + } + + @Override + public void beginTransactionWithListener(@NonNull SQLiteTransactionListener + transactionListener) { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", + Collections.emptyList())); + mDelegate.beginTransactionWithListener(transactionListener); + } + + @Override + public void beginTransactionWithListenerNonExclusive( + @NonNull SQLiteTransactionListener transactionListener) { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION", + Collections.emptyList())); + mDelegate.beginTransactionWithListenerNonExclusive(transactionListener); + } + + @Override + public void endTransaction() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("END TRANSACTION", + Collections.emptyList())); + mDelegate.endTransaction(); + } + + @Override + public void setTransactionSuccessful() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("TRANSACTION SUCCESSFUL", + Collections.emptyList())); + mDelegate.setTransactionSuccessful(); + } + + @Override + public boolean inTransaction() { + return mDelegate.inTransaction(); + } + + @Override + public boolean isDbLockedByCurrentThread() { + return mDelegate.isDbLockedByCurrentThread(); + } + + @Override + public boolean yieldIfContendedSafely() { + return mDelegate.yieldIfContendedSafely(); + } + + @Override + public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { + return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay); + } + + @Override + public int getVersion() { + return mDelegate.getVersion(); + } + + @Override + public void setVersion(int version) { + mDelegate.setVersion(version); + } + + @Override + public long getMaximumSize() { + return mDelegate.getMaximumSize(); + } + + @Override + public long setMaximumSize(long numBytes) { + return mDelegate.setMaximumSize(numBytes); + } + + @Override + public long getPageSize() { + return mDelegate.getPageSize(); + } + + @Override + public void setPageSize(long numBytes) { + mDelegate.setPageSize(numBytes); + } + + @NonNull + @Override + public Cursor query(@NonNull String query) { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query, + Collections.emptyList())); + return mDelegate.query(query); + } + + @NonNull + @Override + public Cursor query(@NonNull String query, @NonNull Object[] bindArgs) { + List inputArguments = new ArrayList<>(); + inputArguments.addAll(Arrays.asList(bindArgs)); + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query, + inputArguments)); + return mDelegate.query(query, bindArgs); + } + + @NonNull + @Override + public Cursor query(@NonNull SupportSQLiteQuery query) { + QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram(); + query.bindTo(queryInterceptorProgram); + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(), + queryInterceptorProgram.getBindArgs())); + return mDelegate.query(query); + } + + @NonNull + @Override + public Cursor query(@NonNull SupportSQLiteQuery query, + @NonNull CancellationSignal cancellationSignal) { + QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram(); + query.bindTo(queryInterceptorProgram); + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(), + queryInterceptorProgram.getBindArgs())); + return mDelegate.query(query); + } + + @Override + public long insert(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values) + throws SQLException { + return mDelegate.insert(table, conflictAlgorithm, values); + } + + @Override + public int delete(@NonNull String table, @NonNull String whereClause, + @NonNull Object[] whereArgs) { + return mDelegate.delete(table, whereClause, whereArgs); + } + + @Override + public int update(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values, + @NonNull String whereClause, + @NonNull Object[] whereArgs) { + return mDelegate.update(table, conflictAlgorithm, values, whereClause, + whereArgs); + } + + @Override + public void execSQL(@NonNull String sql) throws SQLException { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, new ArrayList<>(0))); + mDelegate.execSQL(sql); + } + + @Override + public void execSQL(@NonNull String sql, @NonNull Object[] bindArgs) throws SQLException { + List inputArguments = new ArrayList<>(); + inputArguments.addAll(Arrays.asList(bindArgs)); + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, inputArguments)); + mDelegate.execSQL(sql, inputArguments.toArray()); + } + + @Override + public boolean isReadOnly() { + return mDelegate.isReadOnly(); + } + + @Override + public boolean isOpen() { + return mDelegate.isOpen(); + } + + @Override + public boolean needUpgrade(int newVersion) { + return mDelegate.needUpgrade(newVersion); + } + + @NonNull + @Override + public String getPath() { + return mDelegate.getPath(); + } + + @Override + public void setLocale(@NonNull Locale locale) { + mDelegate.setLocale(locale); + } + + @Override + public void setMaxSqlCacheSize(int cacheSize) { + mDelegate.setMaxSqlCacheSize(cacheSize); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void setForeignKeyConstraintsEnabled(boolean enable) { + mDelegate.setForeignKeyConstraintsEnabled(enable); + } + + @Override + public boolean enableWriteAheadLogging() { + return mDelegate.enableWriteAheadLogging(); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void disableWriteAheadLogging() { + mDelegate.disableWriteAheadLogging(); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public boolean isWriteAheadLoggingEnabled() { + return mDelegate.isWriteAheadLoggingEnabled(); + } + + @NonNull + @Override + public List> getAttachedDbs() { + return mDelegate.getAttachedDbs(); + } + + @Override + public boolean isDatabaseIntegrityOk() { + return mDelegate.isDatabaseIntegrityOk(); + } + + @Override + public void close() throws IOException { + mDelegate.close(); + } + + @Override + public boolean isExecPerConnectionSQLSupported() { + return false; + } + + @Override + public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs) { + } +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorDatabase.kt b/app/src/main/java/androidx/room/QueryInterceptorDatabase.kt deleted file mode 100644 index 0f1ec17328..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorDatabase.kt +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2020 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 androidx.room - -import android.database.Cursor -import android.database.sqlite.SQLiteTransactionListener -import android.os.CancellationSignal -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteQuery -import androidx.sqlite.db.SupportSQLiteStatement -import java.util.concurrent.Executor - -/** - * Implements [SupportSQLiteDatabase] for SQLite queries. - */ -internal class QueryInterceptorDatabase( - private val delegate: SupportSQLiteDatabase, - private val queryCallbackExecutor: Executor, - private val queryCallback: RoomDatabase.QueryCallback -) : SupportSQLiteDatabase by delegate { - - override fun compileStatement(sql: String): SupportSQLiteStatement { - return QueryInterceptorStatement( - delegate.compileStatement(sql), - sql, - queryCallbackExecutor, - queryCallback, - ) - } - - override fun beginTransaction() { - queryCallbackExecutor.execute { - queryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", emptyList()) - } - delegate.beginTransaction() - } - - override fun beginTransactionNonExclusive() { - queryCallbackExecutor.execute { - queryCallback.onQuery("BEGIN DEFERRED TRANSACTION", emptyList()) - } - delegate.beginTransactionNonExclusive() - } - - override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) { - queryCallbackExecutor.execute { - queryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", emptyList()) - } - delegate.beginTransactionWithListener(transactionListener) - } - - override fun beginTransactionWithListenerNonExclusive( - transactionListener: SQLiteTransactionListener - ) { - queryCallbackExecutor.execute { - queryCallback.onQuery("BEGIN DEFERRED TRANSACTION", emptyList()) - } - delegate.beginTransactionWithListenerNonExclusive(transactionListener) - } - - override fun endTransaction() { - queryCallbackExecutor.execute { - queryCallback.onQuery("END TRANSACTION", emptyList()) - } - delegate.endTransaction() - } - - override fun setTransactionSuccessful() { - queryCallbackExecutor.execute { - queryCallback.onQuery("TRANSACTION SUCCESSFUL", emptyList()) - } - delegate.setTransactionSuccessful() - } - - override fun query(query: String): Cursor { - queryCallbackExecutor.execute { - queryCallback.onQuery(query, emptyList()) - } - return delegate.query(query) - } - - override fun query(query: String, bindArgs: Array): Cursor { - queryCallbackExecutor.execute { queryCallback.onQuery(query, bindArgs.toList()) } - return delegate.query(query, bindArgs) - } - - override fun query(query: SupportSQLiteQuery): Cursor { - val queryInterceptorProgram = QueryInterceptorProgram() - query.bindTo(queryInterceptorProgram) - queryCallbackExecutor.execute { - queryCallback.onQuery(query.sql, queryInterceptorProgram.bindArgsCache) - } - return delegate.query(query) - } - - override fun query( - query: SupportSQLiteQuery, - cancellationSignal: CancellationSignal? - ): Cursor { - val queryInterceptorProgram = QueryInterceptorProgram() - query.bindTo(queryInterceptorProgram) - queryCallbackExecutor.execute { - queryCallback.onQuery( - query.sql, - queryInterceptorProgram.bindArgsCache - ) - } - return delegate.query(query) - } - - // Suppress warning about `SQL` in execSQL not being camel case. This is an override function - // and it can't be renamed. - @Suppress("AcronymName") - override fun execSQL(sql: String) { - queryCallbackExecutor.execute { - queryCallback.onQuery(sql, emptyList()) - } - delegate.execSQL(sql) - } - - // Suppress warning about `SQL` in execSQL not being camel case. This is an override function - // and it can't be renamed. - @Suppress("AcronymName") - override fun execSQL(sql: String, bindArgs: Array) { - val inputArguments = buildList { addAll(bindArgs) } - queryCallbackExecutor.execute { - queryCallback.onQuery(sql, inputArguments) - } - delegate.execSQL(sql, inputArguments.toTypedArray()) - } -} diff --git a/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java b/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java new file mode 100644 index 0000000000..9916294cdf --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 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 androidx.room; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.util.concurrent.Executor; + +final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper { + + private final SupportSQLiteOpenHelper mDelegate; + private final RoomDatabase.QueryCallback mQueryCallback; + private final Executor mQueryCallbackExecutor; + + QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper, + @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor + queryCallbackExecutor) { + mDelegate = supportSQLiteOpenHelper; + mQueryCallback = queryCallback; + mQueryCallbackExecutor = queryCallbackExecutor; + } + + @Nullable + @Override + public String getDatabaseName() { + return mDelegate.getDatabaseName(); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void setWriteAheadLoggingEnabled(boolean enabled) { + mDelegate.setWriteAheadLoggingEnabled(enabled); + } + + @Override + public SupportSQLiteDatabase getWritableDatabase() { + return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback, + mQueryCallbackExecutor); + } + + @Override + public SupportSQLiteDatabase getReadableDatabase() { + return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback, + mQueryCallbackExecutor); + } + + @Override + public void close() { + mDelegate.close(); + } + + @Override + @NonNull + public SupportSQLiteOpenHelper getDelegate() { + return mDelegate; + } + +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt b/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt deleted file mode 100644 index c15672f810..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2020 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 androidx.room - -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import java.util.concurrent.Executor - -internal class QueryInterceptorOpenHelper( - override val delegate: SupportSQLiteOpenHelper, - private val queryCallbackExecutor: Executor, - private val queryCallback: RoomDatabase.QueryCallback -) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper { - override val writableDatabase: SupportSQLiteDatabase - get() = QueryInterceptorDatabase( - delegate.writableDatabase, - queryCallbackExecutor, - queryCallback - ) - - override val readableDatabase: SupportSQLiteDatabase - get() = QueryInterceptorDatabase( - delegate.readableDatabase, - queryCallbackExecutor, - queryCallback - ) -} diff --git a/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java b/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java new file mode 100644 index 0000000000..5d94cd10ac --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 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 androidx.room; + +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.util.concurrent.Executor; + +/** + * Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper. + */ +@SuppressWarnings("AcronymName") +final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { + + private final SupportSQLiteOpenHelper.Factory mDelegate; + private final RoomDatabase.QueryCallback mQueryCallback; + private final Executor mQueryCallbackExecutor; + + @SuppressWarnings("LambdaLast") + QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory, + @NonNull RoomDatabase.QueryCallback queryCallback, + @NonNull Executor queryCallbackExecutor) { + mDelegate = factory; + mQueryCallback = queryCallback; + mQueryCallbackExecutor = queryCallbackExecutor; + } + + @NonNull + @Override + public SupportSQLiteOpenHelper create( + @NonNull SupportSQLiteOpenHelper.Configuration configuration) { + return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback, + mQueryCallbackExecutor); + } +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.kt b/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.kt deleted file mode 100644 index 1d86465e0a..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 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 androidx.room - -import androidx.sqlite.db.SupportSQLiteOpenHelper -import java.util.concurrent.Executor - -/** - * Implements [SupportSQLiteOpenHelper.Factory] to wrap [QueryInterceptorOpenHelper]. - */ -internal class QueryInterceptorOpenHelperFactory( - private val delegate: SupportSQLiteOpenHelper.Factory, - private val queryCallbackExecutor: Executor, - private val queryCallback: RoomDatabase.QueryCallback, -) : SupportSQLiteOpenHelper.Factory by delegate { - override fun create( - configuration: SupportSQLiteOpenHelper.Configuration - ): SupportSQLiteOpenHelper { - return QueryInterceptorOpenHelper( - delegate.create(configuration), - queryCallbackExecutor, - queryCallback - ) - } -} diff --git a/app/src/main/java/androidx/room/QueryInterceptorProgram.java b/app/src/main/java/androidx/room/QueryInterceptorProgram.java new file mode 100644 index 0000000000..2b9c554420 --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorProgram.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020 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 androidx.room; + +import androidx.sqlite.db.SupportSQLiteProgram; + +import java.util.ArrayList; +import java.util.List; + +/** + * A program implementing an {@link SupportSQLiteProgram} API to record bind arguments. + */ +final class QueryInterceptorProgram implements SupportSQLiteProgram { + private List mBindArgsCache = new ArrayList<>(); + + @Override + public void bindNull(int index) { + saveArgsToCache(index, null); + } + + @Override + public void bindLong(int index, long value) { + saveArgsToCache(index, value); + } + + @Override + public void bindDouble(int index, double value) { + saveArgsToCache(index, value); + } + + @Override + public void bindString(int index, String value) { + saveArgsToCache(index, value); + } + + @Override + public void bindBlob(int index, byte[] value) { + saveArgsToCache(index, value); + } + + @Override + public void clearBindings() { + mBindArgsCache.clear(); + } + + @Override + public void close() { } + + private void saveArgsToCache(int bindIndex, Object value) { + // The index into bind methods are 1...n + int index = bindIndex - 1; + if (index >= mBindArgsCache.size()) { + for (int i = mBindArgsCache.size(); i <= index; i++) { + mBindArgsCache.add(null); + } + } + mBindArgsCache.set(index, value); + } + + /** + * Returns the list of arguments associated with the query. + * + * @return argument list. + */ + List getBindArgs() { + return mBindArgsCache; + } +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorProgram.kt b/app/src/main/java/androidx/room/QueryInterceptorProgram.kt deleted file mode 100644 index 49aea347ba..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorProgram.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 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 androidx.room - -import androidx.sqlite.db.SupportSQLiteProgram - -/** - * A program implementing an [SupportSQLiteProgram] API to record bind arguments. - */ -internal class QueryInterceptorProgram : SupportSQLiteProgram { - internal val bindArgsCache = mutableListOf() - - override fun bindNull(index: Int) { - saveArgsToCache(index, null) - } - - override fun bindLong(index: Int, value: Long) { - saveArgsToCache(index, value) - } - - override fun bindDouble(index: Int, value: Double) { - saveArgsToCache(index, value) - } - - override fun bindString(index: Int, value: String) { - saveArgsToCache(index, value) - } - - override fun bindBlob(index: Int, value: ByteArray) { - saveArgsToCache(index, value) - } - - override fun clearBindings() { - bindArgsCache.clear() - } - - override fun close() {} - - private fun saveArgsToCache(bindIndex: Int, value: Any?) { - // The index into bind methods are 1...n - val index = bindIndex - 1 - if (index >= bindArgsCache.size) { - for (i in bindArgsCache.size..index) { - bindArgsCache.add(null) - } - } - bindArgsCache[index] = value - } -} diff --git a/app/src/main/java/androidx/room/QueryInterceptorStatement.java b/app/src/main/java/androidx/room/QueryInterceptorStatement.java new file mode 100644 index 0000000000..8825252e5d --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorStatement.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 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 androidx.room; + +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteStatement; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Implements an instance of {@link SupportSQLiteStatement} for SQLite queries. + */ +final class QueryInterceptorStatement implements SupportSQLiteStatement { + + private final SupportSQLiteStatement mDelegate; + private final RoomDatabase.QueryCallback mQueryCallback; + private final String mSqlStatement; + private final List mBindArgsCache = new ArrayList<>(); + private final Executor mQueryCallbackExecutor; + + QueryInterceptorStatement(@NonNull SupportSQLiteStatement compileStatement, + @NonNull RoomDatabase.QueryCallback queryCallback, String sqlStatement, + @NonNull Executor queryCallbackExecutor) { + mDelegate = compileStatement; + mQueryCallback = queryCallback; + mSqlStatement = sqlStatement; + mQueryCallbackExecutor = queryCallbackExecutor; + } + + @Override + public void execute() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + mDelegate.execute(); + } + + @Override + public int executeUpdateDelete() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + return mDelegate.executeUpdateDelete(); + } + + @Override + public long executeInsert() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + return mDelegate.executeInsert(); + } + + @Override + public long simpleQueryForLong() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + return mDelegate.simpleQueryForLong(); + } + + @Override + public String simpleQueryForString() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + return mDelegate.simpleQueryForString(); + } + + @Override + public void bindNull(int index) { + saveArgsToCache(index, mBindArgsCache.toArray()); + mDelegate.bindNull(index); + } + + @Override + public void bindLong(int index, long value) { + saveArgsToCache(index, value); + mDelegate.bindLong(index, value); + } + + @Override + public void bindDouble(int index, double value) { + saveArgsToCache(index, value); + mDelegate.bindDouble(index, value); + } + + @Override + public void bindString(int index, String value) { + saveArgsToCache(index, value); + mDelegate.bindString(index, value); + } + + @Override + public void bindBlob(int index, byte[] value) { + saveArgsToCache(index, value); + mDelegate.bindBlob(index, value); + } + + @Override + public void clearBindings() { + mBindArgsCache.clear(); + mDelegate.clearBindings(); + } + + @Override + public void close() throws IOException { + mDelegate.close(); + } + + private void saveArgsToCache(int bindIndex, Object value) { + int index = bindIndex - 1; + if (index >= mBindArgsCache.size()) { + // Add null entries to the list until we have the desired # of indices + for (int i = mBindArgsCache.size(); i <= index; i++) { + mBindArgsCache.add(null); + } + } + mBindArgsCache.set(index, value); + } +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorStatement.kt b/app/src/main/java/androidx/room/QueryInterceptorStatement.kt deleted file mode 100644 index 37eca347a8..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorStatement.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2020 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 androidx.room - -import androidx.sqlite.db.SupportSQLiteStatement -import java.util.concurrent.Executor - -/** - * Implements an instance of [SupportSQLiteStatement] for SQLite queries. - */ -internal class QueryInterceptorStatement( - private val delegate: SupportSQLiteStatement, - private val sqlStatement: String, - private val queryCallbackExecutor: Executor, - private val queryCallback: RoomDatabase.QueryCallback, -) : SupportSQLiteStatement by delegate { - - private val bindArgsCache = mutableListOf() - - override fun execute() { - queryCallbackExecutor.execute { - queryCallback.onQuery(sqlStatement, bindArgsCache) - } - delegate.execute() - } - - override fun executeUpdateDelete(): Int { - queryCallbackExecutor.execute { - queryCallback.onQuery(sqlStatement, bindArgsCache) - } - return delegate.executeUpdateDelete() - } - - override fun executeInsert(): Long { - queryCallbackExecutor.execute { - queryCallback.onQuery(sqlStatement, bindArgsCache) - } - return delegate.executeInsert() - } - - override fun simpleQueryForLong(): Long { - queryCallbackExecutor.execute { - queryCallback.onQuery(sqlStatement, bindArgsCache) - } - return delegate.simpleQueryForLong() - } - - override fun simpleQueryForString(): String? { - queryCallbackExecutor.execute { - queryCallback.onQuery(sqlStatement, bindArgsCache) - } - return delegate.simpleQueryForString() - } - - override fun bindNull(index: Int) { - saveArgsToCache(index, null) - delegate.bindNull(index) - } - - override fun bindLong(index: Int, value: Long) { - saveArgsToCache(index, value) - delegate.bindLong(index, value) - } - - override fun bindDouble(index: Int, value: Double) { - saveArgsToCache(index, value) - delegate.bindDouble(index, value) - } - - override fun bindString(index: Int, value: String) { - saveArgsToCache(index, value) - delegate.bindString(index, value) - } - - override fun bindBlob(index: Int, value: ByteArray) { - saveArgsToCache(index, value) - delegate.bindBlob(index, value) - } - - override fun clearBindings() { - bindArgsCache.clear() - delegate.clearBindings() - } - - private fun saveArgsToCache(bindIndex: Int, value: Any?) { - val index = bindIndex - 1 - if (index >= bindArgsCache.size) { - // Add null entries to the list until we have the desired # of indices - repeat(index - bindArgsCache.size + 1) { - bindArgsCache.add(null) - } - } - bindArgsCache[index] = value - } -} diff --git a/app/src/main/java/androidx/room/Room.java b/app/src/main/java/androidx/room/Room.java new file mode 100644 index 0000000000..d2520998d1 --- /dev/null +++ b/app/src/main/java/androidx/room/Room.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016 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 androidx.room; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + +/** + * Utility class for Room. + */ +@SuppressWarnings("unused") +public class Room { + static final String LOG_TAG = "ROOM"; + /** + * The master table where room keeps its metadata information. + */ + public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME; + private static final String CURSOR_CONV_SUFFIX = "_CursorConverter"; + + /** + * Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you + * should keep a reference to it and re-use it. + * + * @param context The context for the database. This is usually the Application context. + * @param klass The abstract class which is annotated with {@link Database} and extends + * {@link RoomDatabase}. + * @param name The name of the database file. + * @param The type of the database class. + * @return A {@code RoomDatabaseBuilder} which you can use to create the database. + */ + @SuppressWarnings("WeakerAccess") + @NonNull + public static RoomDatabase.Builder databaseBuilder( + @NonNull Context context, @NonNull Class klass, @NonNull String name) { + //noinspection ConstantConditions + if (name == null || name.trim().length() == 0) { + throw new IllegalArgumentException("Cannot build a database with null or empty name." + + " If you are trying to create an in memory database, use Room" + + ".inMemoryDatabaseBuilder"); + } + return new RoomDatabase.Builder<>(context, klass, name); + } + + /** + * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory + * database disappears when the process is killed. + * Once a database is built, you should keep a reference to it and re-use it. + * + * @param context The context for the database. This is usually the Application context. + * @param klass The abstract class which is annotated with {@link Database} and extends + * {@link RoomDatabase}. + * @param The type of the database class. + * @return A {@code RoomDatabaseBuilder} which you can use to create the database. + */ + @NonNull + public static RoomDatabase.Builder inMemoryDatabaseBuilder( + @NonNull Context context, @NonNull Class klass) { + return new RoomDatabase.Builder<>(context, klass, null); + } + + @SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"}) + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public static T getGeneratedImplementation(@NonNull Class klass, + @NonNull String suffix) { + final String fullPackage = klass.getPackage().getName(); + String name = klass.getCanonicalName(); + final String postPackageName = fullPackage.isEmpty() + ? name + : name.substring(fullPackage.length() + 1); + final String implName = postPackageName.replace('.', '_') + suffix; + //noinspection TryWithIdenticalCatches + try { + + final String fullClassName = fullPackage.isEmpty() + ? implName + : fullPackage + "." + implName; + @SuppressWarnings("unchecked") + final Class aClass = (Class) Class.forName( + fullClassName, true, klass.getClassLoader()); + return aClass.newInstance(); + } catch (ClassNotFoundException e) { + throw new RuntimeException("cannot find implementation for " + + klass.getCanonicalName() + ". " + implName + " does not exist"); + } catch (IllegalAccessException e) { + throw new RuntimeException("Cannot access the constructor" + + klass.getCanonicalName()); + } catch (InstantiationException e) { + throw new RuntimeException("Failed to create an instance of " + + klass.getCanonicalName()); + } + } + + /** @deprecated This type should not be instantiated as it contains only static methods. */ + @Deprecated + @SuppressWarnings("PrivateConstructorForUtilityClass") + public Room() { + } +} diff --git a/app/src/main/java/androidx/room/Room.kt b/app/src/main/java/androidx/room/Room.kt deleted file mode 100644 index d0cb58543e..0000000000 --- a/app/src/main/java/androidx/room/Room.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.room - -import android.content.Context -import androidx.annotation.RestrictTo - -/** - * Utility functions for Room. - */ -object Room { - - internal const val LOG_TAG = "ROOM" - - /** - * The master table where room keeps its metadata information. - */ - const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME - - private const val CURSOR_CONV_SUFFIX = "_CursorConverter" - - @Suppress("UNCHECKED_CAST") - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - @JvmStatic - fun getGeneratedImplementation( - klass: Class, - suffix: String - ): T { - val fullPackage = klass.getPackage()!!.name - val name: String = klass.canonicalName!! - val postPackageName = - if (fullPackage.isEmpty()) name else name.substring(fullPackage.length + 1) - val implName = postPackageName.replace('.', '_') + suffix - return try { - val fullClassName = if (fullPackage.isEmpty()) { - implName - } else { - "$fullPackage.$implName" - } - val aClass = Class.forName( - fullClassName, true, klass.classLoader - ) as Class - aClass.getDeclaredConstructor().newInstance() - } catch (e: ClassNotFoundException) { - throw RuntimeException( - "Cannot find implementation for ${klass.canonicalName}. $implName does not " + - "exist" - ) - } catch (e: IllegalAccessException) { - throw RuntimeException( - "Cannot access the constructor ${klass.canonicalName}" - ) - } catch (e: InstantiationException) { - throw RuntimeException( - "Failed to create an instance of ${klass.canonicalName}" - ) - } - } - - /** - * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory - * database disappears when the process is killed. - * Once a database is built, you should keep a reference to it and re-use it. - * - * @param context The context for the database. This is usually the Application context. - * @param klass The abstract class which is annotated with [Database] and extends - * [RoomDatabase]. - * @param T The type of the database class. - * @return A `RoomDatabaseBuilder` which you can use to create the database. - */ - @JvmStatic - fun inMemoryDatabaseBuilder( - context: Context, - klass: Class - ): RoomDatabase.Builder { - return RoomDatabase.Builder(context, klass, null) - } - - /** - * Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you - * should keep a reference to it and re-use it. - * - * @param context The context for the database. This is usually the Application context. - * @param klass The abstract class which is annotated with [Database] and extends - * [RoomDatabase]. - * @param name The name of the database file. - * @param T The type of the database class. - * @return A `RoomDatabaseBuilder` which you can use to create the database. - */ - @JvmStatic - fun databaseBuilder( - context: Context, - klass: Class, - name: String? - ): RoomDatabase.Builder { - require(!name.isNullOrBlank()) { - "Cannot build a database with null or empty name." + - " If you are trying to create an in memory database, use Room" + - ".inMemoryDatabaseBuilder" - } - return RoomDatabase.Builder(context, klass, name) - } -} diff --git a/app/src/main/java/androidx/room/RoomDatabase.java b/app/src/main/java/androidx/room/RoomDatabase.java new file mode 100644 index 0000000000..54427d6eea --- /dev/null +++ b/app/src/main/java/androidx/room/RoomDatabase.java @@ -0,0 +1,1678 @@ +/* + * Copyright (C) 2016 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 androidx.room; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.os.Build; +import android.os.CancellationSignal; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.CallSuper; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.annotation.WorkerThread; +import androidx.arch.core.executor.ArchTaskExecutor; +import androidx.room.migration.AutoMigrationSpec; +import androidx.room.migration.Migration; +import androidx.room.util.SneakyThrow; +import androidx.sqlite.db.SimpleSQLiteQuery; +import androidx.sqlite.db.SupportSQLiteCompat; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; +import androidx.sqlite.db.SupportSQLiteQuery; +import androidx.sqlite.db.SupportSQLiteStatement; +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Base class for all Room databases. All classes that are annotated with {@link Database} must + * extend this class. + *

    + * RoomDatabase provides direct access to the underlying database implementation but you should + * prefer using {@link Dao} classes. + * + * @see Database + */ +public abstract class RoomDatabase { + private static final String DB_IMPL_SUFFIX = "_Impl"; + /** + * Unfortunately, we cannot read this value so we are only setting it to the SQLite default. + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public static final int MAX_BIND_PARAMETER_CNT = 999; + /** + * Set by the generated open helper. + * + * @deprecated Will be hidden in the next release. + */ + @Deprecated + protected volatile SupportSQLiteDatabase mDatabase; + private Executor mQueryExecutor; + private Executor mTransactionExecutor; + private SupportSQLiteOpenHelper mOpenHelper; + private final InvalidationTracker mInvalidationTracker; + private boolean mAllowMainThreadQueries; + boolean mWriteAheadLoggingEnabled; + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + @Nullable + @Deprecated + protected List mCallbacks; + + /** + * A map of auto migration spec classes to their provided instance. + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @NonNull + protected Map, AutoMigrationSpec> mAutoMigrationSpecs; + + private final ReentrantReadWriteLock mCloseLock = new ReentrantReadWriteLock(); + + @Nullable + private AutoCloser mAutoCloser; + + /** + * {@link InvalidationTracker} uses this lock to prevent the database from closing while it is + * querying database updates. + *

    + * The returned lock is reentrant and will allow multiple threads to acquire the lock + * simultaneously until {@link #close()} is invoked in which the lock becomes exclusive as + * a way to let the InvalidationTracker finish its work before closing the database. + * + * @return The lock for {@link #close()}. + */ + Lock getCloseLock() { + return mCloseLock.readLock(); + } + + /** + * This id is only set on threads that are used to dispatch coroutines within a suspending + * database transaction. + */ + private final ThreadLocal mSuspendingTransactionId = new ThreadLocal<>(); + + /** + * Gets the suspending transaction id of the current thread. + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + ThreadLocal getSuspendingTransactionId() { + return mSuspendingTransactionId; + } + + private final Map mBackingFieldMap = + Collections.synchronizedMap(new HashMap<>()); + + /** + * Gets the map for storing extension properties of Kotlin type. + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + Map getBackingFieldMap() { + return mBackingFieldMap; + } + + // Updated later to an unmodifiable map when init is called. + private final Map, Object> mTypeConverters; + + /** + * Gets the instance of the given Type Converter. + * + * @param klass The Type Converter class. + * @param The type of the expected Type Converter subclass. + * @return An instance of T if it is provided in the builder. + */ + @SuppressWarnings("unchecked") + @Nullable + public T getTypeConverter(@NonNull Class klass) { + return (T) mTypeConverters.get(klass); + } + + /** + * Creates a RoomDatabase. + *

    + * You cannot create an instance of a database, instead, you should acquire it via + * {@link Room#databaseBuilder(Context, Class, String)} or + * {@link Room#inMemoryDatabaseBuilder(Context, Class)}. + */ + public RoomDatabase() { + mInvalidationTracker = createInvalidationTracker(); + mTypeConverters = new HashMap<>(); + mAutoMigrationSpecs = new HashMap<>(); + } + + /** + * Called by {@link Room} when it is initialized. + * + * @param configuration The database configuration. + */ + @CallSuper + public void init(@NonNull DatabaseConfiguration configuration) { + mOpenHelper = createOpenHelper(configuration); + Set> requiredAutoMigrationSpecs = + getRequiredAutoMigrationSpecs(); + BitSet usedSpecs = new BitSet(); + for (Class spec : requiredAutoMigrationSpecs) { + int foundIndex = -1; + for (int providedIndex = configuration.autoMigrationSpecs.size() - 1; + providedIndex >= 0; providedIndex-- + ) { + Object provided = configuration.autoMigrationSpecs.get(providedIndex); + if (spec.isAssignableFrom(provided.getClass())) { + foundIndex = providedIndex; + usedSpecs.set(foundIndex); + break; + } + } + if (foundIndex < 0) { + throw new IllegalArgumentException( + "A required auto migration spec (" + spec.getCanonicalName() + + ") is missing in the database configuration."); + } + mAutoMigrationSpecs.put(spec, configuration.autoMigrationSpecs.get(foundIndex)); + } + + for (int providedIndex = configuration.autoMigrationSpecs.size() - 1; + providedIndex >= 0; providedIndex--) { + if (!usedSpecs.get(providedIndex)) { + throw new IllegalArgumentException("Unexpected auto migration specs found. " + + "Annotate AutoMigrationSpec implementation with " + + "@ProvidedAutoMigrationSpec annotation or remove this spec from the " + + "builder."); + } + } + + List autoMigrations = getAutoMigrations(mAutoMigrationSpecs); + for (Migration autoMigration : autoMigrations) { + boolean migrationExists = configuration.migrationContainer.getMigrations() + .containsKey(autoMigration.startVersion); + if (!migrationExists) { + configuration.migrationContainer.addMigrations(autoMigration); + } + } + + // Configure SqliteCopyOpenHelper if it is available: + SQLiteCopyOpenHelper copyOpenHelper = unwrapOpenHelper(SQLiteCopyOpenHelper.class, + mOpenHelper); + if (copyOpenHelper != null) { + copyOpenHelper.setDatabaseConfiguration(configuration); + } + + AutoClosingRoomOpenHelper autoClosingRoomOpenHelper = + unwrapOpenHelper(AutoClosingRoomOpenHelper.class, mOpenHelper); + + if (autoClosingRoomOpenHelper != null) { + mAutoCloser = autoClosingRoomOpenHelper.getAutoCloser(); + mInvalidationTracker.setAutoCloser(mAutoCloser); + } + + + boolean wal = false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + wal = configuration.journalMode == JournalMode.WRITE_AHEAD_LOGGING; + mOpenHelper.setWriteAheadLoggingEnabled(wal); + } + mCallbacks = configuration.callbacks; + mQueryExecutor = configuration.queryExecutor; + mTransactionExecutor = new TransactionExecutor(configuration.transactionExecutor); + mAllowMainThreadQueries = configuration.allowMainThreadQueries; + mWriteAheadLoggingEnabled = wal; + if (configuration.multiInstanceInvalidationServiceIntent != null) { + mInvalidationTracker.startMultiInstanceInvalidation(configuration.context, + configuration.name, configuration.multiInstanceInvalidationServiceIntent); + } + + Map, List>> requiredFactories = getRequiredTypeConverters(); + // indices for each converter on whether it is used or not so that we can throw an exception + // if developer provides an unused converter. It is not necessarily an error but likely + // to be because why would developer add a converter if it won't be used? + BitSet used = new BitSet(); + for (Map.Entry, List>> entry : requiredFactories.entrySet()) { + Class daoName = entry.getKey(); + for (Class converter : entry.getValue()) { + int foundIndex = -1; + // traverse provided converters in reverse so that newer one overrides + for (int providedIndex = configuration.typeConverters.size() - 1; + providedIndex >= 0; providedIndex--) { + Object provided = configuration.typeConverters.get(providedIndex); + if (converter.isAssignableFrom(provided.getClass())) { + foundIndex = providedIndex; + used.set(foundIndex); + break; + } + } + if (foundIndex < 0) { + throw new IllegalArgumentException( + "A required type converter (" + converter + ") for" + + " " + daoName.getCanonicalName() + + " is missing in the database configuration."); + } + mTypeConverters.put(converter, configuration.typeConverters.get(foundIndex)); + } + } + // now, make sure all provided factories are used + for (int providedIndex = configuration.typeConverters.size() - 1; + providedIndex >= 0; providedIndex--) { + if (!used.get(providedIndex)) { + Object converter = configuration.typeConverters.get(providedIndex); + throw new IllegalArgumentException("Unexpected type converter " + converter + ". " + + "Annotate TypeConverter class with @ProvidedTypeConverter annotation " + + "or remove this converter from the builder."); + } + } + } + + /** + * Returns a list of {@link Migration} of a database that have been automatically generated. + * + * @return A list of migration instances each of which is a generated autoMigration + * @param autoMigrationSpecs + * + * @hide + */ + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public List getAutoMigrations( + @NonNull Map, AutoMigrationSpec> autoMigrationSpecs + ) { + return Collections.emptyList(); + } + + /** + * Unwraps (delegating) open helpers until it finds clazz, otherwise returns null. + * + * @param clazz the open helper type to search for + * @param openHelper the open helper to search through + * @param the type of clazz + * @return the instance of clazz, otherwise null + */ + @Nullable + @SuppressWarnings("unchecked") + private T unwrapOpenHelper(Class clazz, SupportSQLiteOpenHelper openHelper) { + if (clazz.isInstance(openHelper)) { + return (T) openHelper; + } + if (openHelper instanceof DelegatingOpenHelper) { + return unwrapOpenHelper(clazz, ((DelegatingOpenHelper) openHelper).getDelegate()); + } + return null; + } + + /** + * Returns the SQLite open helper used by this database. + * + * @return The SQLite open helper used by this database. + */ + @NonNull + public SupportSQLiteOpenHelper getOpenHelper() { + return mOpenHelper; + } + + /** + * Creates the open helper to access the database. Generated class already implements this + * method. + * Note that this method is called when the RoomDatabase is initialized. + * + * @param config The configuration of the Room database. + * @return A new SupportSQLiteOpenHelper to be used while connecting to the database. + */ + @NonNull + protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config); + + /** + * Called when the RoomDatabase is created. + *

    + * This is already implemented by the generated code. + * + * @return Creates a new InvalidationTracker. + */ + @NonNull + protected abstract InvalidationTracker createInvalidationTracker(); + + /** + * Returns a Map of String -> List<Class> where each entry has the `key` as the DAO name + * and `value` as the list of type converter classes that are necessary for the database to + * function. + *

    + * This is implemented by the generated code. + * + * @return Creates a map that will include all required type converters for this database. + */ + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + protected Map, List>> getRequiredTypeConverters() { + return Collections.emptyMap(); + } + + /** + * Returns a Set of required AutoMigrationSpec classes. + *

    + * This is implemented by the generated code. + * + * @return Creates a set that will include all required auto migration specs for this database. + * + * @hide + */ + @NonNull + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public Set> getRequiredAutoMigrationSpecs() { + return Collections.emptySet(); + } + + /** + * Deletes all rows from all the tables that are registered to this database as + * {@link Database#entities()}. + *

    + * This does NOT reset the auto-increment value generated by {@link PrimaryKey#autoGenerate()}. + *

    + * After deleting the rows, Room will set a WAL checkpoint and run VACUUM. This means that the + * data is completely erased. The space will be reclaimed by the system if the amount surpasses + * the threshold of database file size. + * + * @see Database File Format + */ + @WorkerThread + public abstract void clearAllTables(); + + /** + * Returns true if database connection is open and initialized. + * + * @return true if the database connection is open, false otherwise. + */ + public boolean isOpen() { + // We need to special case for the auto closing database because mDatabase is the + // underlying database and not the wrapped database. + if (mAutoCloser != null) { + return mAutoCloser.isActive(); + } + + final SupportSQLiteDatabase db = mDatabase; + return db != null && db.isOpen(); + } + + /** + * Closes the database if it is already open. + */ + public void close() { + if (isOpen()) { + final Lock closeLock = mCloseLock.writeLock(); + closeLock.lock(); + try { + mInvalidationTracker.stopMultiInstanceInvalidation(); + mOpenHelper.close(); + } finally { + closeLock.unlock(); + } + } + } + + /** + * Asserts that we are not on the main thread. + * + * @hide + */ + @SuppressWarnings("WeakerAccess") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + // used in generated code + public void assertNotMainThread() { + if (mAllowMainThreadQueries) { + return; + } + if (isMainThread()) { + throw new IllegalStateException("Cannot access database on the main thread since" + + " it may potentially lock the UI for a long period of time."); + } + } + + /** + * Asserts that we are not on a suspending transaction. + * + * @hide + */ + @SuppressWarnings("WeakerAccess") + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + // used in generated code + public void assertNotSuspendingTransaction() { + if (!inTransaction() && mSuspendingTransactionId.get() != null) { + throw new IllegalStateException("Cannot access database on a different coroutine" + + " context inherited from a suspending transaction."); + } + } + + // Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which + // methods we are using and also helps unit tests to mock this class without mocking + // all SQLite database methods. + + /** + * Convenience method to query the database with arguments. + * + * @param query The sql query + * @param args The bind arguments for the placeholders in the query + * @return A Cursor obtained by running the given query in the Room database. + */ + @NonNull + public Cursor query(@NonNull String query, @Nullable Object[] args) { + return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args)); + } + + /** + * Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}. + * + * @param query The Query which includes the SQL and a bind callback for bind arguments. + * @return Result of the query. + */ + @NonNull + public Cursor query(@NonNull SupportSQLiteQuery query) { + return query(query, null); + } + + /** + * Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}. + * + * @param query The Query which includes the SQL and a bind callback for bind arguments. + * @param signal The cancellation signal to be attached to the query. + * @return Result of the query. + */ + @NonNull + public Cursor query(@NonNull SupportSQLiteQuery query, @Nullable CancellationSignal signal) { + assertNotMainThread(); + assertNotSuspendingTransaction(); + if (signal != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return mOpenHelper.getWritableDatabase().query(query, signal); + } else { + return mOpenHelper.getWritableDatabase().query(query); + } + } + + /** + * Wrapper for {@link SupportSQLiteDatabase#compileStatement(String)}. + * + * @param sql The query to compile. + * @return The compiled query. + */ + public SupportSQLiteStatement compileStatement(@NonNull String sql) { + assertNotMainThread(); + assertNotSuspendingTransaction(); + return mOpenHelper.getWritableDatabase().compileStatement(sql); + } + + /** + * Wrapper for {@link SupportSQLiteDatabase#beginTransaction()}. + * + * @deprecated Use {@link #runInTransaction(Runnable)} + */ + @Deprecated + public void beginTransaction() { + assertNotMainThread(); + if (mAutoCloser == null) { + internalBeginTransaction(); + } else { + mAutoCloser.executeRefCountingFunction(db -> { + internalBeginTransaction(); + return null; + }); + } + } + + private void internalBeginTransaction() { + assertNotMainThread(); + SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase(); + mInvalidationTracker.syncTriggers(database); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && database.isWriteAheadLoggingEnabled()) { + database.beginTransactionNonExclusive(); + } else { + database.beginTransaction(); + } + } + + /** + * Wrapper for {@link SupportSQLiteDatabase#endTransaction()}. + * + * @deprecated Use {@link #runInTransaction(Runnable)} + */ + @Deprecated + public void endTransaction() { + if (mAutoCloser == null) { + internalEndTransaction(); + } else { + mAutoCloser.executeRefCountingFunction(db -> { + internalEndTransaction(); + return null; + }); + } + } + + private void internalEndTransaction() { + mOpenHelper.getWritableDatabase().endTransaction(); + if (!inTransaction()) { + // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last + // endTransaction call to do it. + mInvalidationTracker.refreshVersionsAsync(); + } + } + + /** + * @return The Executor in use by this database for async queries. + */ + @NonNull + public Executor getQueryExecutor() { + return mQueryExecutor; + } + + /** + * @return The Executor in use by this database for async transactions. + */ + @NonNull + public Executor getTransactionExecutor() { + return mTransactionExecutor; + } + + /** + * Wrapper for {@link SupportSQLiteDatabase#setTransactionSuccessful()}. + * + * @deprecated Use {@link #runInTransaction(Runnable)} + */ + @Deprecated + public void setTransactionSuccessful() { + mOpenHelper.getWritableDatabase().setTransactionSuccessful(); + } + + /** + * Executes the specified {@link Runnable} in a database transaction. The transaction will be + * marked as successful unless an exception is thrown in the {@link Runnable}. + *

    + * Room will only perform at most one transaction at a time. + * + * @param body The piece of code to execute. + */ + @SuppressWarnings("deprecation") + public void runInTransaction(@NonNull Runnable body) { + beginTransaction(); + try { + body.run(); + setTransactionSuccessful(); + } finally { + endTransaction(); + } + } + + /** + * Executes the specified {@link Callable} in a database transaction. The transaction will be + * marked as successful unless an exception is thrown in the {@link Callable}. + *

    + * Room will only perform at most one transaction at a time. + * + * @param body The piece of code to execute. + * @param The type of the return value. + * @return The value returned from the {@link Callable}. + */ + @SuppressWarnings("deprecation") + public V runInTransaction(@NonNull Callable body) { + beginTransaction(); + try { + V result = body.call(); + setTransactionSuccessful(); + return result; + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + SneakyThrow.reThrow(e); + return null; // Unreachable code, but compiler doesn't know it. + } finally { + endTransaction(); + } + } + + /** + * Called by the generated code when database is open. + *

    + * You should never call this method manually. + * + * @param db The database instance. + */ + protected void internalInitInvalidationTracker(@NonNull SupportSQLiteDatabase db) { + mInvalidationTracker.internalInit(db); + } + + /** + * Returns the invalidation tracker for this database. + *

    + * You can use the invalidation tracker to get notified when certain tables in the database + * are modified. + * + * @return The invalidation tracker for the database. + */ + @NonNull + public InvalidationTracker getInvalidationTracker() { + return mInvalidationTracker; + } + + /** + * Returns true if current thread is in a transaction. + * + * @return True if there is an active transaction in current thread, false otherwise. + * @see SupportSQLiteDatabase#inTransaction() + */ + @SuppressWarnings("WeakerAccess") + public boolean inTransaction() { + return mOpenHelper.getWritableDatabase().inTransaction(); + } + + /** + * Journal modes for SQLite database. + * + * @see Builder#setJournalMode(JournalMode) + */ + public enum JournalMode { + + /** + * Let Room choose the journal mode. This is the default value when no explicit value is + * specified. + *

    + * The actual value will be {@link #TRUNCATE} when the device runs API Level lower than 16 + * or it is a low-RAM device. Otherwise, {@link #WRITE_AHEAD_LOGGING} will be used. + */ + AUTOMATIC, + + /** + * Truncate journal mode. + */ + TRUNCATE, + + /** + * Write-Ahead Logging mode. + */ + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) + WRITE_AHEAD_LOGGING; + + /** + * Resolves {@link #AUTOMATIC} to either {@link #TRUNCATE} or + * {@link #WRITE_AHEAD_LOGGING}. + */ + JournalMode resolve(Context context) { + if (this != AUTOMATIC) { + return this; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + ActivityManager manager = (ActivityManager) + context.getSystemService(Context.ACTIVITY_SERVICE); + if (manager != null && !isLowRamDevice(manager)) { + return WRITE_AHEAD_LOGGING; + } + } + return TRUNCATE; + } + + private static boolean isLowRamDevice(@NonNull ActivityManager activityManager) { + if (Build.VERSION.SDK_INT >= 19) { + return SupportSQLiteCompat.Api19Impl.isLowRamDevice(activityManager); + } + return false; + } + } + + /** + * Builder for RoomDatabase. + * + * @param The type of the abstract database class. + */ + public static class Builder { + private final Class mDatabaseClass; + private final String mName; + private final Context mContext; + private ArrayList mCallbacks; + private PrepackagedDatabaseCallback mPrepackagedDatabaseCallback; + private QueryCallback mQueryCallback; + private Executor mQueryCallbackExecutor; + private List mTypeConverters; + private List mAutoMigrationSpecs; + + /** The Executor used to run database queries. This should be background-threaded. */ + private Executor mQueryExecutor; + /** The Executor used to run database transactions. This should be background-threaded. */ + private Executor mTransactionExecutor; + private SupportSQLiteOpenHelper.Factory mFactory; + private boolean mAllowMainThreadQueries; + private JournalMode mJournalMode; + private Intent mMultiInstanceInvalidationIntent; + private boolean mRequireMigration; + private boolean mAllowDestructiveMigrationOnDowngrade; + + private long mAutoCloseTimeout = -1L; + private TimeUnit mAutoCloseTimeUnit; + + /** + * Migrations, mapped by from-to pairs. + */ + private final MigrationContainer mMigrationContainer; + private Set mMigrationsNotRequiredFrom; + /** + * Keeps track of {@link Migration#startVersion}s and {@link Migration#endVersion}s added in + * {@link #addMigrations(Migration...)} for later validation that makes those versions don't + * match any versions passed to {@link #fallbackToDestructiveMigrationFrom(int...)}. + */ + private Set mMigrationStartAndEndVersions; + + private String mCopyFromAssetPath; + private File mCopyFromFile; + private Callable mCopyFromInputStream; + + Builder(@NonNull Context context, @NonNull Class klass, @Nullable String name) { + mContext = context; + mDatabaseClass = klass; + mName = name; + mJournalMode = JournalMode.AUTOMATIC; + mRequireMigration = true; + mMigrationContainer = new MigrationContainer(); + } + + /** + * Configures Room to create and open the database using a pre-packaged database located in + * the application 'assets/' folder. + *

    + * Room does not open the pre-packaged database, instead it copies it into the internal + * app database folder and then opens it. The pre-packaged database file must be located in + * the "assets/" folder of your application. For example, the path for a file located in + * "assets/databases/products.db" would be "databases/products.db". + *

    + * The pre-packaged database schema will be validated. It might be best to create your + * pre-packaged database schema utilizing the exported schema files generated when + * {@link Database#exportSchema()} is enabled. + *

    + * This method is not supported for an in memory database {@link Builder}. + * + * @param databaseFilePath The file path within the 'assets/' directory of where the + * database file is located. + * + * @return This {@link Builder} instance. + */ + @NonNull + public Builder createFromAsset(@NonNull String databaseFilePath) { + mCopyFromAssetPath = databaseFilePath; + return this; + } + + /** + * Configures Room to create and open the database using a pre-packaged database located in + * the application 'assets/' folder. + *

    + * Room does not open the pre-packaged database, instead it copies it into the internal + * app database folder and then opens it. The pre-packaged database file must be located in + * the "assets/" folder of your application. For example, the path for a file located in + * "assets/databases/products.db" would be "databases/products.db". + *

    + * The pre-packaged database schema will be validated. It might be best to create your + * pre-packaged database schema utilizing the exported schema files generated when + * {@link Database#exportSchema()} is enabled. + *

    + * This method is not supported for an in memory database {@link Builder}. + * + * @param databaseFilePath The file path within the 'assets/' directory of where the + * database file is located. + * @param callback The pre-packaged callback. + * + * @return This {@link Builder} instance. + */ + @NonNull + @SuppressLint("BuilderSetStyle") // To keep naming consistency. + public Builder createFromAsset( + @NonNull String databaseFilePath, + @NonNull PrepackagedDatabaseCallback callback) { + mPrepackagedDatabaseCallback = callback; + mCopyFromAssetPath = databaseFilePath; + return this; + } + + /** + * Configures Room to create and open the database using a pre-packaged database file. + *

    + * Room does not open the pre-packaged database, instead it copies it into the internal + * app database folder and then opens it. The given file must be accessible and the right + * permissions must be granted for Room to copy the file. + *

    + * The pre-packaged database schema will be validated. It might be best to create your + * pre-packaged database schema utilizing the exported schema files generated when + * {@link Database#exportSchema()} is enabled. + *

    + * The {@link Callback#onOpen(SupportSQLiteDatabase)} method can be used as an indicator + * that the pre-packaged database was successfully opened by Room and can be cleaned up. + *

    + * This method is not supported for an in memory database {@link Builder}. + * + * @param databaseFile The database file. + * + * @return This {@link Builder} instance. + */ + @NonNull + public Builder createFromFile(@NonNull File databaseFile) { + mCopyFromFile = databaseFile; + return this; + } + + /** + * Configures Room to create and open the database using a pre-packaged database file. + *

    + * Room does not open the pre-packaged database, instead it copies it into the internal + * app database folder and then opens it. The given file must be accessible and the right + * permissions must be granted for Room to copy the file. + *

    + * The pre-packaged database schema will be validated. It might be best to create your + * pre-packaged database schema utilizing the exported schema files generated when + * {@link Database#exportSchema()} is enabled. + *

    + * The {@link Callback#onOpen(SupportSQLiteDatabase)} method can be used as an indicator + * that the pre-packaged database was successfully opened by Room and can be cleaned up. + *

    + * This method is not supported for an in memory database {@link Builder}. + * + * @param databaseFile The database file. + * @param callback The pre-packaged callback. + * + * @return This {@link Builder} instance. + */ + @NonNull + @SuppressLint({"BuilderSetStyle", "StreamFiles"}) // To keep naming consistency. + public Builder createFromFile( + @NonNull File databaseFile, + @NonNull PrepackagedDatabaseCallback callback) { + mPrepackagedDatabaseCallback = callback; + mCopyFromFile = databaseFile; + return this; + } + + /** + * Configures Room to create and open the database using a pre-packaged database via an + * {@link InputStream}. + *

    + * This is useful for processing compressed database files. Room does not open the + * pre-packaged database, instead it copies it into the internal app database folder, and + * then open it. The {@link InputStream} will be closed once Room is done consuming it. + *

    + * The pre-packaged database schema will be validated. It might be best to create your + * pre-packaged database schema utilizing the exported schema files generated when + * {@link Database#exportSchema()} is enabled. + *

    + * The {@link Callback#onOpen(SupportSQLiteDatabase)} method can be used as an indicator + * that the pre-packaged database was successfully opened by Room and can be cleaned up. + *

    + * This method is not supported for an in memory database {@link Builder}. + * + * @param inputStreamCallable A callable that returns an InputStream from which to copy + * the database. The callable will be invoked in a thread from + * the Executor set via {@link #setQueryExecutor(Executor)}. The + * callable is only invoked if Room needs to create and open the + * database from the pre-package database, usually the first time + * it is created or during a destructive migration. + * + * @return This {@link Builder} instance. + */ + @NonNull + @SuppressLint("BuilderSetStyle") // To keep naming consistency. + public Builder createFromInputStream( + @NonNull Callable inputStreamCallable) { + mCopyFromInputStream = inputStreamCallable; + return this; + } + + /** + * Configures Room to create and open the database using a pre-packaged database via an + * {@link InputStream}. + *

    + * This is useful for processing compressed database files. Room does not open the + * pre-packaged database, instead it copies it into the internal app database folder, and + * then open it. The {@link InputStream} will be closed once Room is done consuming it. + *

    + * The pre-packaged database schema will be validated. It might be best to create your + * pre-packaged database schema utilizing the exported schema files generated when + * {@link Database#exportSchema()} is enabled. + *

    + * The {@link Callback#onOpen(SupportSQLiteDatabase)} method can be used as an indicator + * that the pre-packaged database was successfully opened by Room and can be cleaned up. + *

    + * This method is not supported for an in memory database {@link Builder}. + * + * @param inputStreamCallable A callable that returns an InputStream from which to copy + * the database. The callable will be invoked in a thread from + * the Executor set via {@link #setQueryExecutor(Executor)}. The + * callable is only invoked if Room needs to create and open the + * database from the pre-package database, usually the first time + * it is created or during a destructive migration. + * @param callback The pre-packaged callback. + * + * @return This {@link Builder} instance. + */ + @NonNull + @SuppressLint({"BuilderSetStyle", "LambdaLast"}) // To keep naming consistency. + public Builder createFromInputStream( + @NonNull Callable inputStreamCallable, + @NonNull PrepackagedDatabaseCallback callback) { + mPrepackagedDatabaseCallback = callback; + mCopyFromInputStream = inputStreamCallable; + return this; + } + + /** + * Sets the database factory. If not set, it defaults to + * {@link FrameworkSQLiteOpenHelperFactory}. + * + * @param factory The factory to use to access the database. + * @return This {@link Builder} instance. + */ + @NonNull + public Builder openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory) { + mFactory = factory; + return this; + } + + /** + * Adds a migration to the builder. + *

    + * Each Migration has a start and end versions and Room runs these migrations to bring the + * database to the latest version. + *

    + * If a migration item is missing between current version and the latest version, Room + * will clear the database and recreate so even if you have no changes between 2 versions, + * you should still provide a Migration object to the builder. + *

    + * A migration can handle more than 1 version (e.g. if you have a faster path to choose when + * going version 3 to 5 without going to version 4). If Room opens a database at version + * 3 and latest version is >= 5, Room will use the migration object that can migrate from + * 3 to 5 instead of 3 to 4 and 4 to 5. + * + * @param migrations The migration object that can modify the database and to the necessary + * changes. + * @return This {@link Builder} instance. + */ + @NonNull + public Builder addMigrations(@NonNull Migration... migrations) { + if (mMigrationStartAndEndVersions == null) { + mMigrationStartAndEndVersions = new HashSet<>(); + } + for (Migration migration : migrations) { + mMigrationStartAndEndVersions.add(migration.startVersion); + mMigrationStartAndEndVersions.add(migration.endVersion); + } + + mMigrationContainer.addMigrations(migrations); + return this; + } + + /** + * Adds an auto migration spec to the builder. + * + * @param autoMigrationSpec The auto migration object that is annotated with + * {@link AutoMigrationSpec} and is declared in an {@link AutoMigration} annotation. + * @return This {@link Builder} instance. + */ + @NonNull + @SuppressWarnings("MissingGetterMatchingBuilder") + public Builder addAutoMigrationSpec(@NonNull AutoMigrationSpec autoMigrationSpec) { + if (mAutoMigrationSpecs == null) { + mAutoMigrationSpecs = new ArrayList<>(); + } + mAutoMigrationSpecs.add(autoMigrationSpec); + return this; + } + + /** + * Disables the main thread query check for Room. + *

    + * Room ensures that Database is never accessed on the main thread because it may lock the + * main thread and trigger an ANR. If you need to access the database from the main thread, + * you should always use async alternatives or manually move the call to a background + * thread. + *

    + * You may want to turn this check off for testing. + * + * @return This {@link Builder} instance. + */ + @NonNull + public Builder allowMainThreadQueries() { + mAllowMainThreadQueries = true; + return this; + } + + /** + * Sets the journal mode for this database. + * + *

    + * This value is ignored if the builder is initialized with + * {@link Room#inMemoryDatabaseBuilder(Context, Class)}. + *

    + * The journal mode should be consistent across multiple instances of + * {@link RoomDatabase} for a single SQLite database file. + *

    + * The default value is {@link JournalMode#AUTOMATIC}. + * + * @param journalMode The journal mode. + * @return This {@link Builder} instance. + */ + @NonNull + public Builder setJournalMode(@NonNull JournalMode journalMode) { + mJournalMode = journalMode; + return this; + } + + /** + * Sets the {@link Executor} that will be used to execute all non-blocking asynchronous + * queries and tasks, including {@code LiveData} invalidation, {@code Flowable} scheduling + * and {@code ListenableFuture} tasks. + *

    + * When both the query executor and transaction executor are unset, then a default + * {@code Executor} will be used. The default {@code Executor} allocates and shares threads + * amongst Architecture Components libraries. If the query executor is unset but a + * transaction executor was set, then the same {@code Executor} will be used for queries. + *

    + * For best performance the given {@code Executor} should be bounded (max number of threads + * is limited). + *

    + * The input {@code Executor} cannot run tasks on the UI thread. + ** + * @return This {@link Builder} instance. + * + * @see #setTransactionExecutor(Executor) + */ + @NonNull + public Builder setQueryExecutor(@NonNull Executor executor) { + mQueryExecutor = executor; + return this; + } + + /** + * Sets the {@link Executor} that will be used to execute all non-blocking asynchronous + * transaction queries and tasks, including {@code LiveData} invalidation, {@code Flowable} + * scheduling and {@code ListenableFuture} tasks. + *

    + * When both the transaction executor and query executor are unset, then a default + * {@code Executor} will be used. The default {@code Executor} allocates and shares threads + * amongst Architecture Components libraries. If the transaction executor is unset but a + * query executor was set, then the same {@code Executor} will be used for transactions. + *

    + * If the given {@code Executor} is shared then it should be unbounded to avoid the + * possibility of a deadlock. Room will not use more than one thread at a time from this + * executor since only one transaction at a time can be executed, other transactions will + * be queued on a first come, first serve order. + *

    + * The input {@code Executor} cannot run tasks on the UI thread. + * + * @return This {@link Builder} instance. + * + * @see #setQueryExecutor(Executor) + */ + @NonNull + public Builder setTransactionExecutor(@NonNull Executor executor) { + mTransactionExecutor = executor; + return this; + } + + /** + * Sets whether table invalidation in this instance of {@link RoomDatabase} should be + * broadcast and synchronized with other instances of the same {@link RoomDatabase}, + * including those in a separate process. In order to enable multi-instance invalidation, + * this has to be turned on both ends. + *

    + * This is not enabled by default. + *

    + * This does not work for in-memory databases. This does not work between database instances + * targeting different database files. + * + * @return This {@link Builder} instance. + */ + @NonNull + public Builder enableMultiInstanceInvalidation() { + mMultiInstanceInvalidationIntent = mName != null ? new Intent(mContext, + MultiInstanceInvalidationService.class) : null; + return this; + } + + /** + * Sets whether table invalidation in this instance of {@link RoomDatabase} should be + * broadcast and synchronized with other instances of the same {@link RoomDatabase}, + * including those in a separate process. In order to enable multi-instance invalidation, + * this has to be turned on both ends and need to point to the same + * {@link MultiInstanceInvalidationService}. + *

    + * This is not enabled by default. + *

    + * This does not work for in-memory databases. This does not work between database instances + * targeting different database files. + * + * @return This {@link Builder} instance. + * @param invalidationServiceIntent Intent to bind to the + * {@link MultiInstanceInvalidationService}. + */ + @SuppressWarnings("MissingGetterMatchingBuilder") + @NonNull + @ExperimentalRoomApi + public Builder setMultiInstanceInvalidationServiceIntent( + @NonNull Intent invalidationServiceIntent) { + mMultiInstanceInvalidationIntent = mName != null ? invalidationServiceIntent : null; + return this; + } + + /** + * Allows Room to destructively recreate database tables if {@link Migration}s that would + * migrate old database schemas to the latest schema version are not found. + *

    + * When the database version on the device does not match the latest schema version, Room + * runs necessary {@link Migration}s on the database. + *

    + * If it cannot find the set of {@link Migration}s that will bring the database to the + * current version, it will throw an {@link IllegalStateException}. + *

    + * You can call this method to change this behavior to re-create the database instead of + * crashing. + *

    + * If the database was create from an asset or a file then Room will try to use the same + * file to re-create the database, otherwise this will delete all of the data in the + * database tables managed by Room. + *

    + * To let Room fallback to destructive migration only during a schema downgrade then use + * {@link #fallbackToDestructiveMigrationOnDowngrade()}. + * + * @return This {@link Builder} instance. + * + * @see #fallbackToDestructiveMigrationOnDowngrade() + */ + @NonNull + public Builder fallbackToDestructiveMigration() { + mRequireMigration = false; + mAllowDestructiveMigrationOnDowngrade = true; + return this; + } + + /** + * Allows Room to destructively recreate database tables if {@link Migration}s are not + * available when downgrading to old schema versions. + * + * @return This {@link Builder} instance. + * + * @see Builder#fallbackToDestructiveMigration() + */ + @NonNull + public Builder fallbackToDestructiveMigrationOnDowngrade() { + mRequireMigration = true; + mAllowDestructiveMigrationOnDowngrade = true; + return this; + } + + /** + * Informs Room that it is allowed to destructively recreate database tables from specific + * starting schema versions. + *

    + * This functionality is the same as that provided by + * {@link #fallbackToDestructiveMigration()}, except that this method allows the + * specification of a set of schema versions for which destructive recreation is allowed. + *

    + * Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want + * to allow destructive migrations from some schema versions while still taking advantage + * of exceptions being thrown due to unintentionally missing migrations. + *

    + * Note: No versions passed to this method may also exist as either starting or ending + * versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a + * version passed to this method is found as a starting or ending version in a Migration, an + * exception will be thrown. + * + * @param startVersions The set of schema versions from which Room should use a destructive + * migration. + * @return This {@link Builder} instance. + */ + @NonNull + public Builder fallbackToDestructiveMigrationFrom(int... startVersions) { + if (mMigrationsNotRequiredFrom == null) { + mMigrationsNotRequiredFrom = new HashSet<>(startVersions.length); + } + for (int startVersion : startVersions) { + mMigrationsNotRequiredFrom.add(startVersion); + } + return this; + } + + /** + * Adds a {@link Callback} to this database. + * + * @param callback The callback. + * @return This {@link Builder} instance. + */ + @NonNull + public Builder addCallback(@NonNull Callback callback) { + if (mCallbacks == null) { + mCallbacks = new ArrayList<>(); + } + mCallbacks.add(callback); + return this; + } + + /** + * Sets a {@link QueryCallback} to be invoked when queries are executed. + *

    + * The callback is invoked whenever a query is executed, note that adding this callback + * has a small cost and should be avoided in production builds unless needed. + *

    + * A use case for providing a callback is to allow logging executed queries. When the + * callback implementation logs then it is recommended to use an immediate executor. + * + * @param queryCallback The query callback. + * @param executor The executor on which the query callback will be invoked. + */ + @SuppressWarnings("MissingGetterMatchingBuilder") + @NonNull + public Builder setQueryCallback(@NonNull QueryCallback queryCallback, + @NonNull Executor executor) { + mQueryCallback = queryCallback; + mQueryCallbackExecutor = executor; + return this; + } + + /** + * Adds a type converter instance to this database. + * + * @param typeConverter The converter. It must be an instance of a class annotated with + * {@link ProvidedTypeConverter} otherwise Room will throw an exception. + * @return This {@link Builder} instance. + */ + @NonNull + public Builder addTypeConverter(@NonNull Object typeConverter) { + if (mTypeConverters == null) { + mTypeConverters = new ArrayList<>(); + } + mTypeConverters.add(typeConverter); + return this; + } + + /** + * Enables auto-closing for the database to free up unused resources. The underlying + * database will be closed after it's last use after the specified {@code + * autoCloseTimeout} has elapsed since its last usage. The database will be automatically + * re-opened the next time it is accessed. + *

    + * Auto-closing is not compatible with in-memory databases since the data will be lost + * when the database is auto-closed. + *

    + * Also, temp tables and temp triggers will be cleared each time the database is + * auto-closed. If you need to use them, please include them in your + * {@link RoomDatabase.Callback#onOpen callback}. + *

    + * All configuration should happen in your {@link RoomDatabase.Callback#onOpen} + * callback so it is re-applied every time the database is re-opened. Note that the + * {@link RoomDatabase.Callback#onOpen} will be called every time the database is re-opened. + *

    + * The auto-closing database operation runs on the query executor. + *

    + * The database will not be reopened if the RoomDatabase or the + * SupportSqliteOpenHelper is closed manually (by calling + * {@link RoomDatabase#close()} or {@link SupportSQLiteOpenHelper#close()}. If the + * database is closed manually, you must create a new database using + * {@link RoomDatabase.Builder#build()}. + * + * @param autoCloseTimeout the amount of time after the last usage before closing the + * database. Must greater or equal to zero. + * @param autoCloseTimeUnit the timeunit for autoCloseTimeout. + * @return This {@link Builder} instance + */ + @NonNull + @SuppressWarnings("MissingGetterMatchingBuilder") + @ExperimentalRoomApi // When experimental is removed, add these parameters to + // DatabaseConfiguration + public Builder setAutoCloseTimeout( + @IntRange(from = 0) long autoCloseTimeout, @NonNull TimeUnit autoCloseTimeUnit) { + if (autoCloseTimeout < 0) { + throw new IllegalArgumentException("autoCloseTimeout must be >= 0"); + } + mAutoCloseTimeout = autoCloseTimeout; + mAutoCloseTimeUnit = autoCloseTimeUnit; + return this; + } + + /** + * Creates the databases and initializes it. + *

    + * By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive + * triggers. + * + * @return A new database instance. + */ + @SuppressLint("RestrictedApi") + @NonNull + public T build() { + //noinspection ConstantConditions + if (mContext == null) { + throw new IllegalArgumentException("Cannot provide null context for the database."); + } + //noinspection ConstantConditions + if (mDatabaseClass == null) { + throw new IllegalArgumentException("Must provide an abstract class that" + + " extends RoomDatabase"); + } + if (mQueryExecutor == null && mTransactionExecutor == null) { + mQueryExecutor = mTransactionExecutor = ArchTaskExecutor.getIOThreadExecutor(); + } else if (mQueryExecutor != null && mTransactionExecutor == null) { + mTransactionExecutor = mQueryExecutor; + } else if (mQueryExecutor == null && mTransactionExecutor != null) { + mQueryExecutor = mTransactionExecutor; + } + + if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) { + for (Integer version : mMigrationStartAndEndVersions) { + if (mMigrationsNotRequiredFrom.contains(version)) { + throw new IllegalArgumentException( + "Inconsistency detected. A Migration was supplied to " + + "addMigration(Migration... migrations) that has a start " + + "or end version equal to a start version supplied to " + + "fallbackToDestructiveMigrationFrom(int... " + + "startVersions). Start version: " + + version); + } + } + } + + SupportSQLiteOpenHelper.Factory factory; + + AutoCloser autoCloser = null; + + if (mFactory == null) { + factory = new FrameworkSQLiteOpenHelperFactory(); + } else { + factory = mFactory; + } + + if (mAutoCloseTimeout > 0) { + if (mName == null) { + throw new IllegalArgumentException("Cannot create auto-closing database for " + + "an in-memory database."); + } + + autoCloser = new AutoCloser(mAutoCloseTimeout, mAutoCloseTimeUnit, + mTransactionExecutor); + + factory = new AutoClosingRoomOpenHelperFactory(factory, autoCloser); + } + + if (mCopyFromAssetPath != null + || mCopyFromFile != null + || mCopyFromInputStream != null) { + if (mName == null) { + throw new IllegalArgumentException("Cannot create from asset or file for an " + + "in-memory database."); + } + + final int copyConfigurations = (mCopyFromAssetPath == null ? 0 : 1) + + (mCopyFromFile == null ? 0 : 1) + + (mCopyFromInputStream == null ? 0 : 1); + if (copyConfigurations != 1) { + throw new IllegalArgumentException("More than one of createFromAsset(), " + + "createFromInputStream(), and createFromFile() were called on this " + + "Builder, but the database can only be created using one of the " + + "three configurations."); + } + factory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile, + mCopyFromInputStream, factory); + } + + if (mQueryCallback != null) { + factory = new QueryInterceptorOpenHelperFactory(factory, mQueryCallback, + mQueryCallbackExecutor); + } + + DatabaseConfiguration configuration = + new DatabaseConfiguration( + mContext, + mName, + factory, + mMigrationContainer, + mCallbacks, + mAllowMainThreadQueries, + mJournalMode.resolve(mContext), + mQueryExecutor, + mTransactionExecutor, + mMultiInstanceInvalidationIntent, + mRequireMigration, + mAllowDestructiveMigrationOnDowngrade, + mMigrationsNotRequiredFrom, + mCopyFromAssetPath, + mCopyFromFile, + mCopyFromInputStream, + mPrepackagedDatabaseCallback, + mTypeConverters, + mAutoMigrationSpecs); + T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX); + db.init(configuration); + return db; + } + } + + /** + * A container to hold migrations. It also allows querying its contents to find migrations + * between two versions. + */ + public static class MigrationContainer { + private HashMap> mMigrations = new HashMap<>(); + + /** + * Adds the given migrations to the list of available migrations. If 2 migrations have the + * same start-end versions, the latter migration overrides the previous one. + * + * @param migrations List of available migrations. + */ + public void addMigrations(@NonNull Migration... migrations) { + for (Migration migration : migrations) { + addMigration(migration); + } + } + + /** + * Adds the given migrations to the list of available migrations. If 2 migrations have the + * same start-end versions, the latter migration overrides the previous one. + * + * @param migrations List of available migrations. + */ + public void addMigrations(@NonNull List migrations) { + for (Migration migration : migrations) { + addMigration(migration); + } + } + + private void addMigration(Migration migration) { + final int start = migration.startVersion; + final int end = migration.endVersion; + TreeMap targetMap = mMigrations.get(start); + if (targetMap == null) { + targetMap = new TreeMap<>(); + mMigrations.put(start, targetMap); + } + Migration existing = targetMap.get(end); + if (existing != null) { + Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration); + } + targetMap.put(end, migration); + } + + /** + * Returns the map of available migrations where the key is the start version of the + * migration, and the value is a map of (end version -> Migration). + * + * @return Map of migrations keyed by the start version + */ + @NonNull + public Map> getMigrations() { + return Collections.unmodifiableMap(mMigrations); + } + + /** + * Finds the list of migrations that should be run to move from {@code start} version to + * {@code end} version. + * + * @param start The current database version + * @param end The target database version + * @return An ordered list of {@link Migration} objects that should be run to migrate + * between the given versions. If a migration path cannot be found, returns {@code null}. + */ + @SuppressWarnings("WeakerAccess") + @Nullable + public List findMigrationPath(int start, int end) { + if (start == end) { + return Collections.emptyList(); + } + boolean migrateUp = end > start; + List result = new ArrayList<>(); + return findUpMigrationPath(result, migrateUp, start, end); + } + + private List findUpMigrationPath(List result, boolean upgrade, + int start, int end) { + while (upgrade ? start < end : start > end) { + TreeMap targetNodes = mMigrations.get(start); + if (targetNodes == null) { + return null; + } + // keys are ordered so we can start searching from one end of them. + Set keySet; + if (upgrade) { + keySet = targetNodes.descendingKeySet(); + } else { + keySet = targetNodes.keySet(); + } + boolean found = false; + for (int targetVersion : keySet) { + final boolean shouldAddToPath; + if (upgrade) { + shouldAddToPath = targetVersion <= end && targetVersion > start; + } else { + shouldAddToPath = targetVersion >= end && targetVersion < start; + } + if (shouldAddToPath) { + result.add(targetNodes.get(targetVersion)); + start = targetVersion; + found = true; + break; + } + } + if (!found) { + return null; + } + } + return result; + } + } + + /** Returns true if the calling thread is the main thread. */ + private static boolean isMainThread() { + return Looper.getMainLooper().getThread() == Thread.currentThread(); + } + + /** + * Callback for {@link RoomDatabase}. + */ + public abstract static class Callback { + + /** + * Called when the database is created for the first time. This is called after all the + * tables are created. + * + * @param db The database. + */ + public void onCreate(@NonNull SupportSQLiteDatabase db) { + } + + /** + * Called when the database has been opened. + * + * @param db The database. + */ + public void onOpen(@NonNull SupportSQLiteDatabase db) { + } + + /** + * Called after the database was destructively migrated + * + * @param db The database. + */ + public void onDestructiveMigration(@NonNull SupportSQLiteDatabase db){ + } + } + + /** + * Callback for {@link Builder#createFromAsset(String)}, {@link Builder#createFromFile(File)} + * and {@link Builder#createFromInputStream(Callable)} + *

    + * This callback will be invoked after the pre-package DB is copied but before Room had + * a chance to open it and therefore before the {@link RoomDatabase.Callback} methods are + * invoked. This callback can be useful for updating the pre-package DB schema to satisfy + * Room's schema validation. + */ + public abstract static class PrepackagedDatabaseCallback { + + /** + * Called when the pre-packaged database has been copied. + * + * @param db The database. + */ + public void onOpenPrepackagedDatabase(@NonNull SupportSQLiteDatabase db) { + } + } + + /** + * Callback interface for when SQLite queries are executed. + * + * @see RoomDatabase.Builder#setQueryCallback + */ + public interface QueryCallback { + + /** + * Called when a SQL query is executed. + * + * @param sqlQuery The SQLite query statement. + * @param bindArgs Arguments of the query if available, empty list otherwise. + */ + void onQuery(@NonNull String sqlQuery, @NonNull List + bindArgs); + } +} diff --git a/app/src/main/java/androidx/room/RoomDatabase.kt b/app/src/main/java/androidx/room/RoomDatabase.kt deleted file mode 100644 index bc86569549..0000000000 --- a/app/src/main/java/androidx/room/RoomDatabase.kt +++ /dev/null @@ -1,1547 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.room - -import android.annotation.SuppressLint -import android.app.ActivityManager -import android.content.Context -import android.content.Intent -import android.database.Cursor -import android.os.Build -import android.os.CancellationSignal -import android.os.Looper -import android.util.Log -import androidx.annotation.CallSuper -import androidx.annotation.IntRange -import androidx.annotation.RestrictTo -import androidx.annotation.WorkerThread -import androidx.arch.core.executor.ArchTaskExecutor -import androidx.room.Room.LOG_TAG -import androidx.room.migration.AutoMigrationSpec -import androidx.room.migration.Migration -import androidx.sqlite.db.SimpleSQLiteQuery -import androidx.sqlite.db.SupportSQLiteCompat -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import androidx.sqlite.db.SupportSQLiteQuery -import androidx.sqlite.db.SupportSQLiteStatement -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory -import java.io.File -import java.io.InputStream -import java.util.BitSet -import java.util.Collections -import java.util.TreeMap -import java.util.concurrent.Callable -import java.util.concurrent.Executor -import java.util.concurrent.TimeUnit -import java.util.concurrent.locks.Lock -import java.util.concurrent.locks.ReentrantReadWriteLock - -/** - * Base class for all Room databases. All classes that are annotated with [Database] must - * extend this class. - * - * RoomDatabase provides direct access to the underlying database implementation but you should - * prefer using [Dao] classes. - * - * @constructor You cannot create an instance of a database, instead, you should acquire it via - * [#Room.databaseBuilder] or - * [#Room.inMemoryDatabaseBuilder]. - * - * @see Database - */ -abstract class RoomDatabase { - /** - * Set by the generated open helper. - * - */ - @Volatile - @Deprecated("Will be hidden in the next release.") - @JvmField - protected var mDatabase: SupportSQLiteDatabase? = null - - /** - * The Executor in use by this database for async queries. - */ - open val queryExecutor: Executor - get() = internalQueryExecutor - - private lateinit var internalQueryExecutor: Executor - - /** - * The Executor in use by this database for async transactions. - */ - open val transactionExecutor: Executor - get() = internalTransactionExecutor - - private lateinit var internalTransactionExecutor: Executor - - /** - * The SQLite open helper used by this database. - */ - open val openHelper: SupportSQLiteOpenHelper - get() = internalOpenHelper - - private lateinit var internalOpenHelper: SupportSQLiteOpenHelper - - /** - * The invalidation tracker for this database. - * - * You can use the invalidation tracker to get notified when certain tables in the database - * are modified. - * - * @return The invalidation tracker for the database. - */ - open val invalidationTracker: InvalidationTracker = createInvalidationTracker() - private var allowMainThreadQueries = false - private var writeAheadLoggingEnabled = false - - /** - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - @Deprecated("Will be hidden in a future release.") - @JvmField - protected var mCallbacks: List? = null - - /** - * A map of auto migration spec classes to their provided instance. - * - */ - @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - protected var autoMigrationSpecs: MutableMap, AutoMigrationSpec> = - mutableMapOf() - private val readWriteLock = ReentrantReadWriteLock() - private var autoCloser: AutoCloser? = null - - /** - * [InvalidationTracker] uses this lock to prevent the database from closing while it is - * querying database updates. - * - * The returned lock is reentrant and will allow multiple threads to acquire the lock - * simultaneously until [close] is invoked in which the lock becomes exclusive as - * a way to let the InvalidationTracker finish its work before closing the database. - * - * @return The lock for [close]. - */ - internal fun getCloseLock(): Lock { - return readWriteLock.readLock() - } - - /** - * Suspending transaction id of the current thread. - * - * This id is only set on threads that are used to dispatch coroutines within a suspending - * database transaction. - */ - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - val suspendingTransactionId = ThreadLocal() - - /** - * Gets the map for storing extension properties of Kotlin type. - * - */ - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - val backingFieldMap: MutableMap = Collections.synchronizedMap(mutableMapOf()) - - private val typeConverters: MutableMap, Any> = mutableMapOf() - - /** - * Gets the instance of the given Type Converter. - * - * @param klass The Type Converter class. - * @param T The type of the expected Type Converter subclass. - * @return An instance of T if it is provided in the builder. - */ - @Suppress("UNCHECKED_CAST") - open fun getTypeConverter(klass: Class): T? { - return typeConverters[klass] as T? - } - - /** - * Called by Room when it is initialized. - * - * @throws IllegalArgumentException if initialization fails. - * - * @param configuration The database configuration. - */ - @CallSuper - @Suppress("DEPRECATION") - open fun init(configuration: DatabaseConfiguration) { - internalOpenHelper = createOpenHelper(configuration) - val requiredAutoMigrationSpecs = getRequiredAutoMigrationSpecs() - val usedSpecs = BitSet() - for (spec in requiredAutoMigrationSpecs) { - var foundIndex = -1 - for (providedIndex in configuration.autoMigrationSpecs.indices.reversed()) { - val provided: Any = configuration.autoMigrationSpecs[providedIndex] - if (spec.isAssignableFrom(provided.javaClass)) { - foundIndex = providedIndex - usedSpecs.set(foundIndex) - break - } - } - require(foundIndex >= 0) { - "A required auto migration spec (${spec.canonicalName}) is missing in the " + - "database configuration." - } - autoMigrationSpecs[spec] = configuration.autoMigrationSpecs[foundIndex] - } - for (providedIndex in configuration.autoMigrationSpecs.indices.reversed()) { - require(usedSpecs[providedIndex]) { - "Unexpected auto migration specs found. " + - "Annotate AutoMigrationSpec implementation with " + - "@ProvidedAutoMigrationSpec annotation or remove this spec from the " + - "builder." - } - } - val autoMigrations = getAutoMigrations(autoMigrationSpecs) - for (autoMigration in autoMigrations) { - val migrationExists = configuration.migrationContainer.contains( - autoMigration.startVersion, - autoMigration.endVersion - ) - if (!migrationExists) { - configuration.migrationContainer.addMigrations(autoMigration) - } - } - - // Configure SqliteCopyOpenHelper if it is available: - unwrapOpenHelper( - clazz = SQLiteCopyOpenHelper::class.java, - openHelper = openHelper - )?.setDatabaseConfiguration(configuration) - - unwrapOpenHelper( - clazz = AutoClosingRoomOpenHelper::class.java, - openHelper = openHelper - )?.let { - autoCloser = it.autoCloser - invalidationTracker.setAutoCloser(it.autoCloser) - } - - val wal = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - val enabled = configuration.journalMode == JournalMode.WRITE_AHEAD_LOGGING - openHelper.setWriteAheadLoggingEnabled(enabled) - enabled - } else { - false - } - mCallbacks = configuration.callbacks - internalQueryExecutor = configuration.queryExecutor - internalTransactionExecutor = TransactionExecutor(configuration.transactionExecutor) - allowMainThreadQueries = configuration.allowMainThreadQueries - writeAheadLoggingEnabled = wal - if (configuration.multiInstanceInvalidationServiceIntent != null) { - requireNotNull(configuration.name) - invalidationTracker.startMultiInstanceInvalidation( - configuration.context, - configuration.name, - configuration.multiInstanceInvalidationServiceIntent - ) - } - val requiredFactories = getRequiredTypeConverters() - // indices for each converter on whether it is used or not so that we can throw an exception - // if developer provides an unused converter. It is not necessarily an error but likely - // to be because why would developer add a converter if it won't be used? - val used = BitSet() - requiredFactories.forEach { (daoName, converters) -> - for (converter in converters) { - var foundIndex = -1 - // traverse provided converters in reverse so that newer one overrides - for (providedIndex in configuration.typeConverters.indices.reversed()) { - val provided = configuration.typeConverters[providedIndex] - if (converter.isAssignableFrom(provided.javaClass)) { - foundIndex = providedIndex - used.set(foundIndex) - break - } - } - require(foundIndex >= 0) { - "A required type converter ($converter) for" + - " ${daoName.canonicalName} is missing in the database configuration." - } - typeConverters[converter] = configuration.typeConverters[foundIndex] - } - } - // now, make sure all provided factories are used - for (providedIndex in configuration.typeConverters.indices.reversed()) { - if (!used[providedIndex]) { - val converter = configuration.typeConverters[providedIndex] - throw IllegalArgumentException( - "Unexpected type converter $converter. " + - "Annotate TypeConverter class with @ProvidedTypeConverter annotation " + - "or remove this converter from the builder." - ) - } - } - } - - /** - * Returns a list of [Migration] of a database that have been automatically generated. - * - * @return A list of migration instances each of which is a generated autoMigration - * @param autoMigrationSpecs - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - @JvmSuppressWildcards // Suppress wildcards due to generated Java code - open fun getAutoMigrations( - autoMigrationSpecs: Map, AutoMigrationSpec> - ): List { - return emptyList() - } - - /** - * Unwraps (delegating) open helpers until it finds clazz, otherwise returns null. - * - * @param clazz the open helper type to search for - * @param openHelper the open helper to search through - * @param T the type of clazz - * @return the instance of clazz, otherwise null - */ - @Suppress("UNCHECKED_CAST") - private fun unwrapOpenHelper(clazz: Class, openHelper: SupportSQLiteOpenHelper): T? { - if (clazz.isInstance(openHelper)) { - return openHelper as T - } - return if (openHelper is DelegatingOpenHelper) { - unwrapOpenHelper( - clazz = clazz, - openHelper = openHelper.delegate - ) - } else null - } - - /** - * Creates the open helper to access the database. Generated class already implements this - * method. - * Note that this method is called when the RoomDatabase is initialized. - * - * @param config The configuration of the Room database. - * @return A new SupportSQLiteOpenHelper to be used while connecting to the database. - */ - protected abstract fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper - - /** - * Called when the RoomDatabase is created. - * - * This is already implemented by the generated code. - * - * @return Creates a new InvalidationTracker. - */ - protected abstract fun createInvalidationTracker(): InvalidationTracker - - /** - * Returns a Map of String -> List<Class> where each entry has the `key` as the DAO name - * and `value` as the list of type converter classes that are necessary for the database to - * function. - * - * This is implemented by the generated code. - * - * @return Creates a map that will include all required type converters for this database. - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - protected open fun getRequiredTypeConverters(): Map, List>> { - return emptyMap() - } - - /** - * Returns a Set of required AutoMigrationSpec classes. - * - * This is implemented by the generated code. - * - * @return Creates a set that will include all required auto migration specs for this database. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - open fun getRequiredAutoMigrationSpecs(): Set> { - return emptySet() - } - - /** - * Deletes all rows from all the tables that are registered to this database as - * [Database.entities]. - * - * This does NOT reset the auto-increment value generated by [PrimaryKey.autoGenerate]. - * - * After deleting the rows, Room will set a WAL checkpoint and run VACUUM. This means that the - * data is completely erased. The space will be reclaimed by the system if the amount surpasses - * the threshold of database file size. - * - * See SQLite documentation for details. [FileFormat](https://www.sqlite.org/fileformat.html) - */ - @WorkerThread - abstract fun clearAllTables() - - /** - * True if database connection is open and initialized. - * - * When Room is configured with [RoomDatabase.Builder.setAutoCloseTimeout] the database - * is considered open even if internally the connection has been closed, unless manually closed. - * - * @return true if the database connection is open, false otherwise. - */ - @Suppress("Deprecation") // Due to usage of `mDatabase` - open val isOpen: Boolean - get() = (autoCloser?.isActive ?: mDatabase?.isOpen) == true - - /** - * True if the actual database connection is open, regardless of auto-close. - */ - @Suppress("Deprecation") // Due to usage of `mDatabase` - @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - val isOpenInternal: Boolean - get() = mDatabase?.isOpen == true - - /** - * Closes the database if it is already open. - */ - open fun close() { - if (isOpen) { - val closeLock: Lock = readWriteLock.writeLock() - closeLock.lock() - try { - invalidationTracker.stopMultiInstanceInvalidation() - openHelper.close() - } finally { - closeLock.unlock() - } - } - } - - /** True if the calling thread is the main thread. */ - internal val isMainThread: Boolean - get() = Looper.getMainLooper().thread === Thread.currentThread() - - /** - * Asserts that we are not on the main thread. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) // used in generated code - open fun assertNotMainThread() { - if (allowMainThreadQueries) { - return - } - check(!isMainThread) { - "Cannot access database on the main thread since" + - " it may potentially lock the UI for a long period of time." - } - } - - /** - * Asserts that we are not on a suspending transaction. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // used in generated code - open fun assertNotSuspendingTransaction() { - check(inTransaction() || suspendingTransactionId.get() == null) { - "Cannot access database on a different coroutine" + - " context inherited from a suspending transaction." - } - } - // Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which - // methods we are using and also helps unit tests to mock this class without mocking - // all SQLite database methods. - /** - * Convenience method to query the database with arguments. - * - * @param query The sql query - * @param args The bind arguments for the placeholders in the query - * @return A Cursor obtained by running the given query in the Room database. - */ - open fun query(query: String, args: Array?): Cursor { - return openHelper.writableDatabase.query(SimpleSQLiteQuery(query, args)) - } - - /** - * Wrapper for [SupportSQLiteDatabase.query]. - * - * @param query The Query which includes the SQL and a bind callback for bind arguments. - * @param signal The cancellation signal to be attached to the query. - * @return Result of the query. - */ - @JvmOverloads - open fun query(query: SupportSQLiteQuery, signal: CancellationSignal? = null): Cursor { - assertNotMainThread() - assertNotSuspendingTransaction() - return if (signal != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - openHelper.writableDatabase.query(query, signal) - } else { - openHelper.writableDatabase.query(query) - } - } - - /** - * Wrapper for [SupportSQLiteDatabase.compileStatement]. - * - * @param sql The query to compile. - * @return The compiled query. - */ - open fun compileStatement(sql: String): SupportSQLiteStatement { - assertNotMainThread() - assertNotSuspendingTransaction() - return openHelper.writableDatabase.compileStatement(sql) - } - - /** - * Wrapper for [SupportSQLiteDatabase.beginTransaction]. - */ - @Deprecated( - "beginTransaction() is deprecated", - ReplaceWith("runInTransaction(Runnable)") - ) - open fun beginTransaction() { - assertNotMainThread() - val autoCloser = autoCloser - if (autoCloser == null) { - internalBeginTransaction() - } else { - autoCloser.executeRefCountingFunction { - internalBeginTransaction() - null - } - } - } - - private fun internalBeginTransaction() { - assertNotMainThread() - val database = openHelper.writableDatabase - invalidationTracker.syncTriggers(database) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && - database.isWriteAheadLoggingEnabled - ) { - database.beginTransactionNonExclusive() - } else { - database.beginTransaction() - } - } - - /** - * Wrapper for [SupportSQLiteDatabase.endTransaction]. - */ - @Deprecated( - "endTransaction() is deprecated", - ReplaceWith("runInTransaction(Runnable)") - ) - open fun endTransaction() { - val autoCloser = autoCloser - if (autoCloser == null) { - internalEndTransaction() - } else { - autoCloser.executeRefCountingFunction { - internalEndTransaction() - null - } - } - } - - private fun internalEndTransaction() { - openHelper.writableDatabase.endTransaction() - if (!inTransaction()) { - // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last - // endTransaction call to do it. - invalidationTracker.refreshVersionsAsync() - } - } - - /** - * Wrapper for [SupportSQLiteDatabase.setTransactionSuccessful]. - * - */ - @Deprecated( - "setTransactionSuccessful() is deprecated", - ReplaceWith("runInTransaction(Runnable)") - ) - open fun setTransactionSuccessful() { - openHelper.writableDatabase.setTransactionSuccessful() - } - - /** - * Executes the specified [Runnable] in a database transaction. The transaction will be - * marked as successful unless an exception is thrown in the [Runnable]. - * - * Room will only perform at most one transaction at a time. - * - * @param body The piece of code to execute. - */ - @Suppress("DEPRECATION") - open fun runInTransaction(body: Runnable) { - beginTransaction() - try { - body.run() - setTransactionSuccessful() - } finally { - endTransaction() - } - } - - /** - * Executes the specified [Callable] in a database transaction. The transaction will be - * marked as successful unless an exception is thrown in the [Callable]. - * - * Room will only perform at most one transaction at a time. - * - * @param body The piece of code to execute. - * @param V The type of the return value. - * @return The value returned from the [Callable]. - */ - @Suppress("DEPRECATION") - open fun runInTransaction(body: Callable): V { - beginTransaction() - return try { - val result = body.call() - setTransactionSuccessful() - result - } finally { - endTransaction() - } - } - - /** - * Called by the generated code when database is open. - * - * You should never call this method manually. - * - * @param db The database instance. - */ - protected open fun internalInitInvalidationTracker(db: SupportSQLiteDatabase) { - invalidationTracker.internalInit(db) - } - - /** - * Returns true if current thread is in a transaction. - * - * @return True if there is an active transaction in current thread, false otherwise. - * @see SupportSQLiteDatabase.inTransaction - */ - open fun inTransaction(): Boolean { - return openHelper.writableDatabase.inTransaction() - } - - /** - * Journal modes for SQLite database. - * - * @see Builder.setJournalMode - */ - enum class JournalMode { - /** - * Let Room choose the journal mode. This is the default value when no explicit value is - * specified. - * - * The actual value will be [TRUNCATE] when the device runs API Level lower than 16 - * or it is a low-RAM device. Otherwise, [WRITE_AHEAD_LOGGING] will be used. - */ - AUTOMATIC, - - /** - * Truncate journal mode. - */ - TRUNCATE, - - /** - * Write-Ahead Logging mode. - */ - WRITE_AHEAD_LOGGING; - - /** - * Resolves [AUTOMATIC] to either [TRUNCATE] or [WRITE_AHEAD_LOGGING]. - */ - internal fun resolve(context: Context): JournalMode { - if (this != AUTOMATIC) { - return this - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - val manager = - context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager - if (manager != null && !isLowRamDevice(manager)) { - return WRITE_AHEAD_LOGGING - } - } - return TRUNCATE - } - - private fun isLowRamDevice(activityManager: ActivityManager) = - if (Build.VERSION.SDK_INT >= 19) { - SupportSQLiteCompat.Api19Impl.isLowRamDevice(activityManager) - } else { - false - } - } - - /** - * Builder for RoomDatabase. - * - * @param T The type of the abstract database class. - */ - @Suppress("GetterOnBuilder") // To keep ABI compatibility from Java - open class Builder internal constructor( - private val context: Context, - private val klass: Class, - private val name: String? - ) { - private val callbacks: MutableList = mutableListOf() - private var prepackagedDatabaseCallback: PrepackagedDatabaseCallback? = null - private var queryCallback: QueryCallback? = null - private var queryCallbackExecutor: Executor? = null - private val typeConverters: MutableList = mutableListOf() - private var autoMigrationSpecs: MutableList = mutableListOf() - - private var queryExecutor: Executor? = null - - private var transactionExecutor: Executor? = null - private var factory: SupportSQLiteOpenHelper.Factory? = null - private var allowMainThreadQueries = false - private var journalMode: JournalMode = JournalMode.AUTOMATIC - private var multiInstanceInvalidationIntent: Intent? = null - private var requireMigration: Boolean = true - private var allowDestructiveMigrationOnDowngrade = false - private var autoCloseTimeout = -1L - private var autoCloseTimeUnit: TimeUnit? = null - - /** - * Migrations, mapped by from-to pairs. - */ - private val migrationContainer: MigrationContainer = MigrationContainer() - private var migrationsNotRequiredFrom: MutableSet = mutableSetOf() - - /** - * Keeps track of [Migration.startVersion]s and [Migration.endVersion]s added in - * [addMigrations] for later validation that makes those versions don't - * match any versions passed to [fallbackToDestructiveMigrationFrom]. - */ - private var migrationStartAndEndVersions: MutableSet? = null - private var copyFromAssetPath: String? = null - private var copyFromFile: File? = null - private var copyFromInputStream: Callable? = null - - /** - * Configures Room to create and open the database using a pre-packaged database located in - * the application 'assets/' folder. - * - * Room does not open the pre-packaged database, instead it copies it into the internal - * app database folder and then opens it. The pre-packaged database file must be located in - * the "assets/" folder of your application. For example, the path for a file located in - * "assets/databases/products.db" would be "databases/products.db". - * - * The pre-packaged database schema will be validated. It might be best to create your - * pre-packaged database schema utilizing the exported schema files generated when - * [Database.exportSchema] is enabled. - * - * This method is not supported for an in memory database [Builder]. - * - * @param databaseFilePath The file path within the 'assets/' directory of where the - * database file is located. - * - * @return This builder instance. - */ - open fun createFromAsset(databaseFilePath: String) = apply { - this.copyFromAssetPath = databaseFilePath - } - - /** - * Configures Room to create and open the database using a pre-packaged database located in - * the application 'assets/' folder. - * - * Room does not open the pre-packaged database, instead it copies it into the internal - * app database folder and then opens it. The pre-packaged database file must be located in - * the "assets/" folder of your application. For example, the path for a file located in - * "assets/databases/products.db" would be "databases/products.db". - * - * The pre-packaged database schema will be validated. It might be best to create your - * pre-packaged database schema utilizing the exported schema files generated when - * [Database.exportSchema] is enabled. - * - * This method is not supported for an in memory database [Builder]. - * - * @param databaseFilePath The file path within the 'assets/' directory of where the - * database file is located. - * @param callback The pre-packaged callback. - * - * @return This builder instance. - */ - @SuppressLint("BuilderSetStyle") // To keep naming consistency. - open fun createFromAsset( - databaseFilePath: String, - callback: PrepackagedDatabaseCallback - ) = apply { - this.prepackagedDatabaseCallback = callback - this.copyFromAssetPath = databaseFilePath - } - - /** - * Configures Room to create and open the database using a pre-packaged database file. - * - * Room does not open the pre-packaged database, instead it copies it into the internal - * app database folder and then opens it. The given file must be accessible and the right - * permissions must be granted for Room to copy the file. - * - * The pre-packaged database schema will be validated. It might be best to create your - * pre-packaged database schema utilizing the exported schema files generated when - * [Database.exportSchema] is enabled. - * - * The [Callback.onOpen] method can be used as an indicator - * that the pre-packaged database was successfully opened by Room and can be cleaned up. - * - * This method is not supported for an in memory database [Builder]. - * - * @param databaseFile The database file. - * - * @return This builder instance. - */ - open fun createFromFile(databaseFile: File) = apply { - this.copyFromFile = databaseFile - } - - /** - * Configures Room to create and open the database using a pre-packaged database file. - * - * Room does not open the pre-packaged database, instead it copies it into the internal - * app database folder and then opens it. The given file must be accessible and the right - * permissions must be granted for Room to copy the file. - * - * The pre-packaged database schema will be validated. It might be best to create your - * pre-packaged database schema utilizing the exported schema files generated when - * [Database.exportSchema] is enabled. - * - * The [Callback.onOpen] method can be used as an indicator - * that the pre-packaged database was successfully opened by Room and can be cleaned up. - * - * This method is not supported for an in memory database [Builder]. - * - * @param databaseFile The database file. - * @param callback The pre-packaged callback. - * - * @return This builder instance. - */ - @SuppressLint("BuilderSetStyle", "StreamFiles") // To keep naming consistency. - open fun createFromFile( - databaseFile: File, - callback: PrepackagedDatabaseCallback - ) = apply { - this.prepackagedDatabaseCallback = callback - this.copyFromFile = databaseFile - } - - /** - * Configures Room to create and open the database using a pre-packaged database via an - * [InputStream]. - * - * This is useful for processing compressed database files. Room does not open the - * pre-packaged database, instead it copies it into the internal app database folder, and - * then open it. The [InputStream] will be closed once Room is done consuming it. - * - * The pre-packaged database schema will be validated. It might be best to create your - * pre-packaged database schema utilizing the exported schema files generated when - * [Database.exportSchema] is enabled. - * - * The [Callback.onOpen] method can be used as an indicator - * that the pre-packaged database was successfully opened by Room and can be cleaned up. - * - * This method is not supported for an in memory database [Builder]. - * - * @param inputStreamCallable A callable that returns an InputStream from which to copy - * the database. The callable will be invoked in a thread from - * the Executor set via [setQueryExecutor]. The - * callable is only invoked if Room needs to create and open the - * database from the pre-package database, usually the first time - * it is created or during a destructive migration. - * - * @return This builder instance. - */ - @SuppressLint("BuilderSetStyle") // To keep naming consistency. - open fun createFromInputStream( - inputStreamCallable: Callable - ) = apply { - this.copyFromInputStream = inputStreamCallable - } - - /** - * Configures Room to create and open the database using a pre-packaged database via an - * [InputStream]. - * - * This is useful for processing compressed database files. Room does not open the - * pre-packaged database, instead it copies it into the internal app database folder, and - * then open it. The [InputStream] will be closed once Room is done consuming it. - * - * The pre-packaged database schema will be validated. It might be best to create your - * pre-packaged database schema utilizing the exported schema files generated when - * [Database.exportSchema] is enabled. - * - * The [Callback.onOpen] method can be used as an indicator - * that the pre-packaged database was successfully opened by Room and can be cleaned up. - * - * This method is not supported for an in memory database [Builder]. - * - * @param inputStreamCallable A callable that returns an InputStream from which to copy - * the database. The callable will be invoked in a thread from - * the Executor set via [setQueryExecutor]. The - * callable is only invoked if Room needs to create and open the - * database from the pre-package database, usually the first time - * it is created or during a destructive migration. - * @param callback The pre-packaged callback. - * - * @return This builder instance. - */ - @SuppressLint("BuilderSetStyle", "LambdaLast") // To keep naming consistency. - open fun createFromInputStream( - inputStreamCallable: Callable, - callback: PrepackagedDatabaseCallback - ) = apply { - this.prepackagedDatabaseCallback = callback - this.copyFromInputStream = inputStreamCallable - } - - /** - * Sets the database factory. If not set, it defaults to - * [FrameworkSQLiteOpenHelperFactory]. - * - * @param factory The factory to use to access the database. - * @return This builder instance. - */ - open fun openHelperFactory(factory: SupportSQLiteOpenHelper.Factory?) = apply { - this.factory = factory - } - - /** - * Adds a migration to the builder. - * - * Each Migration has a start and end versions and Room runs these migrations to bring the - * database to the latest version. - * - * If a migration item is missing between current version and the latest version, Room - * will clear the database and recreate so even if you have no changes between 2 versions, - * you should still provide a Migration object to the builder. - * - * A migration can handle more than 1 version (e.g. if you have a faster path to choose when - * going version 3 to 5 without going to version 4). If Room opens a database at version - * 3 and latest version is >= 5, Room will use the migration object that can migrate from - * 3 to 5 instead of 3 to 4 and 4 to 5. - * - * @param migrations The migration object that can modify the database and to the necessary - * changes. - * @return This builder instance. - */ - open fun addMigrations(vararg migrations: Migration) = apply { - if (migrationStartAndEndVersions == null) { - migrationStartAndEndVersions = HashSet() - } - for (migration in migrations) { - migrationStartAndEndVersions!!.add(migration.startVersion) - migrationStartAndEndVersions!!.add(migration.endVersion) - } - migrationContainer.addMigrations(*migrations) - } - - /** - * Adds an auto migration spec to the builder. - * - * @param autoMigrationSpec The auto migration object that is annotated with - * [AutoMigrationSpec] and is declared in an [AutoMigration] annotation. - * @return This builder instance. - */ - @Suppress("MissingGetterMatchingBuilder") - open fun addAutoMigrationSpec(autoMigrationSpec: AutoMigrationSpec) = apply { - this.autoMigrationSpecs.add(autoMigrationSpec) - } - - /** - * Disables the main thread query check for Room. - * - * Room ensures that Database is never accessed on the main thread because it may lock the - * main thread and trigger an ANR. If you need to access the database from the main thread, - * you should always use async alternatives or manually move the call to a background - * thread. - * - * You may want to turn this check off for testing. - * - * @return This builder instance. - */ - open fun allowMainThreadQueries() = apply { - this.allowMainThreadQueries = true - } - - /** - * Sets the journal mode for this database. - * - * This value is ignored if the builder is initialized with - * [Room.inMemoryDatabaseBuilder]. - * - * The journal mode should be consistent across multiple instances of - * [RoomDatabase] for a single SQLite database file. - * - * The default value is [JournalMode.AUTOMATIC]. - * - * @param journalMode The journal mode. - * @return This builder instance. - */ - open fun setJournalMode(journalMode: JournalMode) = apply { - this.journalMode = journalMode - } - - /** - * Sets the [Executor] that will be used to execute all non-blocking asynchronous - * queries and tasks, including `LiveData` invalidation, `Flowable` scheduling - * and `ListenableFuture` tasks. - * - * When both the query executor and transaction executor are unset, then a default - * `Executor` will be used. The default `Executor` allocates and shares threads - * amongst Architecture Components libraries. If the query executor is unset but a - * transaction executor was set [setTransactionExecutor], then the same `Executor` will be - * used for queries. - * - * For best performance the given `Executor` should be bounded (max number of threads - * is limited). - * - * The input `Executor` cannot run tasks on the UI thread. - * - * @return This builder instance. - */ - open fun setQueryExecutor(executor: Executor) = apply { - this.queryExecutor = executor - } - - /** - * Sets the [Executor] that will be used to execute all non-blocking asynchronous - * transaction queries and tasks, including `LiveData` invalidation, `Flowable` - * scheduling and `ListenableFuture` tasks. - * - * When both the transaction executor and query executor are unset, then a default - * `Executor` will be used. The default `Executor` allocates and shares threads - * amongst Architecture Components libraries. If the transaction executor is unset but a - * query executor was set using [setQueryExecutor], then the same `Executor` will be used - * for transactions. - * - * If the given `Executor` is shared then it should be unbounded to avoid the - * possibility of a deadlock. Room will not use more than one thread at a time from this - * executor since only one transaction at a time can be executed, other transactions will - * be queued on a first come, first serve order. - * - * The input `Executor` cannot run tasks on the UI thread. - * - * @return This builder instance. - */ - open fun setTransactionExecutor(executor: Executor) = apply { - this.transactionExecutor = executor - } - - /** - * Sets whether table invalidation in this instance of [RoomDatabase] should be - * broadcast and synchronized with other instances of the same [RoomDatabase], - * including those in a separate process. In order to enable multi-instance invalidation, - * this has to be turned on both ends. - * - * This is not enabled by default. - * - * This does not work for in-memory databases. This does not work between database instances - * targeting different database files. - * - * @return This builder instance. - */ - @Suppress("UnsafeOptInUsageError") - open fun enableMultiInstanceInvalidation() = apply { - this.multiInstanceInvalidationIntent = if (name != null) { - Intent(context, MultiInstanceInvalidationService::class.java) - } else { - null - } - } - - /** - * Sets whether table invalidation in this instance of [RoomDatabase] should be - * broadcast and synchronized with other instances of the same [RoomDatabase], - * including those in a separate process. In order to enable multi-instance invalidation, - * this has to be turned on both ends and need to point to the same - * [MultiInstanceInvalidationService]. - * - * This is not enabled by default. - * - * This does not work for in-memory databases. This does not work between database instances - * targeting different database files. - * - * @param invalidationServiceIntent Intent to bind to the - * [MultiInstanceInvalidationService]. - * @return This builder instance. - */ - @ExperimentalRoomApi - @Suppress("MissingGetterMatchingBuilder") - open fun setMultiInstanceInvalidationServiceIntent( - invalidationServiceIntent: Intent - ) = apply { - this.multiInstanceInvalidationIntent = - if (name != null) invalidationServiceIntent else null - } - - /** - * Allows Room to destructively recreate database tables if [Migration]s that would - * migrate old database schemas to the latest schema version are not found. - * - * When the database version on the device does not match the latest schema version, Room - * runs necessary [Migration]s on the database. - * - * If it cannot find the set of [Migration]s that will bring the database to the - * current version, it will throw an [IllegalStateException]. - * - * You can call this method to change this behavior to re-create the database instead of - * crashing. - * - * If the database was create from an asset or a file then Room will try to use the same - * file to re-create the database, otherwise this will delete all of the data in the - * database tables managed by Room. - * - * To let Room fallback to destructive migration only during a schema downgrade then use - * [fallbackToDestructiveMigrationOnDowngrade]. - * - * @return This builder instance. - */ - open fun fallbackToDestructiveMigration() = apply { - this.requireMigration = false - this.allowDestructiveMigrationOnDowngrade = true - } - - /** - * Allows Room to destructively recreate database tables if [Migration]s are not - * available when downgrading to old schema versions. - * - * For details, see [Builder.fallbackToDestructiveMigration]. - * - * @return This builder instance. - */ - open fun fallbackToDestructiveMigrationOnDowngrade() = apply { - this.requireMigration = true - this.allowDestructiveMigrationOnDowngrade = true - } - - /** - * Informs Room that it is allowed to destructively recreate database tables from specific - * starting schema versions. - * - * This functionality is the same as that provided by - * [fallbackToDestructiveMigration], except that this method allows the - * specification of a set of schema versions for which destructive recreation is allowed. - * - * Using this method is preferable to [fallbackToDestructiveMigration] if you want - * to allow destructive migrations from some schema versions while still taking advantage - * of exceptions being thrown due to unintentionally missing migrations. - * - * Note: No versions passed to this method may also exist as either starting or ending - * versions in the [Migration]s provided to [addMigrations]. If a - * version passed to this method is found as a starting or ending version in a Migration, an - * exception will be thrown. - * - * @param startVersions The set of schema versions from which Room should use a destructive - * migration. - * @return This builder instance. - */ - open fun fallbackToDestructiveMigrationFrom(vararg startVersions: Int) = apply { - for (startVersion in startVersions) { - this.migrationsNotRequiredFrom.add(startVersion) - } - } - - /** - * Adds a [Callback] to this database. - * - * @param callback The callback. - * @return This builder instance. - */ - open fun addCallback(callback: Callback) = apply { - this.callbacks.add(callback) - } - - /** - * Sets a [QueryCallback] to be invoked when queries are executed. - * - * The callback is invoked whenever a query is executed, note that adding this callback - * has a small cost and should be avoided in production builds unless needed. - * - * A use case for providing a callback is to allow logging executed queries. When the - * callback implementation logs then it is recommended to use an immediate executor. - * - * @param queryCallback The query callback. - * @param executor The executor on which the query callback will be invoked. - * @return This builder instance. - */ - @Suppress("MissingGetterMatchingBuilder") - open fun setQueryCallback( - queryCallback: QueryCallback, - executor: Executor - ) = apply { - this.queryCallback = queryCallback - this.queryCallbackExecutor = executor - } - - /** - * Adds a type converter instance to this database. - * - * @param typeConverter The converter. It must be an instance of a class annotated with - * [ProvidedTypeConverter] otherwise Room will throw an exception. - * @return This builder instance. - */ - open fun addTypeConverter(typeConverter: Any) = apply { - this.typeConverters.add(typeConverter) - } - - /** - * Enables auto-closing for the database to free up unused resources. The underlying - * database will be closed after it's last use after the specified [autoCloseTimeout] has - * elapsed since its last usage. The database will be automatically - * re-opened the next time it is accessed. - * - * Auto-closing is not compatible with in-memory databases since the data will be lost - * when the database is auto-closed. - * - * Also, temp tables and temp triggers will be cleared each time the database is - * auto-closed. If you need to use them, please include them in your - * callback [RoomDatabase.Callback.onOpen]. - * - * All configuration should happen in your [RoomDatabase.Callback.onOpen] - * callback so it is re-applied every time the database is re-opened. Note that the - * [RoomDatabase.Callback.onOpen] will be called every time the database is re-opened. - * - * The auto-closing database operation runs on the query executor. - * - * The database will not be re-opened if the RoomDatabase or the - * SupportSqliteOpenHelper is closed manually (by calling - * [RoomDatabase.close] or [SupportSQLiteOpenHelper.close]. If the - * database is closed manually, you must create a new database using - * [RoomDatabase.Builder.build]. - * - * @param autoCloseTimeout the amount of time after the last usage before closing the - * database. Must greater or equal to zero. - * @param autoCloseTimeUnit the timeunit for autoCloseTimeout. - * @return This builder instance. - */ - @ExperimentalRoomApi // When experimental is removed, add these parameters to - // DatabaseConfiguration - @Suppress("MissingGetterMatchingBuilder") - open fun setAutoCloseTimeout( - @IntRange(from = 0) autoCloseTimeout: Long, - autoCloseTimeUnit: TimeUnit - ) = apply { - require(autoCloseTimeout >= 0) { "autoCloseTimeout must be >= 0" } - this.autoCloseTimeout = autoCloseTimeout - this.autoCloseTimeUnit = autoCloseTimeUnit - } - - /** - * Creates the databases and initializes it. - * - * By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive - * triggers. - * - * @return A new database instance. - */ - open fun build(): T { - if (queryExecutor == null && transactionExecutor == null) { - transactionExecutor = ArchTaskExecutor.getIOThreadExecutor() - queryExecutor = transactionExecutor - } else if (queryExecutor != null && transactionExecutor == null) { - transactionExecutor = queryExecutor - } else if (queryExecutor == null) { - queryExecutor = transactionExecutor - } - if (migrationStartAndEndVersions != null) { - for (version in migrationStartAndEndVersions!!) { - require(!migrationsNotRequiredFrom.contains(version)) { - "Inconsistency detected. A Migration was supplied to " + - "addMigration(Migration... migrations) that has a start " + - "or end version equal to a start version supplied to " + - "fallbackToDestructiveMigrationFrom(int... " + - "startVersions). Start version: $version" - } - } - } - - val factory: SupportSQLiteOpenHelper.Factory = if (factory == null) { - FrameworkSQLiteOpenHelperFactory() - } else { - factory - }?.let { - if (autoCloseTimeout > 0) { - requireNotNull(name) { - "Cannot create auto-closing database for an in-memory database." - } - val autoCloser = AutoCloser( - autoCloseTimeout, - requireNotNull(autoCloseTimeUnit), - requireNotNull(queryExecutor) - ) - AutoClosingRoomOpenHelperFactory(it, autoCloser) - } else { - it - } - }?.let { - if ( - copyFromAssetPath != null || - copyFromFile != null || - copyFromInputStream != null - ) { - requireNotNull(name) { - "Cannot create from asset or file for an in-memory database." - } - - val copyFromAssetPathConfig = if (copyFromAssetPath == null) 0 else 1 - val copyFromFileConfig = if (copyFromFile == null) 0 else 1 - val copyFromInputStreamConfig = if (copyFromInputStream == null) 0 else 1 - val copyConfigurations = copyFromAssetPathConfig + copyFromFileConfig + - copyFromInputStreamConfig - - require(copyConfigurations == 1) { - "More than one of createFromAsset(), " + - "createFromInputStream(), and createFromFile() were called on this " + - "Builder, but the database can only be created using one of the " + - "three configurations." - } - SQLiteCopyOpenHelperFactory( - copyFromAssetPath, - copyFromFile, - copyFromInputStream, - it - ) - } else { - it - } - }.let { - requireNotNull(it) - if (queryCallback != null) { - QueryInterceptorOpenHelperFactory( - it, - requireNotNull(queryCallbackExecutor), - requireNotNull(queryCallback) - ) - } else { - it - } - } - val configuration = DatabaseConfiguration( - context, - name, - factory, - migrationContainer, - callbacks, - allowMainThreadQueries, - journalMode.resolve(context), - requireNotNull(queryExecutor), - requireNotNull(transactionExecutor), - multiInstanceInvalidationIntent, - requireMigration, - allowDestructiveMigrationOnDowngrade, - migrationsNotRequiredFrom, - copyFromAssetPath, - copyFromFile, - copyFromInputStream, - prepackagedDatabaseCallback, - typeConverters, - autoMigrationSpecs - ) - val db = Room.getGeneratedImplementation(klass, "_Impl") - db.init(configuration) - return db - } - } - - /** - * A container to hold migrations. It also allows querying its contents to find migrations - * between two versions. - */ - open class MigrationContainer { - private val migrations = mutableMapOf>() - - /** - * Adds the given migrations to the list of available migrations. If 2 migrations have the - * same start-end versions, the latter migration overrides the previous one. - * - * @param migrations List of available migrations. - */ - open fun addMigrations(vararg migrations: Migration) { - migrations.forEach(::addMigration) - } - - /** - * Adds the given migrations to the list of available migrations. If 2 migrations have the - * same start-end versions, the latter migration overrides the previous one. - * - * @param migrations List of available migrations. - */ - open fun addMigrations(migrations: List) { - migrations.forEach(::addMigration) - } - - private fun addMigration(migration: Migration) { - val start = migration.startVersion - val end = migration.endVersion - val targetMap = migrations.getOrPut(start) { TreeMap() } - - if (targetMap.contains(end)) { - Log.w(LOG_TAG, "Overriding migration ${targetMap[end]} with $migration") - } - targetMap[end] = migration - } - - /** - * Returns the map of available migrations where the key is the start version of the - * migration, and the value is a map of (end version -> Migration). - * - * @return Map of migrations keyed by the start version - */ - open fun getMigrations(): Map> { - return migrations - } - - /** - * Finds the list of migrations that should be run to move from `start` version to - * `end` version. - * - * @param start The current database version - * @param end The target database version - * @return An ordered list of [Migration] objects that should be run to migrate - * between the given versions. If a migration path cannot be found, returns `null`. - */ - open fun findMigrationPath(start: Int, end: Int): List? { - if (start == end) { - return emptyList() - } - val migrateUp = end > start - val result = mutableListOf() - return findUpMigrationPath(result, migrateUp, start, end) - } - - private fun findUpMigrationPath( - result: MutableList, - upgrade: Boolean, - start: Int, - end: Int - ): List? { - var migrationStart = start - while (if (upgrade) migrationStart < end else migrationStart > end) { - val targetNodes = migrations[migrationStart] ?: return null - // keys are ordered so we can start searching from one end of them. - val keySet = if (upgrade) { - targetNodes.descendingKeySet() - } else { - targetNodes.keys - } - var found = false - for (targetVersion in keySet) { - val shouldAddToPath = if (upgrade) { - targetVersion in (migrationStart + 1)..end - } else { - targetVersion in end until migrationStart - } - if (shouldAddToPath) { - // We are iterating over the key set of targetNodes, so we can assume it - // won't return a null value. - result.add(targetNodes[targetVersion]!!) - migrationStart = targetVersion - found = true - break - } - } - if (!found) { - return null - } - } - return result - } - - /** - * Indicates if the given migration is contained within the [MigrationContainer] based - * on its start-end versions. - * - * @param startVersion Start version of the migration. - * @param endVersion End version of the migration - * @return True if it contains a migration with the same start-end version, false otherwise. - */ - fun contains(startVersion: Int, endVersion: Int): Boolean { - val migrations = getMigrations() - if (migrations.containsKey(startVersion)) { - val startVersionMatches = migrations[startVersion] ?: emptyMap() - return startVersionMatches.containsKey(endVersion) - } - return false - } - } - - /** - * Callback for [RoomDatabase]. - */ - abstract class Callback { - /** - * Called when the database is created for the first time. This is called after all the - * tables are created. - * - * @param db The database. - */ - open fun onCreate(db: SupportSQLiteDatabase) {} - - /** - * Called when the database has been opened. - * - * @param db The database. - */ - open fun onOpen(db: SupportSQLiteDatabase) {} - - /** - * Called after the database was destructively migrated - * - * @param db The database. - */ - open fun onDestructiveMigration(db: SupportSQLiteDatabase) {} - } - - /** - * Callback for [Builder.createFromAsset], [Builder.createFromFile] - * and [Builder.createFromInputStream] - * - * This callback will be invoked after the pre-package DB is copied but before Room had - * a chance to open it and therefore before the [RoomDatabase.Callback] methods are - * invoked. This callback can be useful for updating the pre-package DB schema to satisfy - * Room's schema validation. - */ - abstract class PrepackagedDatabaseCallback { - /** - * Called when the pre-packaged database has been copied. - * - * @param db The database. - */ - open fun onOpenPrepackagedDatabase(db: SupportSQLiteDatabase) {} - } - - /** - * Callback interface for when SQLite queries are executed. - * - * Can be set using [RoomDatabase.Builder.setQueryCallback]. - */ - fun interface QueryCallback { - /** - * Called when a SQL query is executed. - * - * @param sqlQuery The SQLite query statement. - * @param bindArgs Arguments of the query if available, empty list otherwise. - */ - fun onQuery(sqlQuery: String, bindArgs: List) - } - - companion object { - /** - * Unfortunately, we cannot read this value so we are only setting it to the SQLite default. - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - const val MAX_BIND_PARAMETER_CNT = 999 - } -} diff --git a/app/src/main/java/androidx/room/RoomOpenHelper.java b/app/src/main/java/androidx/room/RoomOpenHelper.java new file mode 100644 index 0000000000..3dbe7ddeac --- /dev/null +++ b/app/src/main/java/androidx/room/RoomOpenHelper.java @@ -0,0 +1,277 @@ +/* + * 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 androidx.room; + +import android.database.Cursor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SimpleSQLiteQuery; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.util.List; + +/** + * An open helper that holds a reference to the configuration until the database is opened. + * + * @hide + */ +@SuppressWarnings("unused") +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback { + @Nullable + private DatabaseConfiguration mConfiguration; + @NonNull + private final Delegate mDelegate; + @NonNull + private final String mIdentityHash; + /** + * Room v1 had a bug where the hash was not consistent if fields are reordered. + * The new has fixes it but we still need to accept the legacy hash. + */ + @NonNull // b/64290754 + private final String mLegacyHash; + + public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate, + @NonNull String identityHash, @NonNull String legacyHash) { + super(delegate.version); + mConfiguration = configuration; + mDelegate = delegate; + mIdentityHash = identityHash; + mLegacyHash = legacyHash; + } + + public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate, + @NonNull String legacyHash) { + this(configuration, delegate, "", legacyHash); + } + + @Override + public void onConfigure(SupportSQLiteDatabase db) { + super.onConfigure(db); + } + + @Override + public void onCreate(SupportSQLiteDatabase db) { + boolean isEmptyDatabase = hasEmptySchema(db); + mDelegate.createAllTables(db); + if (!isEmptyDatabase) { + // A 0 version pre-populated database goes through the create path because the + // framework's SQLiteOpenHelper thinks the database was just created from scratch. If we + // find the database not to be empty, then it is a pre-populated, we must validate it to + // see if its suitable for usage. + ValidationResult result = mDelegate.onValidateSchema(db); + if (!result.isValid) { + throw new IllegalStateException("Pre-packaged database has an invalid schema: " + + result.expectedFoundMsg); + } + } + updateIdentity(db); + mDelegate.onCreate(db); + } + + @Override + public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { + boolean migrated = false; + if (mConfiguration != null) { + List migrations = mConfiguration.migrationContainer.findMigrationPath( + oldVersion, newVersion); + if (migrations != null) { + mDelegate.onPreMigrate(db); + for (Migration migration : migrations) { + migration.migrate(db); + } + ValidationResult result = mDelegate.onValidateSchema(db); + if (!result.isValid) { + throw new IllegalStateException("Migration didn't properly handle: " + + result.expectedFoundMsg); + } + mDelegate.onPostMigrate(db); + updateIdentity(db); + migrated = true; + } + } + if (!migrated) { + if (mConfiguration != null + && !mConfiguration.isMigrationRequired(oldVersion, newVersion)) { + mDelegate.dropAllTables(db); + mDelegate.createAllTables(db); + } else { + throw new IllegalStateException("A migration from " + oldVersion + " to " + + newVersion + " was required but not found. Please provide the " + + "necessary Migration path via " + + "RoomDatabase.Builder.addMigration(Migration ...) or allow for " + + "destructive migrations via one of the " + + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods."); + } + } + } + + @Override + public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { + onUpgrade(db, oldVersion, newVersion); + } + + @Override + public void onOpen(SupportSQLiteDatabase db) { + super.onOpen(db); + checkIdentity(db); + mDelegate.onOpen(db); + // there might be too many configurations etc, just clear it. + mConfiguration = null; + } + + private void checkIdentity(SupportSQLiteDatabase db) { + if (hasRoomMasterTable(db)) { + String identityHash = null; + Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY)); + //noinspection TryFinallyCanBeTryWithResources + try { + if (cursor.moveToFirst()) { + identityHash = cursor.getString(0); + } + } finally { + cursor.close(); + } + if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) { + throw new IllegalStateException("Room cannot verify the data integrity. Looks like" + + " you've changed schema but forgot to update the version number. You can" + + " simply fix this by increasing the version number."); + } + } else { + // No room_master_table, this might an a pre-populated DB, we must validate to see if + // its suitable for usage. + ValidationResult result = mDelegate.onValidateSchema(db); + if (!result.isValid) { + throw new IllegalStateException("Pre-packaged database has an invalid schema: " + + result.expectedFoundMsg); + } + mDelegate.onPostMigrate(db); + updateIdentity(db); + } + } + + private void updateIdentity(SupportSQLiteDatabase db) { + createMasterTableIfNotExists(db); + db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash)); + } + + private void createMasterTableIfNotExists(SupportSQLiteDatabase db) { + db.execSQL(RoomMasterTable.CREATE_QUERY); + } + + private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) { + Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='" + + RoomMasterTable.TABLE_NAME + "'"); + //noinspection TryFinallyCanBeTryWithResources + try { + return cursor.moveToFirst() && cursor.getInt(0) != 0; + } finally { + cursor.close(); + } + } + + private static boolean hasEmptySchema(SupportSQLiteDatabase db) { + Cursor cursor = db.query( + "SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'"); + //noinspection TryFinallyCanBeTryWithResources + try { + return cursor.moveToFirst() && cursor.getInt(0) == 0; + } finally { + cursor.close(); + } + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public abstract static class Delegate { + public final int version; + + public Delegate(int version) { + this.version = version; + } + + protected abstract void dropAllTables(SupportSQLiteDatabase database); + + protected abstract void createAllTables(SupportSQLiteDatabase database); + + protected abstract void onOpen(SupportSQLiteDatabase database); + + protected abstract void onCreate(SupportSQLiteDatabase database); + + /** + * Called after a migration run to validate database integrity. + * + * @param db The SQLite database. + * + * @deprecated Use {@link #onValidateSchema(SupportSQLiteDatabase)} + */ + @Deprecated + protected void validateMigration(SupportSQLiteDatabase db) { + throw new UnsupportedOperationException("validateMigration is deprecated"); + } + + /** + * Called after a migration run or pre-package database copy to validate database integrity. + * + * @param db The SQLite database. + */ + @SuppressWarnings("deprecation") + @NonNull + protected ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) { + validateMigration(db); + return new ValidationResult(true, null); + } + + /** + * Called before migrations execute to perform preliminary work. + * @param database The SQLite database. + */ + protected void onPreMigrate(SupportSQLiteDatabase database) { + + } + + /** + * Called after migrations execute to perform additional work. + * @param database The SQLite database. + */ + protected void onPostMigrate(SupportSQLiteDatabase database) { + + } + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public static class ValidationResult { + + public final boolean isValid; + @Nullable + public final String expectedFoundMsg; + + public ValidationResult(boolean isValid, @Nullable String expectedFoundMsg) { + this.isValid = isValid; + this.expectedFoundMsg = expectedFoundMsg; + } + } +} diff --git a/app/src/main/java/androidx/room/RoomOpenHelper.kt b/app/src/main/java/androidx/room/RoomOpenHelper.kt deleted file mode 100644 index 879f98d79c..0000000000 --- a/app/src/main/java/androidx/room/RoomOpenHelper.kt +++ /dev/null @@ -1,244 +0,0 @@ -/* - * 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 androidx.room - -import androidx.annotation.RestrictTo -import androidx.room.util.useCursor -import androidx.sqlite.db.SimpleSQLiteQuery -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper - -/** - * An open helper that holds a reference to the configuration until the database is opened. - * - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -open class RoomOpenHelper( - configuration: DatabaseConfiguration, - delegate: Delegate, - identityHash: String, - legacyHash: String -) : SupportSQLiteOpenHelper.Callback(delegate.version) { - private var configuration: DatabaseConfiguration? - private val delegate: Delegate - private val identityHash: String - - /** - * Room v1 had a bug where the hash was not consistent if fields are reordered. - * The new has fixes it but we still need to accept the legacy hash. - */ - // b/64290754 - private val legacyHash: String - - init { - this.configuration = configuration - this.delegate = delegate - this.identityHash = identityHash - this.legacyHash = legacyHash - } - - constructor( - configuration: DatabaseConfiguration, - delegate: Delegate, - legacyHash: String - ) : this(configuration, delegate, "", legacyHash) - - override fun onConfigure(db: SupportSQLiteDatabase) { - super.onConfigure(db) - } - - override fun onCreate(db: SupportSQLiteDatabase) { - val isEmptyDatabase = hasEmptySchema(db) - delegate.createAllTables(db) - if (!isEmptyDatabase) { - // A 0 version pre-populated database goes through the create path because the - // framework's SQLiteOpenHelper thinks the database was just created from scratch. If we - // find the database not to be empty, then it is a pre-populated, we must validate it to - // see if its suitable for usage. - val result = delegate.onValidateSchema(db) - if (!result.isValid) { - throw IllegalStateException( - "Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}" - ) - } - } - updateIdentity(db) - delegate.onCreate(db) - } - - override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { - var migrated = false - configuration?.let { config -> - val migrations = config.migrationContainer.findMigrationPath( - oldVersion, newVersion - ) - if (migrations != null) { - delegate.onPreMigrate(db) - migrations.forEach { it.migrate(db) } - val result = delegate.onValidateSchema(db) - if (!result.isValid) { - throw IllegalStateException( - ("Migration didn't properly handle: " + - result.expectedFoundMsg) - ) - } - delegate.onPostMigrate(db) - updateIdentity(db) - migrated = true - } - } - if (!migrated) { - val config = this.configuration - if (config != null && !config.isMigrationRequired(oldVersion, newVersion)) { - delegate.dropAllTables(db) - delegate.createAllTables(db) - } else { - throw IllegalStateException( - "A migration from $oldVersion to $newVersion was required but not found. " + - "Please provide the " + - "necessary Migration path via " + - "RoomDatabase.Builder.addMigration(Migration ...) or allow for " + - "destructive migrations via one of the " + - "RoomDatabase.Builder.fallbackToDestructiveMigration* methods." - ) - } - } - } - - override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { - onUpgrade(db, oldVersion, newVersion) - } - - override fun onOpen(db: SupportSQLiteDatabase) { - super.onOpen(db) - checkIdentity(db) - delegate.onOpen(db) - // there might be too many configurations etc, just clear it. - configuration = null - } - - private fun checkIdentity(db: SupportSQLiteDatabase) { - if (hasRoomMasterTable(db)) { - val identityHash: String? = db.query( - SimpleSQLiteQuery(RoomMasterTable.READ_QUERY) - ).useCursor { cursor -> - if (cursor.moveToFirst()) { - cursor.getString(0) - } else { - null - } - } - - if (this.identityHash != identityHash && this.legacyHash != identityHash) { - throw IllegalStateException( - "Room cannot verify the data integrity. Looks like" + - " you've changed schema but forgot to update the version number. You can" + - " simply fix this by increasing the version number. Expected identity" + - " hash: ${ this.identityHash }, found: $identityHash" - ) - } - } else { - // No room_master_table, this might an a pre-populated DB, we must validate to see if - // its suitable for usage. - val result = delegate.onValidateSchema(db) - if (!result.isValid) { - throw IllegalStateException( - "Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}" - ) - } - delegate.onPostMigrate(db) - updateIdentity(db) - } - } - - private fun updateIdentity(db: SupportSQLiteDatabase) { - createMasterTableIfNotExists(db) - db.execSQL(RoomMasterTable.createInsertQuery(identityHash)) - } - - private fun createMasterTableIfNotExists(db: SupportSQLiteDatabase) { - db.execSQL(RoomMasterTable.CREATE_QUERY) - } - - /** - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - abstract class Delegate(@JvmField val version: Int) { - abstract fun dropAllTables(db: SupportSQLiteDatabase) - abstract fun createAllTables(db: SupportSQLiteDatabase) - abstract fun onOpen(db: SupportSQLiteDatabase) - abstract fun onCreate(db: SupportSQLiteDatabase) - - /** - * Called after a migration run to validate database integrity. - * - * @param db The SQLite database. - */ - @Deprecated("Use [onValidateSchema(SupportSQLiteDatabase)]") - protected open fun validateMigration(db: SupportSQLiteDatabase) { - throw UnsupportedOperationException("validateMigration is deprecated") - } - - /** - * Called after a migration run or pre-package database copy to validate database integrity. - * - * @param db The SQLite database. - */ - @Suppress("DEPRECATION") - open fun onValidateSchema(db: SupportSQLiteDatabase): ValidationResult { - validateMigration(db) - return ValidationResult(true, null) - } - - /** - * Called before migrations execute to perform preliminary work. - * @param database The SQLite database. - */ - open fun onPreMigrate(db: SupportSQLiteDatabase) {} - - /** - * Called after migrations execute to perform additional work. - * @param database The SQLite database. - */ - open fun onPostMigrate(db: SupportSQLiteDatabase) {} - } - - /** - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - open class ValidationResult( - @JvmField val isValid: Boolean, - @JvmField val expectedFoundMsg: String? - ) - companion object { - internal fun hasRoomMasterTable(db: SupportSQLiteDatabase): Boolean { - db.query( - "SELECT 1 FROM sqlite_master WHERE type = 'table' AND " + - "name='${ RoomMasterTable.TABLE_NAME }'" - ).useCursor { cursor -> - return cursor.moveToFirst() && cursor.getInt(0) != 0 - } - } - - internal fun hasEmptySchema(db: SupportSQLiteDatabase): Boolean { - db.query( - "SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'" - ).useCursor { cursor -> - return cursor.moveToFirst() && cursor.getInt(0) == 0 - } - } - } -} diff --git a/app/src/main/java/androidx/room/RoomSQLiteQuery.java b/app/src/main/java/androidx/room/RoomSQLiteQuery.java new file mode 100644 index 0000000000..689352c032 --- /dev/null +++ b/app/src/main/java/androidx/room/RoomSQLiteQuery.java @@ -0,0 +1,299 @@ +/* + * 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 androidx.room; + +import androidx.annotation.IntDef; +import androidx.annotation.RestrictTo; +import androidx.annotation.VisibleForTesting; +import androidx.sqlite.db.SupportSQLiteProgram; +import androidx.sqlite.db.SupportSQLiteQuery; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + +/** + * This class is used as an intermediate place to keep binding arguments so that we can run + * Cursor queries with correct types rather than passing everything as a string. + *

    + * Because it is relatively a big object, they are pooled and must be released after each use. + * + * @hide + */ +@SuppressWarnings("unused") +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram { + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + // Maximum number of queries we'll keep cached. + static final int POOL_LIMIT = 15; + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + // Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always + // clear the bigger queries (# of arguments). + static final int DESIRED_POOL_SIZE = 10; + private volatile String mQuery; + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + final long[] mLongBindings; + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + final double[] mDoubleBindings; + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + final String[] mStringBindings; + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + final byte[][] mBlobBindings; + + @Binding + private final int[] mBindingTypes; + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + final int mCapacity; + // number of arguments in the query + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + int mArgCount; + + + @SuppressWarnings("WeakerAccess") + @VisibleForTesting + static final TreeMap sQueryPool = new TreeMap<>(); + + /** + * Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery. + * + * @param supportSQLiteQuery The query to copy from + * @return A new query copied from the provided one. + */ + public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) { + final RoomSQLiteQuery query = RoomSQLiteQuery.acquire( + supportSQLiteQuery.getSql(), + supportSQLiteQuery.getArgCount()); + supportSQLiteQuery.bindTo(new SupportSQLiteProgram() { + @Override + public void bindNull(int index) { + query.bindNull(index); + } + + @Override + public void bindLong(int index, long value) { + query.bindLong(index, value); + } + + @Override + public void bindDouble(int index, double value) { + query.bindDouble(index, value); + } + + @Override + public void bindString(int index, String value) { + query.bindString(index, value); + } + + @Override + public void bindBlob(int index, byte[] value) { + query.bindBlob(index, value); + } + + @Override + public void clearBindings() { + query.clearBindings(); + } + + @Override + public void close() { + // ignored. + } + }); + return query; + } + + /** + * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the + * given query. + * + * @param query The query to prepare + * @param argumentCount The number of query arguments + * @return A RoomSQLiteQuery that holds the given query and has space for the given number of + * arguments. + */ + @SuppressWarnings("WeakerAccess") + public static RoomSQLiteQuery acquire(String query, int argumentCount) { + synchronized (sQueryPool) { + final Map.Entry entry = + sQueryPool.ceilingEntry(argumentCount); + if (entry != null) { + sQueryPool.remove(entry.getKey()); + final RoomSQLiteQuery sqliteQuery = entry.getValue(); + sqliteQuery.init(query, argumentCount); + return sqliteQuery; + } + } + RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount); + sqLiteQuery.init(query, argumentCount); + return sqLiteQuery; + } + + private RoomSQLiteQuery(int capacity) { + mCapacity = capacity; + // because, 1 based indices... we don't want to offsets everything with 1 all the time. + int limit = capacity + 1; + //noinspection WrongConstant + mBindingTypes = new int[limit]; + mLongBindings = new long[limit]; + mDoubleBindings = new double[limit]; + mStringBindings = new String[limit]; + mBlobBindings = new byte[limit][]; + } + + @SuppressWarnings("WeakerAccess") + void init(String query, int argCount) { + mQuery = query; + mArgCount = argCount; + } + + /** + * Releases the query back to the pool. + *

    + * After released, the statement might be returned when {@link #acquire(String, int)} is called + * so you should never re-use it after releasing. + */ + @SuppressWarnings("WeakerAccess") + public void release() { + synchronized (sQueryPool) { + sQueryPool.put(mCapacity, this); + prunePoolLocked(); + } + } + + private static void prunePoolLocked() { + if (sQueryPool.size() > POOL_LIMIT) { + int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE; + final Iterator iterator = sQueryPool.descendingKeySet().iterator(); + while (toBeRemoved-- > 0) { + iterator.next(); + iterator.remove(); + } + } + } + + @Override + public String getSql() { + return mQuery; + } + + @Override + public int getArgCount() { + return mArgCount; + } + + @Override + public void bindTo(SupportSQLiteProgram program) { + for (int index = 1; index <= mArgCount; index++) { + switch (mBindingTypes[index]) { + case NULL: + program.bindNull(index); + break; + case LONG: + program.bindLong(index, mLongBindings[index]); + break; + case DOUBLE: + program.bindDouble(index, mDoubleBindings[index]); + break; + case STRING: + program.bindString(index, mStringBindings[index]); + break; + case BLOB: + program.bindBlob(index, mBlobBindings[index]); + break; + } + } + } + + @Override + public void bindNull(int index) { + mBindingTypes[index] = NULL; + } + + @Override + public void bindLong(int index, long value) { + mBindingTypes[index] = LONG; + mLongBindings[index] = value; + } + + @Override + public void bindDouble(int index, double value) { + mBindingTypes[index] = DOUBLE; + mDoubleBindings[index] = value; + } + + @Override + public void bindString(int index, String value) { + mBindingTypes[index] = STRING; + mStringBindings[index] = value; + } + + @Override + public void bindBlob(int index, byte[] value) { + mBindingTypes[index] = BLOB; + mBlobBindings[index] = value; + } + + @Override + public void close() { + // no-op. not calling release because it is internal API. + } + + /** + * Copies arguments from another RoomSQLiteQuery into this query. + * + * @param other The other query, which holds the arguments to be copied. + */ + public void copyArgumentsFrom(RoomSQLiteQuery other) { + int argCount = other.getArgCount() + 1; // +1 for the binding offsets + System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount); + System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount); + System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount); + System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount); + System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount); + } + + @Override + public void clearBindings() { + Arrays.fill(mBindingTypes, NULL); + Arrays.fill(mStringBindings, null); + Arrays.fill(mBlobBindings, null); + mQuery = null; + // no need to clear others + } + + private static final int NULL = 1; + private static final int LONG = 2; + private static final int DOUBLE = 3; + private static final int STRING = 4; + private static final int BLOB = 5; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({NULL, LONG, DOUBLE, STRING, BLOB}) + @interface Binding { + } +} diff --git a/app/src/main/java/androidx/room/RoomSQLiteQuery.kt b/app/src/main/java/androidx/room/RoomSQLiteQuery.kt deleted file mode 100644 index 9a2e832b0f..0000000000 --- a/app/src/main/java/androidx/room/RoomSQLiteQuery.kt +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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 androidx.room - -import androidx.annotation.IntDef -import androidx.annotation.RestrictTo -import androidx.annotation.VisibleForTesting -import androidx.sqlite.db.SupportSQLiteProgram -import androidx.sqlite.db.SupportSQLiteQuery -import java.util.Arrays -import java.util.TreeMap - -/** - * This class is used as an intermediate place to keep binding arguments so that we can run - * Cursor queries with correct types rather than passing everything as a string. - * - * Because it is relatively a big object, they are pooled and must be released after each use. - * - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -class RoomSQLiteQuery private constructor( - @field:VisibleForTesting val capacity: Int -) : SupportSQLiteQuery, SupportSQLiteProgram { - @Volatile - private var query: String? = null - - @JvmField - @VisibleForTesting - val longBindings: LongArray - - @JvmField - @VisibleForTesting - val doubleBindings: DoubleArray - - @JvmField - @VisibleForTesting - val stringBindings: Array - - @JvmField - @VisibleForTesting - val blobBindings: Array - - @Binding - private val bindingTypes: IntArray - - // number of arguments in the query - override var argCount = 0 - private set - - fun init(query: String, initArgCount: Int) { - this.query = query - argCount = initArgCount - } - - init { - // because, 1 based indices... we don't want to offsets everything with 1 all the time. - val limit = capacity + 1 - bindingTypes = IntArray(limit) - longBindings = LongArray(limit) - doubleBindings = DoubleArray(limit) - stringBindings = arrayOfNulls(limit) - blobBindings = arrayOfNulls(limit) - } - - /** - * Releases the query back to the pool. - * - * After released, the statement might be returned when [.acquire] is called - * so you should never re-use it after releasing. - */ - fun release() { - synchronized(queryPool) { - queryPool[capacity] = this - prunePoolLocked() - } - } - - override val sql: String - get() = checkNotNull(this.query) - - override fun bindTo(statement: SupportSQLiteProgram) { - for (index in 1..argCount) { - when (bindingTypes[index]) { - NULL -> statement.bindNull(index) - LONG -> statement.bindLong(index, longBindings[index]) - DOUBLE -> statement.bindDouble(index, doubleBindings[index]) - STRING -> statement.bindString(index, requireNotNull(stringBindings[index])) - BLOB -> statement.bindBlob(index, requireNotNull(blobBindings[index])) - } - } - } - - override fun bindNull(index: Int) { - bindingTypes[index] = NULL - } - - override fun bindLong(index: Int, value: Long) { - bindingTypes[index] = LONG - longBindings[index] = value - } - - override fun bindDouble(index: Int, value: Double) { - bindingTypes[index] = DOUBLE - doubleBindings[index] = value - } - - override fun bindString(index: Int, value: String) { - bindingTypes[index] = STRING - stringBindings[index] = value - } - - override fun bindBlob(index: Int, value: ByteArray) { - bindingTypes[index] = BLOB - blobBindings[index] = value - } - - override fun close() { - // no-op. not calling release because it is internal API. - } - - /** - * Copies arguments from another RoomSQLiteQuery into this query. - * - * @param other The other query, which holds the arguments to be copied. - */ - fun copyArgumentsFrom(other: RoomSQLiteQuery) { - val argCount = other.argCount + 1 // +1 for the binding offsets - System.arraycopy(other.bindingTypes, 0, bindingTypes, 0, argCount) - System.arraycopy(other.longBindings, 0, longBindings, 0, argCount) - System.arraycopy(other.stringBindings, 0, stringBindings, 0, argCount) - System.arraycopy(other.blobBindings, 0, blobBindings, 0, argCount) - System.arraycopy(other.doubleBindings, 0, doubleBindings, 0, argCount) - } - - override fun clearBindings() { - Arrays.fill(bindingTypes, NULL) - Arrays.fill(stringBindings, null) - Arrays.fill(blobBindings, null) - query = null - // no need to clear others - } - - @Retention(AnnotationRetention.SOURCE) - @IntDef(NULL, LONG, DOUBLE, STRING, BLOB) - internal annotation class Binding - - companion object { - // Maximum number of queries we'll keep cached. - @VisibleForTesting - const val POOL_LIMIT = 15 - - // Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always - // clear the bigger queries (# of arguments). - @VisibleForTesting - const val DESIRED_POOL_SIZE = 10 - - @JvmField - @VisibleForTesting - val queryPool = TreeMap() - - /** - * Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery. - * - * @param supportSQLiteQuery The query to copy from - * @return A new query copied from the provided one. - */ - @JvmStatic - fun copyFrom(supportSQLiteQuery: SupportSQLiteQuery): RoomSQLiteQuery { - val query = acquire( - supportSQLiteQuery.sql, - supportSQLiteQuery.argCount - ) - - supportSQLiteQuery.bindTo(object : SupportSQLiteProgram by query {}) - return query - } - - /** - * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the - * given query. - * - * @param query The query to prepare - * @param argumentCount The number of query arguments - * @return A RoomSQLiteQuery that holds the given query and has space for the given number - * of arguments. - */ - @JvmStatic - fun acquire(query: String, argumentCount: Int): RoomSQLiteQuery { - synchronized(queryPool) { - val entry = queryPool.ceilingEntry(argumentCount) - if (entry != null) { - queryPool.remove(entry.key) - val sqliteQuery = entry.value - sqliteQuery.init(query, argumentCount) - return sqliteQuery - } - } - val sqLiteQuery = RoomSQLiteQuery(argumentCount) - sqLiteQuery.init(query, argumentCount) - return sqLiteQuery - } - - internal fun prunePoolLocked() { - if (queryPool.size > POOL_LIMIT) { - var toBeRemoved = queryPool.size - DESIRED_POOL_SIZE - val iterator = queryPool.descendingKeySet().iterator() - while (toBeRemoved-- > 0) { - iterator.next() - iterator.remove() - } - } - } - - private const val NULL = 1 - private const val LONG = 2 - private const val DOUBLE = 3 - private const val STRING = 4 - private const val BLOB = 5 - } -} diff --git a/app/src/main/java/androidx/room/RoomTrackingLiveData.java b/app/src/main/java/androidx/room/RoomTrackingLiveData.java new file mode 100644 index 0000000000..1e3daa74ea --- /dev/null +++ b/app/src/main/java/androidx/room/RoomTrackingLiveData.java @@ -0,0 +1,173 @@ +/* + * Copyright 2018 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 androidx.room; + + +import android.annotation.SuppressLint; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; +import androidx.arch.core.executor.ArchTaskExecutor; +import androidx.lifecycle.LiveData; + +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A LiveData implementation that closely works with {@link InvalidationTracker} to implement + * database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long + * as they are active. + *

    + * We need this extra handling for {@link androidx.lifecycle.LiveData} because when they are + * observed forever, there is no {@link androidx.lifecycle.Lifecycle} that will keep them in + * memory but they should stay. We cannot add-remove observer in {@link LiveData#onActive()}, + * {@link LiveData#onInactive()} because that would mean missing changes in between or doing an + * extra query on every UI rotation. + *

    + * This {@link LiveData} keeps a weak observer to the {@link InvalidationTracker} but it is hold + * strongly by the {@link InvalidationTracker} as long as it is active. + */ +class RoomTrackingLiveData extends LiveData { + @SuppressWarnings("WeakerAccess") + final RoomDatabase mDatabase; + + @SuppressWarnings("WeakerAccess") + final boolean mInTransaction; + + @SuppressWarnings("WeakerAccess") + final Callable mComputeFunction; + + private final InvalidationLiveDataContainer mContainer; + + @SuppressWarnings("WeakerAccess") + final InvalidationTracker.Observer mObserver; + + private int queued = 0; + private final Object lock = new Object(); + + @SuppressWarnings("WeakerAccess") + final AtomicBoolean mRegisteredObserver = new AtomicBoolean(false); + + @SuppressWarnings("WeakerAccess") + final Runnable mRefreshRunnable = new Runnable() { + @WorkerThread + @Override + public void run() { + synchronized (lock) { + queued--; + if (queued < 0) { + eu.faircode.email.Log.e(mComputeFunction + " queued=" + queued); + queued = 0; + } + } + + if (mRegisteredObserver.compareAndSet(false, true)) { + mDatabase.getInvalidationTracker().addWeakObserver(mObserver); + } + + T value = null; + boolean computed = false; + synchronized (mComputeFunction) { + int retry = 0; + while (!computed) { + try { + value = mComputeFunction.call(); + computed = true; + } catch (Throwable e) { + if (++retry > 5) { + eu.faircode.email.Log.e(e); + break; + } + eu.faircode.email.Log.w(e); + try { + Thread.sleep(2000L); + } catch (InterruptedException ignored) { + } + } + } + } + if (computed) { + postValue(value); + } + } + }; + + @SuppressWarnings("WeakerAccess") + final Runnable mInvalidationRunnable = new Runnable() { + @MainThread + @Override + public void run() { + boolean isActive = hasActiveObservers(); + if (isActive) + synchronized (lock) { + if (queued > 0) + eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug, + mComputeFunction + " queued=" + queued); + else { + queued++; + getQueryExecutor().execute(mRefreshRunnable); + } + } + } + }; + + @SuppressLint("RestrictedApi") + RoomTrackingLiveData( + RoomDatabase database, + InvalidationLiveDataContainer container, + boolean inTransaction, + Callable computeFunction, + String[] tableNames) { + mDatabase = database; + mInTransaction = inTransaction; + mComputeFunction = computeFunction; + mContainer = container; + mObserver = new InvalidationTracker.Observer(tableNames) { + @Override + public void onInvalidated(@NonNull Set tables) { + ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable); + } + }; + } + + @Override + protected void onActive() { + super.onActive(); + mContainer.onActive(this); + synchronized (lock) { + queued++; + getQueryExecutor().execute(mRefreshRunnable); + } + } + + @Override + protected void onInactive() { + super.onInactive(); + mContainer.onInactive(this); + } + + Executor getQueryExecutor() { + if (mInTransaction) { + return mDatabase.getTransactionExecutor(); + } else { + return mDatabase.getQueryExecutor(); + } + } +} diff --git a/app/src/main/java/androidx/room/RoomTrackingLiveData.kt b/app/src/main/java/androidx/room/RoomTrackingLiveData.kt deleted file mode 100644 index 171b57d16e..0000000000 --- a/app/src/main/java/androidx/room/RoomTrackingLiveData.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2018 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 androidx.room - -import android.annotation.SuppressLint -import androidx.arch.core.executor.ArchTaskExecutor -import androidx.lifecycle.LiveData -import java.lang.Exception -import java.lang.RuntimeException -import java.util.concurrent.Callable -import java.util.concurrent.Executor -import java.util.concurrent.atomic.AtomicBoolean - -/** - * A LiveData implementation that closely works with [InvalidationTracker] to implement - * database drive [androidx.lifecycle.LiveData] queries that are strongly hold as long - * as they are active. - * - * We need this extra handling for [androidx.lifecycle.LiveData] because when they are - * observed forever, there is no [androidx.lifecycle.Lifecycle] that will keep them in - * memory but they should stay. We cannot add-remove observer in [LiveData.onActive], - * [LiveData.onInactive] because that would mean missing changes in between or doing an - * extra query on every UI rotation. - * - * This [LiveData] keeps a weak observer to the [InvalidationTracker] but it is hold - * strongly by the [InvalidationTracker] as long as it is active. - */ -@SuppressLint("RestrictedApi") -internal class RoomTrackingLiveData ( - val database: RoomDatabase, - private val container: InvalidationLiveDataContainer, - val inTransaction: Boolean, - val computeFunction: Callable, - tableNames: Array -) : LiveData() { - val observer: InvalidationTracker.Observer = object : InvalidationTracker.Observer(tableNames) { - override fun onInvalidated(tables: Set) { - ArchTaskExecutor.getInstance().executeOnMainThread(invalidationRunnable) - } - } - val invalid = AtomicBoolean(true) - val computing = AtomicBoolean(false) - val registeredObserver = AtomicBoolean(false) - val refreshRunnable = Runnable { - if (registeredObserver.compareAndSet(false, true)) { - database.invalidationTracker.addWeakObserver(observer) - } - var computed: Boolean - do { - computed = false - // compute can happen only in 1 thread but no reason to lock others. - if (computing.compareAndSet(false, true)) { - // as long as it is invalid, keep computing. - try { - var value: T? = null - while (invalid.compareAndSet(true, false)) { - computed = true - try { - value = computeFunction.call() - } catch (e: Exception) { - throw RuntimeException( - "Exception while computing database live data.", - e - ) - } - } - if (computed) { - postValue(value) - } - } finally { - // release compute lock - computing.set(false) - } - } - // check invalid after releasing compute lock to avoid the following scenario. - // Thread A runs compute() - // Thread A checks invalid, it is false - // Main thread sets invalid to true - // Thread B runs, fails to acquire compute lock and skips - // Thread A releases compute lock - // We've left invalid in set state. The check below recovers. - } while (computed && invalid.get()) - } - - val invalidationRunnable = Runnable { - val isActive = hasActiveObservers() - if (invalid.compareAndSet(false, true)) { - if (isActive) { - queryExecutor.execute(refreshRunnable) - } - } - } - - @Suppress("UNCHECKED_CAST") - override fun onActive() { - super.onActive() - container.onActive(this as LiveData) - queryExecutor.execute(refreshRunnable) - } - - @Suppress("UNCHECKED_CAST") - override fun onInactive() { - super.onInactive() - container.onInactive(this as LiveData) - } - - val queryExecutor: Executor - get() = if (inTransaction) { - database.transactionExecutor - } else { - database.queryExecutor - } -} diff --git a/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java new file mode 100644 index 0000000000..300145cde3 --- /dev/null +++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java @@ -0,0 +1,289 @@ +/* + * Copyright 2019 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 androidx.room; + +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.room.util.CopyLock; +import androidx.room.util.DBUtil; +import androidx.room.util.FileUtil; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.util.concurrent.Callable; + +/** + * An open helper that will copy & open a pre-populated database if it doesn't exists in internal + * storage. + */ +class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper { + + @NonNull + private final Context mContext; + @Nullable + private final String mCopyFromAssetPath; + @Nullable + private final File mCopyFromFile; + @Nullable + private final Callable mCopyFromInputStream; + private final int mDatabaseVersion; + @NonNull + private final SupportSQLiteOpenHelper mDelegate; + @Nullable + private DatabaseConfiguration mDatabaseConfiguration; + + private boolean mVerified; + + SQLiteCopyOpenHelper( + @NonNull Context context, + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream, + int databaseVersion, + @NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) { + mContext = context; + mCopyFromAssetPath = copyFromAssetPath; + mCopyFromFile = copyFromFile; + mCopyFromInputStream = copyFromInputStream; + mDatabaseVersion = databaseVersion; + mDelegate = supportSQLiteOpenHelper; + } + + @Override + public String getDatabaseName() { + return mDelegate.getDatabaseName(); + } + + @Override + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + public void setWriteAheadLoggingEnabled(boolean enabled) { + mDelegate.setWriteAheadLoggingEnabled(enabled); + } + + @Override + public synchronized SupportSQLiteDatabase getWritableDatabase() { + if (!mVerified) { + verifyDatabaseFile(true); + mVerified = true; + } + return mDelegate.getWritableDatabase(); + } + + @Override + public synchronized SupportSQLiteDatabase getReadableDatabase() { + if (!mVerified) { + verifyDatabaseFile(false); + mVerified = true; + } + return mDelegate.getReadableDatabase(); + } + + @Override + public synchronized void close() { + mDelegate.close(); + mVerified = false; + } + + @Override + @NonNull + public SupportSQLiteOpenHelper getDelegate() { + return mDelegate; + } + + // Can't be constructor param because the factory is needed by the database builder which in + // turn is the one that actually builds the configuration. + void setDatabaseConfiguration(@Nullable DatabaseConfiguration databaseConfiguration) { + mDatabaseConfiguration = databaseConfiguration; + } + + private void verifyDatabaseFile(boolean writable) { + String databaseName = getDatabaseName(); + File databaseFile = mContext.getDatabasePath(databaseName); + boolean processLevelLock = mDatabaseConfiguration == null + || mDatabaseConfiguration.multiInstanceInvalidation; + CopyLock copyLock = new CopyLock(databaseName, mContext.getFilesDir(), processLevelLock); + try { + // Acquire a copy lock, this lock works across threads and processes, preventing + // concurrent copy attempts from occurring. + copyLock.lock(); + + if (!databaseFile.exists()) { + try { + // No database file found, copy and be done. + copyDatabaseFile(databaseFile, writable); + return; + } catch (IOException e) { + throw new RuntimeException("Unable to copy database file.", e); + } + } + + if (mDatabaseConfiguration == null) { + return; + } + + // A database file is present, check if we need to re-copy it. + int currentVersion; + try { + currentVersion = DBUtil.readVersion(databaseFile); + } catch (IOException e) { + Log.w(Room.LOG_TAG, "Unable to read database version.", e); + return; + } + + if (currentVersion == mDatabaseVersion) { + return; + } + + if (mDatabaseConfiguration.isMigrationRequired(currentVersion, mDatabaseVersion)) { + // From the current version to the desired version a migration is required, i.e. + // we won't be performing a copy destructive migration. + return; + } + + if (mContext.deleteDatabase(databaseName)) { + try { + copyDatabaseFile(databaseFile, writable); + } catch (IOException e) { + // We are more forgiving copying a database on a destructive migration since + // there is already a database file that can be opened. + Log.w(Room.LOG_TAG, "Unable to copy database file.", e); + } + } else { + Log.w(Room.LOG_TAG, "Failed to delete database file (" + + databaseName + ") for a copy destructive migration."); + } + } finally { + copyLock.unlock(); + } + } + + private void copyDatabaseFile(File destinationFile, boolean writable) throws IOException { + ReadableByteChannel input; + if (mCopyFromAssetPath != null) { + input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath)); + } else if (mCopyFromFile != null) { + input = new FileInputStream(mCopyFromFile).getChannel(); + } else if (mCopyFromInputStream != null) { + final InputStream inputStream; + try { + inputStream = mCopyFromInputStream.call(); + } catch (Exception e) { + throw new IOException("inputStreamCallable exception on call", e); + } + input = Channels.newChannel(inputStream); + } else { + throw new IllegalStateException("copyFromAssetPath, copyFromFile and " + + "copyFromInputStream are all null!"); + } + + // An intermediate file is used so that we never end up with a half-copied database file + // in the internal directory. + //File intermediateFile = File.createTempFile( + // "room-copy-helper", ".tmp", mContext.getCacheDir()); + File dir = new File(mContext.getFilesDir(), "room"); + if (!dir.exists() && !dir.mkdirs()) + throw new IOException("Failed to create=" + dir); + File intermediateFile = new File(dir, "room-copy-helper.tmp"); + intermediateFile.deleteOnExit(); + FileChannel output = new FileOutputStream(intermediateFile).getChannel(); + FileUtil.copy(input, output); + + File parent = destinationFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + throw new IOException("Failed to create directories for " + + destinationFile.getAbsolutePath()); + } + + // Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch + // the open pre-packaged callback. If it fails then intermediate file won't be copied making + // invoking pre-packaged callback a transactional operation. + dispatchOnOpenPrepackagedDatabase(intermediateFile, writable); + + if (!intermediateFile.renameTo(destinationFile)) { + throw new IOException("Failed to move intermediate file (" + + intermediateFile.getAbsolutePath() + ") to destination (" + + destinationFile.getAbsolutePath() + ")."); + } + } + + private void dispatchOnOpenPrepackagedDatabase(File databaseFile, boolean writable) { + if (mDatabaseConfiguration == null + || mDatabaseConfiguration.prepackagedDatabaseCallback == null) { + return; + } + + SupportSQLiteOpenHelper helper = createFrameworkOpenHelper(databaseFile); + try { + SupportSQLiteDatabase db = writable ? helper.getWritableDatabase() : + helper.getReadableDatabase(); + mDatabaseConfiguration.prepackagedDatabaseCallback.onOpenPrepackagedDatabase(db); + } finally { + // Close the db and let Room re-open it through a normal path + helper.close(); + } + } + + private SupportSQLiteOpenHelper createFrameworkOpenHelper(File databaseFile) { + final int version; + try { + version = DBUtil.readVersion(databaseFile); + } catch (IOException e) { + throw new RuntimeException("Malformed database file, unable to read version.", e); + } + FrameworkSQLiteOpenHelperFactory factory = new FrameworkSQLiteOpenHelperFactory(); + Configuration configuration = Configuration.builder(mContext) + .name(databaseFile.getAbsolutePath()) + .callback(new Callback(Math.max(version, 1)) { + @Override + public void onCreate(@NonNull SupportSQLiteDatabase db) { + } + + @Override + public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + } + + @Override + public void onOpen(@NonNull SupportSQLiteDatabase db) { + // If pre-packaged database has a version < 1 we will open it as if it was + // version 1 because the framework open helper does not allow version < 1. + // The database will be considered as newly created and onCreate() will be + // invoked, but we do nothing and reset the version back so Room later runs + // migrations as usual. + if (version < 1) { + db.setVersion(version); + } + } + }) + .build(); + return factory.create(configuration); + } +} diff --git a/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt deleted file mode 100644 index 3672ab0225..0000000000 --- a/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2019 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 androidx.room - -import android.content.Context -import android.os.Build -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.room.Room.LOG_TAG -import androidx.room.util.copy -import androidx.room.util.readVersion -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory -import androidx.sqlite.util.ProcessLock -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.lang.Exception -import java.lang.IllegalStateException -import java.lang.RuntimeException -import java.nio.channels.Channels -import java.nio.channels.ReadableByteChannel -import java.util.concurrent.Callable - -/** - * An open helper that will copy & open a pre-populated database if it doesn't exists in internal - * storage. - */ -@Suppress("BanSynchronizedMethods") -internal class SQLiteCopyOpenHelper( - private val context: Context, - private val copyFromAssetPath: String?, - private val copyFromFile: File?, - private val copyFromInputStream: Callable?, - private val databaseVersion: Int, - override val delegate: SupportSQLiteOpenHelper -) : SupportSQLiteOpenHelper, DelegatingOpenHelper { - private lateinit var databaseConfiguration: DatabaseConfiguration - private var verified = false - - override val databaseName: String? - get() = delegate.databaseName - - @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) - override fun setWriteAheadLoggingEnabled(enabled: Boolean) { - delegate.setWriteAheadLoggingEnabled(enabled) - } - - override val writableDatabase: SupportSQLiteDatabase - get() { - if (!verified) { - verifyDatabaseFile(true) - verified = true - } - return delegate.writableDatabase - } - - override val readableDatabase: SupportSQLiteDatabase - get() { - if (!verified) { - verifyDatabaseFile(false) - verified = true - } - return delegate.readableDatabase - } - - @Synchronized - override fun close() { - delegate.close() - verified = false - } - - // Can't be constructor param because the factory is needed by the database builder which in - // turn is the one that actually builds the configuration. - fun setDatabaseConfiguration(databaseConfiguration: DatabaseConfiguration) { - this.databaseConfiguration = databaseConfiguration - } - - private fun verifyDatabaseFile(writable: Boolean) { - val name = checkNotNull(databaseName) - val databaseFile = context.getDatabasePath(name) - val processLevelLock = (databaseConfiguration.multiInstanceInvalidation) - val copyLock = ProcessLock( - name, - context.filesDir, - processLevelLock - ) - try { - // Acquire a copy lock, this lock works across threads and processes, preventing - // concurrent copy attempts from occurring. - copyLock.lock() - if (!databaseFile.exists()) { - try { - // No database file found, copy and be done. - copyDatabaseFile(databaseFile, writable) - return - } catch (e: IOException) { - throw RuntimeException("Unable to copy database file.", e) - } - } - - // A database file is present, check if we need to re-copy it. - val currentVersion = try { - readVersion(databaseFile) - } catch (e: IOException) { - Log.w(LOG_TAG, "Unable to read database version.", e) - return - } - if (currentVersion == databaseVersion) { - return - } - if (databaseConfiguration.isMigrationRequired(currentVersion, databaseVersion)) { - // From the current version to the desired version a migration is required, i.e. - // we won't be performing a copy destructive migration. - return - } - if (context.deleteDatabase(name)) { - try { - copyDatabaseFile(databaseFile, writable) - } catch (e: IOException) { - // We are more forgiving copying a database on a destructive migration since - // there is already a database file that can be opened. - Log.w(LOG_TAG, "Unable to copy database file.", e) - } - } else { - Log.w( - LOG_TAG, "Failed to delete database file ($name) for " + - "a copy destructive migration." - ) - } - } finally { - copyLock.unlock() - } - } - - @Throws(IOException::class) - private fun copyDatabaseFile(destinationFile: File, writable: Boolean) { - val input: ReadableByteChannel - if (copyFromAssetPath != null) { - input = Channels.newChannel(context.assets.open(copyFromAssetPath)) - } else if (copyFromFile != null) { - input = FileInputStream(copyFromFile).channel - } else if (copyFromInputStream != null) { - val inputStream = try { - copyFromInputStream.call() - } catch (e: Exception) { - throw IOException("inputStreamCallable exception on call", e) - } - input = Channels.newChannel(inputStream) - } else { - throw IllegalStateException( - "copyFromAssetPath, copyFromFile and copyFromInputStream are all null!" - ) - } - - // An intermediate file is used so that we never end up with a half-copied database file - // in the internal directory. - val intermediateFile = File.createTempFile( - "room-copy-helper", ".tmp", context.cacheDir - ) - intermediateFile.deleteOnExit() - val output = FileOutputStream(intermediateFile).channel - copy(input, output) - val parent = destinationFile.parentFile - if (parent != null && !parent.exists() && !parent.mkdirs()) { - throw IOException( - "Failed to create directories for ${destinationFile.absolutePath}" - ) - } - - // Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch - // the open pre-packaged callback. If it fails then intermediate file won't be copied making - // invoking pre-packaged callback a transactional operation. - dispatchOnOpenPrepackagedDatabase(intermediateFile, writable) - if (!intermediateFile.renameTo(destinationFile)) { - throw IOException( - "Failed to move intermediate file (${intermediateFile.absolutePath}) to " + - "destination (${destinationFile.absolutePath})." - ) - } - } - - private fun dispatchOnOpenPrepackagedDatabase(databaseFile: File, writable: Boolean) { - if (databaseConfiguration.prepackagedDatabaseCallback == null - ) { - return - } - createFrameworkOpenHelper(databaseFile).use { helper -> - val db = if (writable) helper.writableDatabase else helper.readableDatabase - databaseConfiguration.prepackagedDatabaseCallback!!.onOpenPrepackagedDatabase(db) - } - } - - private fun createFrameworkOpenHelper(databaseFile: File): SupportSQLiteOpenHelper { - val version = try { - readVersion(databaseFile) - } catch (e: IOException) { - throw RuntimeException("Malformed database file, unable to read version.", e) - } - val factory = FrameworkSQLiteOpenHelperFactory() - val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) - .name(databaseFile.absolutePath) - .callback(object : SupportSQLiteOpenHelper.Callback(version.coerceAtLeast(1)) { - override fun onCreate(db: SupportSQLiteDatabase) {} - override fun onUpgrade( - db: SupportSQLiteDatabase, - oldVersion: Int, - newVersion: Int - ) { - } - - override fun onOpen(db: SupportSQLiteDatabase) { - // If pre-packaged database has a version < 1 we will open it as if it was - // version 1 because the framework open helper does not allow version < 1. - // The database will be considered as newly created and onCreate() will be - // invoked, but we do nothing and reset the version back so Room later runs - // migrations as usual. - if (version < 1) { - db.version = version - } - } - }) - .build() - return factory.create(configuration) - } -} diff --git a/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java b/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java new file mode 100644 index 0000000000..84eebe4b72 --- /dev/null +++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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 androidx.room; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.io.File; +import java.io.InputStream; +import java.util.concurrent.Callable; + +/** + * Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates + * {@link SQLiteCopyOpenHelper}. + */ +class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { + + @Nullable + private final String mCopyFromAssetPath; + @Nullable + private final File mCopyFromFile; + @Nullable + private final Callable mCopyFromInputStream; + @NonNull + private final SupportSQLiteOpenHelper.Factory mDelegate; + + SQLiteCopyOpenHelperFactory( + @Nullable String copyFromAssetPath, + @Nullable File copyFromFile, + @Nullable Callable copyFromInputStream, + @NonNull SupportSQLiteOpenHelper.Factory factory) { + mCopyFromAssetPath = copyFromAssetPath; + mCopyFromFile = copyFromFile; + mCopyFromInputStream = copyFromInputStream; + mDelegate = factory; + } + + @NonNull + @Override + public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) { + return new SQLiteCopyOpenHelper( + configuration.context, + mCopyFromAssetPath, + mCopyFromFile, + mCopyFromInputStream, + configuration.callback.version, + mDelegate.create(configuration)); + } +} diff --git a/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.kt b/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.kt deleted file mode 100644 index 9ecb35bdc5..0000000000 --- a/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2019 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 androidx.room - -import androidx.sqlite.db.SupportSQLiteOpenHelper -import java.io.File -import java.io.InputStream -import java.util.concurrent.Callable - -/** - * Implementation of [SupportSQLiteOpenHelper.Factory] that creates - * [SQLiteCopyOpenHelper]. - */ -internal class SQLiteCopyOpenHelperFactory( - private val mCopyFromAssetPath: String?, - private val mCopyFromFile: File?, - private val mCopyFromInputStream: Callable?, - private val mDelegate: SupportSQLiteOpenHelper.Factory -) : SupportSQLiteOpenHelper.Factory { - override fun create( - configuration: SupportSQLiteOpenHelper.Configuration - ): SupportSQLiteOpenHelper { - return SQLiteCopyOpenHelper( - configuration.context, - mCopyFromAssetPath, - mCopyFromFile, - mCopyFromInputStream, - configuration.callback.version, - mDelegate.create(configuration) - ) - } -} diff --git a/app/src/main/java/androidx/room/SharedSQLiteStatement.java b/app/src/main/java/androidx/room/SharedSQLiteStatement.java new file mode 100644 index 0000000000..20c06c8b50 --- /dev/null +++ b/app/src/main/java/androidx/room/SharedSQLiteStatement.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016 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 androidx.room; + +import androidx.annotation.RestrictTo; +import androidx.sqlite.db.SupportSQLiteStatement; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Represents a prepared SQLite state that can be re-used multiple times. + *

    + * This class is used by generated code. After it is used, {@code release} must be called so that + * it can be used by other threads. + *

    + * To avoid re-entry even within the same thread, this class allows only 1 time access to the shared + * statement until it is released. + * + * @hide + */ +@SuppressWarnings({"WeakerAccess", "unused"}) +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public abstract class SharedSQLiteStatement { + private final AtomicBoolean mLock = new AtomicBoolean(false); + + private final RoomDatabase mDatabase; + private volatile SupportSQLiteStatement mStmt; + + /** + * Creates an SQLite prepared statement that can be re-used across threads. If it is in use, + * it automatically creates a new one. + * + * @param database The database to create the statement in. + */ + public SharedSQLiteStatement(RoomDatabase database) { + mDatabase = database; + } + + /** + * Create the query. + * + * @return The SQL query to prepare. + */ + protected abstract String createQuery(); + + protected void assertNotMainThread() { + mDatabase.assertNotMainThread(); + } + + private SupportSQLiteStatement createNewStatement() { + String query = createQuery(); + return mDatabase.compileStatement(query); + } + + private SupportSQLiteStatement getStmt(boolean canUseCached) { + final SupportSQLiteStatement stmt; + if (canUseCached) { + if (mStmt == null) { + mStmt = createNewStatement(); + } + stmt = mStmt; + } else { + // it is in use, create a one off statement + stmt = createNewStatement(); + } + return stmt; + } + + /** + * Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done. + */ + public SupportSQLiteStatement acquire() { + assertNotMainThread(); + return getStmt(mLock.compareAndSet(false, true)); + } + + /** + * Must call this when statement will not be used anymore. + * + * @param statement The statement that was returned from acquire. + */ + public void release(SupportSQLiteStatement statement) { + if (statement == mStmt) { + mLock.set(false); + } + } +} diff --git a/app/src/main/java/androidx/room/SharedSQLiteStatement.kt b/app/src/main/java/androidx/room/SharedSQLiteStatement.kt deleted file mode 100644 index 710426d5b8..0000000000 --- a/app/src/main/java/androidx/room/SharedSQLiteStatement.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2016 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 androidx.room - -import androidx.annotation.RestrictTo -import androidx.sqlite.db.SupportSQLiteStatement -import java.util.concurrent.atomic.AtomicBoolean - -/** - * Represents a prepared SQLite state that can be re-used multiple times. - * - * This class is used by generated code. After it is used, `release` must be called so that - * it can be used by other threads. - * - * To avoid re-entry even within the same thread, this class allows only 1 time access to the shared - * statement until it is released. - * - * @constructor Creates an SQLite prepared statement that can be re-used across threads. If it is - * in use, it automatically creates a new one. - * - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -abstract class SharedSQLiteStatement(private val database: RoomDatabase) { - private val lock = AtomicBoolean(false) - - private val stmt: SupportSQLiteStatement by lazy { - createNewStatement() - } - - /** - * Create the query. - * - * @return The SQL query to prepare. - */ - protected abstract fun createQuery(): String - - protected open fun assertNotMainThread() { - database.assertNotMainThread() - } - - private fun createNewStatement(): SupportSQLiteStatement { - val query = createQuery() - return database.compileStatement(query) - } - - private fun getStmt(canUseCached: Boolean): SupportSQLiteStatement { - val stmt = if (canUseCached) { - stmt - } else { - // it is in use, create a one off statement - createNewStatement() - } - return stmt - } - - /** - * Call this to get the statement. Must call [.release] once done. - */ - open fun acquire(): SupportSQLiteStatement { - assertNotMainThread() - return getStmt(lock.compareAndSet(false, true)) - } - - /** - * Must call this when statement will not be used anymore. - * - * @param statement The statement that was returned from acquire. - */ - open fun release(statement: SupportSQLiteStatement) { - if (statement === stmt) { - lock.set(false) - } - } -} diff --git a/app/src/main/java/androidx/room/SneakyThrow.java b/app/src/main/java/androidx/room/SneakyThrow.java new file mode 100644 index 0000000000..76968ab344 --- /dev/null +++ b/app/src/main/java/androidx/room/SneakyThrow.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 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 androidx.room.util; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + +/** + * Java 8 Sneaky Throw technique. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) +public class SneakyThrow { + + /** + * Re-throws a checked exception as if it was a runtime exception without wrapping it. + * + * @param e the exception to re-throw. + */ + public static void reThrow(@NonNull Exception e) { + sneakyThrow(e); + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(@NonNull Throwable e) throws E { + throw (E) e; + } + + private SneakyThrow() { + + } +} diff --git a/app/src/main/java/androidx/room/StringUtil.java b/app/src/main/java/androidx/room/StringUtil.java new file mode 100644 index 0000000000..2b30eb64de --- /dev/null +++ b/app/src/main/java/androidx/room/StringUtil.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 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 androidx.room.util; + +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +/** + * @hide + * + * String utilities for Room + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public class StringUtil { + + @SuppressWarnings("unused") + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + /** + * Returns a new StringBuilder to be used while producing SQL queries. + * + * @return A new or recycled StringBuilder + */ + public static StringBuilder newStringBuilder() { + // TODO pool: + return new StringBuilder(); + } + + /** + * Adds bind variable placeholders (?) to the given string. Each placeholder is separated + * by a comma. + * + * @param builder The StringBuilder for the query + * @param count Number of placeholders + */ + public static void appendPlaceholders(StringBuilder builder, int count) { + for (int i = 0; i < count; i++) { + builder.append("?"); + if (i < count - 1) { + builder.append(","); + } + } + } + /** + * Splits a comma separated list of integers to integer list. + *

    + * If an input is malformed, it is omitted from the result. + * + * @param input Comma separated list of integers. + * @return A List containing the integers or null if the input is null. + */ + @Nullable + public static List splitToIntList(@Nullable String input) { + if (input == null) { + return null; + } + List result = new ArrayList<>(); + StringTokenizer tokenizer = new StringTokenizer(input, ","); + while (tokenizer.hasMoreElements()) { + final String item = tokenizer.nextToken(); + try { + result.add(Integer.parseInt(item)); + } catch (NumberFormatException ex) { + Log.e("ROOM", "Malformed integer list", ex); + } + } + return result; + } + + /** + * Joins the given list of integers into a comma separated list. + * + * @param input The list of integers. + * @return Comma separated string composed of integers in the list. If the list is null, return + * value is null. + */ + @Nullable + public static String joinIntoString(@Nullable List input) { + if (input == null) { + return null; + } + + final int size = input.size(); + if (size == 0) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < size; i++) { + sb.append(Integer.toString(input.get(i))); + if (i < size - 1) { + sb.append(","); + } + } + return sb.toString(); + } + + private StringUtil() { + } +} diff --git a/app/src/main/java/androidx/room/TableInfo.java b/app/src/main/java/androidx/room/TableInfo.java new file mode 100644 index 0000000000..2d3c8be329 --- /dev/null +++ b/app/src/main/java/androidx/room/TableInfo.java @@ -0,0 +1,743 @@ +/* + * 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 androidx.room.util; + +import android.database.Cursor; +import android.os.Build; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.room.ColumnInfo; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * A data class that holds the information about a table. + *

    + * It directly maps to the result of {@code PRAGMA table_info()}. Check the + * PRAGMA table_info + * documentation for more details. + *

    + * Even though SQLite column names are case insensitive, this class uses case sensitive matching. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources", + "SimplifiableIfStatement"}) +// if you change this class, you must change TableInfoValidationWriter.kt +public final class TableInfo { + + /** + * Identifies from where the info object was created. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE}) + @interface CreatedFrom { + } + + /** + * Identifier for when the info is created from an unknown source. + */ + public static final int CREATED_FROM_UNKNOWN = 0; + + /** + * Identifier for when the info is created from an entity definition, such as generated code + * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file. + */ + public static final int CREATED_FROM_ENTITY = 1; + + /** + * Identifier for when the info is created from the database itself, reading information from a + * PRAGMA, such as table_info. + */ + public static final int CREATED_FROM_DATABASE = 2; + + /** + * The table name. + */ + public final String name; + /** + * Unmodifiable map of columns keyed by column name. + */ + public final Map columns; + + public final Set foreignKeys; + + /** + * Sometimes, Index information is not available (older versions). If so, we skip their + * verification. + */ + @Nullable + public final Set indices; + + @SuppressWarnings("unused") + public TableInfo(String name, Map columns, Set foreignKeys, + Set indices) { + this.name = name; + this.columns = Collections.unmodifiableMap(columns); + this.foreignKeys = Collections.unmodifiableSet(foreignKeys); + this.indices = indices == null ? null : Collections.unmodifiableSet(indices); + } + + /** + * For backward compatibility with dbs created with older versions. + */ + @SuppressWarnings("unused") + public TableInfo(String name, Map columns, Set foreignKeys) { + this(name, columns, foreignKeys, Collections.emptySet()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TableInfo)) return false; + + TableInfo tableInfo = (TableInfo) o; + + if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false; + if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) { + return false; + } + if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys) + : tableInfo.foreignKeys != null) { + return false; + } + if (indices == null || tableInfo.indices == null) { + // if one us is missing index information, seems like we couldn't acquire the + // information so we better skip. + return true; + } + return indices.equals(tableInfo.indices); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (columns != null ? columns.hashCode() : 0); + result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0); + // skip index, it is not reliable for comparison. + return result; + } + + @Override + public String toString() { + return "TableInfo{" + + "name='" + name + '\'' + + ", columns=" + columns + + ", foreignKeys=" + foreignKeys + + ", indices=" + indices + + '}'; + } + + /** + * Reads the table information from the given database. + * + * @param database The database to read the information from. + * @param tableName The table name. + * @return A TableInfo containing the schema information for the provided table name. + */ + @SuppressWarnings("SameParameterValue") + public static TableInfo read(SupportSQLiteDatabase database, String tableName) { + Map columns = readColumns(database, tableName); + Set foreignKeys = readForeignKeys(database, tableName); + Set indices = readIndices(database, tableName); + return new TableInfo(tableName, columns, foreignKeys, indices); + } + + private static Set readForeignKeys(SupportSQLiteDatabase database, + String tableName) { + Set foreignKeys = new HashSet<>(); + // this seems to return everything in order but it is not documented so better be safe + Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)"); + try { + final int idColumnIndex = cursor.getColumnIndex("id"); + final int seqColumnIndex = cursor.getColumnIndex("seq"); + final int tableColumnIndex = cursor.getColumnIndex("table"); + final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete"); + final int onUpdateColumnIndex = cursor.getColumnIndex("on_update"); + + final List ordered = readForeignKeyFieldMappings(cursor); + final int count = cursor.getCount(); + for (int position = 0; position < count; position++) { + cursor.moveToPosition(position); + final int seq = cursor.getInt(seqColumnIndex); + if (seq != 0) { + continue; + } + final int id = cursor.getInt(idColumnIndex); + List myColumns = new ArrayList<>(); + List refColumns = new ArrayList<>(); + for (ForeignKeyWithSequence key : ordered) { + if (key.mId == id) { + myColumns.add(key.mFrom); + refColumns.add(key.mTo); + } + } + foreignKeys.add(new ForeignKey( + cursor.getString(tableColumnIndex), + cursor.getString(onDeleteColumnIndex), + cursor.getString(onUpdateColumnIndex), + myColumns, + refColumns + )); + } + } finally { + cursor.close(); + } + return foreignKeys; + } + + private static List readForeignKeyFieldMappings(Cursor cursor) { + final int idColumnIndex = cursor.getColumnIndex("id"); + final int seqColumnIndex = cursor.getColumnIndex("seq"); + final int fromColumnIndex = cursor.getColumnIndex("from"); + final int toColumnIndex = cursor.getColumnIndex("to"); + final int count = cursor.getCount(); + List result = new ArrayList<>(); + for (int i = 0; i < count; i++) { + cursor.moveToPosition(i); + result.add(new ForeignKeyWithSequence( + cursor.getInt(idColumnIndex), + cursor.getInt(seqColumnIndex), + cursor.getString(fromColumnIndex), + cursor.getString(toColumnIndex) + )); + } + Collections.sort(result); + return result; + } + + private static Map readColumns(SupportSQLiteDatabase database, + String tableName) { + + Cursor cursor = database + .query("PRAGMA table_info(`" + tableName + "`)"); + //noinspection TryFinallyCanBeTryWithResources + Map columns = new HashMap<>(); + try { + if (cursor.getColumnCount() > 0) { + int nameIndex = cursor.getColumnIndex("name"); + int typeIndex = cursor.getColumnIndex("type"); + int notNullIndex = cursor.getColumnIndex("notnull"); + int pkIndex = cursor.getColumnIndex("pk"); + int defaultValueIndex = cursor.getColumnIndex("dflt_value"); + + while (cursor.moveToNext()) { + final String name = cursor.getString(nameIndex); + final String type = cursor.getString(typeIndex); + final boolean notNull = 0 != cursor.getInt(notNullIndex); + final int primaryKeyPosition = cursor.getInt(pkIndex); + final String defaultValue = cursor.getString(defaultValueIndex); + columns.put(name, + new Column(name, type, notNull, primaryKeyPosition, defaultValue, + CREATED_FROM_DATABASE)); + } + } + } finally { + cursor.close(); + } + return columns; + } + + /** + * @return null if we cannot read the indices due to older sqlite implementations. + */ + @Nullable + private static Set readIndices(SupportSQLiteDatabase database, String tableName) { + Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)"); + try { + final int nameColumnIndex = cursor.getColumnIndex("name"); + final int originColumnIndex = cursor.getColumnIndex("origin"); + final int uniqueIndex = cursor.getColumnIndex("unique"); + if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) { + // we cannot read them so better not validate any index. + return null; + } + HashSet indices = new HashSet<>(); + while (cursor.moveToNext()) { + String origin = cursor.getString(originColumnIndex); + if (!"c".equals(origin)) { + // Ignore auto-created indices + continue; + } + String name = cursor.getString(nameColumnIndex); + boolean unique = cursor.getInt(uniqueIndex) == 1; + Index index = readIndex(database, name, unique); + if (index == null) { + // we cannot read it properly so better not read it + return null; + } + indices.add(index); + } + return indices; + } finally { + cursor.close(); + } + } + + /** + * @return null if we cannot read the index due to older sqlite implementations. + */ + @Nullable + private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) { + Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)"); + try { + final int seqnoColumnIndex = cursor.getColumnIndex("seqno"); + final int cidColumnIndex = cursor.getColumnIndex("cid"); + final int nameColumnIndex = cursor.getColumnIndex("name"); + final int descColumnIndex = cursor.getColumnIndex("desc"); + if (seqnoColumnIndex == -1 || cidColumnIndex == -1 + || nameColumnIndex == -1 || descColumnIndex == -1) { + // we cannot read them so better not validate any index. + return null; + } + final TreeMap columnsMap = new TreeMap<>(); + final TreeMap ordersMap = new TreeMap<>(); + + while (cursor.moveToNext()) { + int cid = cursor.getInt(cidColumnIndex); + if (cid < 0) { + // Ignore SQLite row ID + continue; + } + int seq = cursor.getInt(seqnoColumnIndex); + String columnName = cursor.getString(nameColumnIndex); + String order = cursor.getInt(descColumnIndex) > 0 ? "DESC" : "ASC"; + + columnsMap.put(seq, columnName); + ordersMap.put(seq, order); + } + final List columns = new ArrayList<>(columnsMap.size()); + columns.addAll(columnsMap.values()); + final List orders = new ArrayList<>(ordersMap.size()); + orders.addAll(ordersMap.values()); + return new Index(name, unique, columns, orders); + } finally { + cursor.close(); + } + } + + /** + * Holds the information about a database column. + */ + @SuppressWarnings("WeakerAccess") + public static final class Column { + /** + * The column name. + */ + public final String name; + /** + * The column type affinity. + */ + public final String type; + /** + * The column type after it is normalized to one of the basic types according to + * https://www.sqlite.org/datatype3.html Section 3.1. + *

    + * This is the value Room uses for equality check. + */ + @ColumnInfo.SQLiteTypeAffinity + public final int affinity; + /** + * Whether or not the column can be NULL. + */ + public final boolean notNull; + /** + * The position of the column in the list of primary keys, 0 if the column is not part + * of the primary key. + *

    + * This information is only available in API 20+. + * (SQLite version 3.7.16.2) + * On older platforms, it will be 1 if the column is part of the primary key and 0 + * otherwise. + *

    + * The {@link #equals(Object)} implementation handles this inconsistency based on + * API levels os if you are using a custom SQLite deployment, it may return false + * positives. + */ + public final int primaryKeyPosition; + /** + * The default value of this column. + */ + public final String defaultValue; + + @CreatedFrom + private final int mCreatedFrom; + + /** + * @deprecated Use {@link Column#Column(String, String, boolean, int, String, int)} instead. + */ + @Deprecated + public Column(String name, String type, boolean notNull, int primaryKeyPosition) { + this(name, type, notNull, primaryKeyPosition, null, CREATED_FROM_UNKNOWN); + } + + // if you change this constructor, you must change TableInfoWriter.kt + public Column(String name, String type, boolean notNull, int primaryKeyPosition, + String defaultValue, @CreatedFrom int createdFrom) { + this.name = name; + this.type = type; + this.notNull = notNull; + this.primaryKeyPosition = primaryKeyPosition; + this.affinity = findAffinity(type); + this.defaultValue = defaultValue; + this.mCreatedFrom = createdFrom; + } + + /** + * Implements https://www.sqlite.org/datatype3.html section 3.1 + * + * @param type The type that was given to the sqlite + * @return The normalized type which is one of the 5 known affinities + */ + @ColumnInfo.SQLiteTypeAffinity + private static int findAffinity(@Nullable String type) { + if (type == null) { + return ColumnInfo.BLOB; + } + String uppercaseType = type.toUpperCase(Locale.US); + if (uppercaseType.contains("INT")) { + return ColumnInfo.INTEGER; + } + if (uppercaseType.contains("CHAR") + || uppercaseType.contains("CLOB") + || uppercaseType.contains("TEXT")) { + return ColumnInfo.TEXT; + } + if (uppercaseType.contains("BLOB")) { + return ColumnInfo.BLOB; + } + if (uppercaseType.contains("REAL") + || uppercaseType.contains("FLOA") + || uppercaseType.contains("DOUB")) { + return ColumnInfo.REAL; + } + // sqlite returns NUMERIC here but it is like a catch all. We already + // have UNDEFINED so it is better to use UNDEFINED for consistency. + return ColumnInfo.UNDEFINED; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Column)) return false; + + Column column = (Column) o; + if (Build.VERSION.SDK_INT >= 20) { + if (primaryKeyPosition != column.primaryKeyPosition) return false; + } else { + if (isPrimaryKey() != column.isPrimaryKey()) return false; + } + + if (!name.equals(column.name)) return false; + //noinspection SimplifiableIfStatement + if (notNull != column.notNull) return false; + + // Only validate default value if it was defined in an entity, i.e. if the info + // from the compiler itself has it. b/136019383 + if (mCreatedFrom == CREATED_FROM_ENTITY + && column.mCreatedFrom == CREATED_FROM_DATABASE + && (defaultValue != null && !defaultValueEquals(defaultValue, + column.defaultValue))) { + return false; + } else if (mCreatedFrom == CREATED_FROM_DATABASE + && column.mCreatedFrom == CREATED_FROM_ENTITY + && (column.defaultValue != null && !defaultValueEquals( + column.defaultValue, defaultValue))) { + return false; + } else if (mCreatedFrom != CREATED_FROM_UNKNOWN + && mCreatedFrom == column.mCreatedFrom + && (defaultValue != null ? !defaultValueEquals(defaultValue, + column.defaultValue) + : column.defaultValue != null)) { + return false; + } + + return affinity == column.affinity; + } + + /** + * Checks if the default values provided match. Handles the special case in which the + * default value is surrounded by parenthesis (e.g. encountered in b/182284899). + * + * Surrounding parenthesis are removed by SQLite when reading from the database, hence + * this function will check if they are present in the actual value, if so, it will + * compare the two values by ignoring the surrounding parenthesis. + * + */ + public static boolean defaultValueEquals(@NonNull String actual, @Nullable String other) { + if (other == null) { + return false; + } + + if (actual.equals(other)) { + return true; + } else if (containsSurroundingParenthesis(actual)) { + return actual.substring(1, actual.length() - 1).trim().equals(other); + } + return false; + } + + /** + * Checks for potential surrounding parenthesis, if found, removes them and checks if + * remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant, + * and returns true. + */ + private static boolean containsSurroundingParenthesis(@NonNull String actual) { + if (actual.length() == 0) { + return false; + } + int surroundingParenthesis = 0; + for (int i = 0; i < actual.length(); i++) { + char c = actual.charAt(i); + if (i == 0 && c != '(') { + return false; + } + + if (c == '(') { + surroundingParenthesis++; + } else if (c == ')') { + surroundingParenthesis--; + if (surroundingParenthesis == 0 && i != actual.length() - 1) { + return false; + } + } + } + return surroundingParenthesis == 0; + } + + /** + * Returns whether this column is part of the primary key or not. + * + * @return True if this column is part of the primary key, false otherwise. + */ + public boolean isPrimaryKey() { + return primaryKeyPosition > 0; + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + affinity; + result = 31 * result + (notNull ? 1231 : 1237); + result = 31 * result + primaryKeyPosition; + // Default value is not part of the hashcode since we conditionally check it for + // equality which would break the equals + hashcode contract. + // result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Column{" + + "name='" + name + '\'' + + ", type='" + type + '\'' + + ", affinity='" + affinity + '\'' + + ", notNull=" + notNull + + ", primaryKeyPosition=" + primaryKeyPosition + + ", defaultValue='" + defaultValue + '\'' + + '}'; + } + } + + /** + * Holds the information about an SQLite foreign key + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public static final class ForeignKey { + @NonNull + public final String referenceTable; + @NonNull + public final String onDelete; + @NonNull + public final String onUpdate; + @NonNull + public final List columnNames; + @NonNull + public final List referenceColumnNames; + + public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete, + @NonNull String onUpdate, + @NonNull List columnNames, @NonNull List referenceColumnNames) { + this.referenceTable = referenceTable; + this.onDelete = onDelete; + this.onUpdate = onUpdate; + this.columnNames = Collections.unmodifiableList(columnNames); + this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ForeignKey)) return false; + + ForeignKey that = (ForeignKey) o; + + if (!referenceTable.equals(that.referenceTable)) return false; + if (!onDelete.equals(that.onDelete)) return false; + if (!onUpdate.equals(that.onUpdate)) return false; + //noinspection SimplifiableIfStatement + if (!columnNames.equals(that.columnNames)) return false; + return referenceColumnNames.equals(that.referenceColumnNames); + } + + @Override + public int hashCode() { + int result = referenceTable.hashCode(); + result = 31 * result + onDelete.hashCode(); + result = 31 * result + onUpdate.hashCode(); + result = 31 * result + columnNames.hashCode(); + result = 31 * result + referenceColumnNames.hashCode(); + return result; + } + + @Override + public String toString() { + return "ForeignKey{" + + "referenceTable='" + referenceTable + '\'' + + ", onDelete='" + onDelete + '\'' + + ", onUpdate='" + onUpdate + '\'' + + ", columnNames=" + columnNames + + ", referenceColumnNames=" + referenceColumnNames + + '}'; + } + } + + /** + * Temporary data holder for a foreign key row in the pragma result. We need this to ensure + * sorting in the generated foreign key object. + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + static class ForeignKeyWithSequence implements Comparable { + final int mId; + final int mSequence; + final String mFrom; + final String mTo; + + ForeignKeyWithSequence(int id, int sequence, String from, String to) { + mId = id; + mSequence = sequence; + mFrom = from; + mTo = to; + } + + @Override + public int compareTo(@NonNull ForeignKeyWithSequence o) { + final int idCmp = mId - o.mId; + if (idCmp == 0) { + return mSequence - o.mSequence; + } else { + return idCmp; + } + } + } + + /** + * Holds the information about an SQLite index + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) + public static final class Index { + // should match the value in Index.kt + public static final String DEFAULT_PREFIX = "index_"; + public final String name; + public final boolean unique; + public final List columns; + public final List orders; + + /** + * @deprecated Use {@link #Index(String, boolean, List, List)} + */ + public Index(String name, boolean unique, List columns) { + this(name, unique, columns, null); + } + + public Index(String name, boolean unique, List columns, List orders) { + this.name = name; + this.unique = unique; + this.columns = columns; + this.orders = orders == null || orders.size() == 0 + ? Collections.nCopies(columns.size(), androidx.room.Index.Order.ASC.name()) + : orders; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Index)) return false; + + Index index = (Index) o; + if (unique != index.unique) { + return false; + } + if (!columns.equals(index.columns)) { + return false; + } + if (!orders.equals(index.orders)) { + return false; + } + if (name.startsWith(Index.DEFAULT_PREFIX)) { + return index.name.startsWith(Index.DEFAULT_PREFIX); + } else { + return name.equals(index.name); + } + } + + @Override + public int hashCode() { + int result; + if (name.startsWith(DEFAULT_PREFIX)) { + result = DEFAULT_PREFIX.hashCode(); + } else { + result = name.hashCode(); + } + result = 31 * result + (unique ? 1 : 0); + result = 31 * result + columns.hashCode(); + result = 31 * result + orders.hashCode(); + return result; + } + + @Override + public String toString() { + return "Index{" + + "name='" + name + '\'' + + ", unique=" + unique + + ", columns=" + columns + + ", orders=" + orders + + '}'; + } + } +} diff --git a/app/src/main/java/androidx/room/TransactionExecutor.kt b/app/src/main/java/androidx/room/TransactionExecutor.java similarity index 53% rename from app/src/main/java/androidx/room/TransactionExecutor.kt rename to app/src/main/java/androidx/room/TransactionExecutor.java index 93ba86d78d..53648d377b 100644 --- a/app/src/main/java/androidx/room/TransactionExecutor.kt +++ b/app/src/main/java/androidx/room/TransactionExecutor.java @@ -13,42 +13,52 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.room -import java.util.ArrayDeque -import java.util.concurrent.Executor +package androidx.room; + +import androidx.annotation.NonNull; + +import java.util.ArrayDeque; +import java.util.concurrent.Executor; /** * Executor wrapper for performing database transactions serially. - * + *

    * Since database transactions are exclusive, this executor ensures that transactions are performed * in-order and one at a time, preventing threads from blocking each other when multiple concurrent * transactions are attempted. */ -internal class TransactionExecutor(private val executor: Executor) : Executor { - private val tasks = ArrayDeque() - private var active: Runnable? = null - private val syncLock = Any() - override fun execute(command: Runnable) { - synchronized(syncLock) { - tasks.offer(Runnable { +class TransactionExecutor implements Executor { + + private final Executor mExecutor; + private final ArrayDeque mTasks = new ArrayDeque<>(); + private Runnable mActive; + + TransactionExecutor(@NonNull Executor executor) { + mExecutor = executor; + } + + @Override + public synchronized void execute(final Runnable command) { + mTasks.offer(new Runnable() { + @Override + public void run() { try { - command.run() + command.run(); } finally { - scheduleNext() + scheduleNext(); } - }) - if (active == null) { - scheduleNext() } + }); + if (mActive == null) { + scheduleNext(); } } - fun scheduleNext() { - synchronized(syncLock) { - if (tasks.poll().also { active = it } != null) { - executor.execute(active) - } + @SuppressWarnings("WeakerAccess") + synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + mExecutor.execute(mActive); } } } diff --git a/app/src/main/java/androidx/room/UUIDUtil.java b/app/src/main/java/androidx/room/UUIDUtil.java new file mode 100644 index 0000000000..275a95ca76 --- /dev/null +++ b/app/src/main/java/androidx/room/UUIDUtil.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 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 androidx.room.util; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * UUID / byte[] two-way conversion utility for Room + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public final class UUIDUtil { + + // private constructor to prevent instantiation + private UUIDUtil() {} + + /** + * Converts a 16-bytes array BLOB into a UUID pojo + * + * @param bytes byte array stored in database as BLOB + * @return a UUID object created based on the provided byte array + */ + @NonNull + public static UUID convertByteToUUID(@NonNull byte[] bytes) { + ByteBuffer buffer = ByteBuffer.wrap(bytes); + long firstLong = buffer.getLong(); + long secondLong = buffer.getLong(); + return new UUID(firstLong, secondLong); + } + + /** + * Converts a UUID pojo into a 16-bytes array to store into database as BLOB + * + * @param uuid the UUID pojo + * @return a byte array to store into database + */ + @NonNull + public static byte[] convertUUIDToByte(@NonNull UUID uuid) { + byte[] bytes = new byte[16]; + ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.putLong(uuid.getMostSignificantBits()); + buffer.putLong(uuid.getLeastSignificantBits()); + return buffer.array(); + } +} diff --git a/app/src/main/java/androidx/room/ViewInfo.java b/app/src/main/java/androidx/room/ViewInfo.java new file mode 100644 index 0000000000..81e6aa901c --- /dev/null +++ b/app/src/main/java/androidx/room/ViewInfo.java @@ -0,0 +1,97 @@ +/* + * Copyright 2018 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 androidx.room.util; + +import android.database.Cursor; + +import androidx.annotation.RestrictTo; +import androidx.sqlite.db.SupportSQLiteDatabase; + +/** + * A data class that holds the information about a view. + *

    + * This derives information from sqlite_master. + *

    + * Even though SQLite column names are case insensitive, this class uses case sensitive matching. + * + * @hide + */ +@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) +public final class ViewInfo { + + /** + * The view name + */ + public final String name; + + /** + * The SQL of CREATE VIEW. + */ + public final String sql; + + public ViewInfo(String name, String sql) { + this.name = name; + this.sql = sql; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ViewInfo)) return false; + ViewInfo viewInfo = (ViewInfo) o; + return (name != null ? name.equals(viewInfo.name) : viewInfo.name == null) + && (sql != null ? sql.equals(viewInfo.sql) : viewInfo.sql == null); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (sql != null ? sql.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ViewInfo{" + + "name='" + name + '\'' + + ", sql='" + sql + '\'' + + '}'; + } + + /** + * Reads the view information from the given database. + * + * @param database The database to read the information from. + * @param viewName The view name. + * @return A ViewInfo containing the schema information for the provided view name. + */ + @SuppressWarnings("SameParameterValue") + public static ViewInfo read(SupportSQLiteDatabase database, String viewName) { + Cursor cursor = database.query("SELECT name, sql FROM sqlite_master " + + "WHERE type = 'view' AND name = '" + viewName + "'"); + //noinspection TryFinallyCanBeTryWithResources + try { + if (cursor.moveToFirst()) { + return new ViewInfo(cursor.getString(0), cursor.getString(1)); + } else { + return new ViewInfo(viewName, null); + } + } finally { + cursor.close(); + } + } +} diff --git a/app/src/main/java/androidx/room/util/CursorUtil.kt b/app/src/main/java/androidx/room/util/CursorUtil.kt deleted file mode 100644 index 6e94283440..0000000000 --- a/app/src/main/java/androidx/room/util/CursorUtil.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2018 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:JvmName("CursorUtil") -@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - -package androidx.room.util - -import android.database.Cursor -import android.database.CursorWrapper -import android.database.MatrixCursor -import android.os.Build -import android.util.Log -import androidx.annotation.RestrictTo -import androidx.annotation.VisibleForTesting - -/** - * Copies the given cursor into a in-memory cursor and then closes it. - * - * - * This is useful for iterating over a cursor multiple times without the cost of JNI while - * reading or IO while filling the window at the expense of memory consumption. - * - * @param c the cursor to copy. - * @return a new cursor containing the same data as the given cursor. - */ -fun copyAndClose(c: Cursor): Cursor = c.useCursor { cursor -> - val matrixCursor = MatrixCursor(cursor.columnNames, cursor.count) - while (cursor.moveToNext()) { - val row = arrayOfNulls(cursor.columnCount) - for (i in 0 until c.columnCount) { - when (cursor.getType(i)) { - Cursor.FIELD_TYPE_NULL -> row[i] = null - Cursor.FIELD_TYPE_INTEGER -> row[i] = cursor.getLong(i) - Cursor.FIELD_TYPE_FLOAT -> row[i] = cursor.getDouble(i) - Cursor.FIELD_TYPE_STRING -> row[i] = cursor.getString(i) - Cursor.FIELD_TYPE_BLOB -> row[i] = cursor.getBlob(i) - else -> throw IllegalStateException() - } - } - matrixCursor.addRow(row) - } - matrixCursor -} - -/** - * Patches [Cursor.getColumnIndex] to work around issues on older devices. - * If the column is not found, it retries with the specified name surrounded by backticks. - * - * @param c The cursor. - * @param name The name of the target column. - * @return The index of the column, or -1 if not found. - */ -fun getColumnIndex(c: Cursor, name: String): Int { - var index = c.getColumnIndex(name) - if (index >= 0) { - return index - } - index = c.getColumnIndex("`$name`") - return if (index >= 0) { - index - } else { - findColumnIndexBySuffix(c, name) - } -} - -/** - * Patches [Cursor.getColumnIndexOrThrow] to work around issues on older devices. - * If the column is not found, it retries with the specified name surrounded by backticks. - * - * @param c The cursor. - * @param name The name of the target column. - * @return The index of the column. - * @throws IllegalArgumentException if the column does not exist. - */ -fun getColumnIndexOrThrow(c: Cursor, name: String): Int { - val index: Int = getColumnIndex(c, name) - if (index >= 0) { - return index - } - val availableColumns = try { - c.columnNames.joinToString() - } catch (e: Exception) { - Log.d("RoomCursorUtil", "Cannot collect column names for debug purposes", e) - "unknown" - } - throw IllegalArgumentException( - "column '$name' does not exist. Available columns: $availableColumns" - ) -} - -/** - * Finds a column by name by appending `.` in front of it and checking by suffix match. - * Also checks for the version wrapped with `` (backticks). - * workaround for b/157261134 for API levels 25 and below - * - * e.g. "foo" will match "any.foo" and "`any.foo`" - */ -private fun findColumnIndexBySuffix(cursor: Cursor, name: String): Int { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { - // we need this workaround only on APIs < 26. So just return not found on newer APIs - return -1 - } - if (name.isEmpty()) { - return -1 - } - val columnNames = cursor.columnNames - return findColumnIndexBySuffix(columnNames, name) -} - -@VisibleForTesting -fun findColumnIndexBySuffix(columnNames: Array, name: String): Int { - val dotSuffix = ".$name" - val backtickSuffix = ".$name`" - columnNames.forEachIndexed { index, columnName -> - // do not check if column name is not long enough. 1 char for table name, 1 char for '.' - if (columnName.length >= name.length + 2) { - if (columnName.endsWith(dotSuffix)) { - return index - } else if (columnName[0] == '`' && columnName.endsWith(backtickSuffix)) { - return index - } - } - } - return -1 -} - -/** - * Backwards compatible function that executes the given block function on this Cursor and then - * closes the Cursor. - */ -inline fun Cursor.useCursor(block: (Cursor) -> R): R { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { - return this.use(block) - } else { - try { - return block(this) - } finally { - this.close() - } - } -} - -/** - * Wraps the given cursor such that `getColumnIndex()` will utilize the provided - * `mapping` when getting the index of a column in `columnNames`. - * - * This is useful when the original cursor contains duplicate columns. Instead of letting the - * cursor return the first matching column with a name, we can resolve the ambiguous column - * indices and wrap the cursor such that for a set of desired column indices, the returned - * value will be that from the pre-computation. - * - * @param cursor the cursor to wrap. - * @param columnNames the column names whose index are known. The result column index of the - * column name at i will be at `mapping[i]`. - * @param mapping the cursor column indices of the columns at `columnNames`. - * @return the wrapped Cursor. - */ -fun wrapMappedColumns(cursor: Cursor, columnNames: Array, mapping: IntArray): Cursor { - check(columnNames.size == mapping.size) { "Expected columnNames.length == mapping.length" } - return object : CursorWrapper(cursor) { - override fun getColumnIndex(columnName: String): Int { - columnNames.forEachIndexed { i, mappedColumnName -> - if (mappedColumnName.equals(columnName, ignoreCase = true)) { - return mapping[i] - } - } - return super.getColumnIndex(columnName) - } - } -} diff --git a/app/src/main/java/androidx/room/util/DBUtil.kt b/app/src/main/java/androidx/room/util/DBUtil.kt deleted file mode 100644 index 99a5d96b48..0000000000 --- a/app/src/main/java/androidx/room/util/DBUtil.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2018 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:JvmName("DBUtil") -@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - -package androidx.room.util - -import android.database.AbstractWindowedCursor -import android.database.Cursor -import android.database.sqlite.SQLiteConstraintException -import android.os.Build -import android.os.CancellationSignal -import androidx.annotation.RestrictTo -import androidx.room.RoomDatabase -import androidx.sqlite.db.SupportSQLiteCompat -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteQuery -import java.io.File -import java.io.FileInputStream -import java.io.IOException -import java.nio.ByteBuffer - -/** - * Performs the SQLiteQuery on the given database. - * - * This util method encapsulates copying the cursor if the `maybeCopy` parameter is - * `true` and either the api level is below a certain threshold or the full result of the - * query does not fit in a single window. - * - * @param db The database to perform the query on. - * @param sqLiteQuery The query to perform. - * @param maybeCopy True if the result cursor should maybe be copied, false otherwise. - * @return Result of the query. - * - */ -@Deprecated( - "This is only used in the generated code and shouldn't be called directly." -) -fun query(db: RoomDatabase, sqLiteQuery: SupportSQLiteQuery, maybeCopy: Boolean): Cursor { - return query(db, sqLiteQuery, maybeCopy, null) -} - -/** - * Performs the SQLiteQuery on the given database. - * - * This util method encapsulates copying the cursor if the `maybeCopy` parameter is - * `true` and either the api level is below a certain threshold or the full result of the - * query does not fit in a single window. - * - * @param db The database to perform the query on. - * @param sqLiteQuery The query to perform. - * @param maybeCopy True if the result cursor should maybe be copied, false otherwise. - * @param signal The cancellation signal to be attached to the query. - * @return Result of the query. - */ -fun query( - db: RoomDatabase, - sqLiteQuery: SupportSQLiteQuery, - maybeCopy: Boolean, - signal: CancellationSignal? -): Cursor { - val cursor = db.query(sqLiteQuery, signal) - if (maybeCopy && cursor is AbstractWindowedCursor) { - val rowsInCursor = cursor.count // Should fill the window. - val rowsInWindow = if (cursor.hasWindow()) { - cursor.window.numRows - } else { - rowsInCursor - } - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) { - return copyAndClose(cursor) - } - } - return cursor -} - -/** - * Drops all FTS content sync triggers created by Room. - * - * FTS content sync triggers created by Room are those that are found in the sqlite_master table - * who's names start with 'room_fts_content_sync_'. - * - * @param db The database. - */ -fun dropFtsSyncTriggers(db: SupportSQLiteDatabase) { - val existingTriggers = buildList { - db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'").useCursor { cursor -> - while (cursor.moveToNext()) { - add(cursor.getString(0)) - } - } - } - - existingTriggers.forEach { triggerName -> - if (triggerName.startsWith("room_fts_content_sync_")) { - db.execSQL("DROP TRIGGER IF EXISTS $triggerName") - } - } -} - -/** - * Checks for foreign key violations by executing a PRAGMA foreign_key_check. - */ -fun foreignKeyCheck( - db: SupportSQLiteDatabase, - tableName: String -) { - db.query("PRAGMA foreign_key_check(`$tableName`)").useCursor { cursor -> - if (cursor.count > 0) { - val errorMsg = processForeignKeyCheckFailure(cursor) - throw SQLiteConstraintException(errorMsg) - } - } -} - -/** - * Reads the user version number out of the database header from the given file. - * - * @param databaseFile the database file. - * @return the database version - * @throws IOException if something goes wrong reading the file, such as bad database header or - * missing permissions. - * - * @see [User Version - * Number](https://www.sqlite.org/fileformat.html.user_version_number). - */ -@Throws(IOException::class) -fun readVersion(databaseFile: File): Int { - FileInputStream(databaseFile).channel.use { input -> - val buffer = ByteBuffer.allocate(4) - input.tryLock(60, 4, true) - input.position(60) - val read = input.read(buffer) - if (read != 4) { - throw IOException("Bad database header, unable to read 4 bytes at offset 60") - } - buffer.rewind() - return buffer.int // ByteBuffer is big-endian by default - } -} - -/** - * CancellationSignal is only available from API 16 on. This function will create a new - * instance of the Cancellation signal only if the current API > 16. - * - * @return A new instance of CancellationSignal or null. - */ -fun createCancellationSignal(): CancellationSignal? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - SupportSQLiteCompat.Api16Impl.createCancellationSignal() - } else { - null - } -} - -/** - * Converts the [Cursor] returned in case of a foreign key violation into a detailed - * error message for debugging. - * - * The foreign_key_check pragma returns one row output for each foreign key violation. - * - * The cursor received has four columns for each row output. The first column is the name of - * the child table. The second column is the rowId of the row that contains the foreign key - * violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the - * name of the parent table. The fourth column is the index of the specific foreign key - * constraint that failed. - * - * @param cursor Cursor containing information regarding the FK violation - * @return Error message generated containing debugging information - */ -private fun processForeignKeyCheckFailure(cursor: Cursor): String { - return buildString { - val rowCount = cursor.count - val fkParentTables = mutableMapOf() - - while (cursor.moveToNext()) { - if (cursor.isFirst) { - append("Foreign key violation(s) detected in '") - append(cursor.getString(0)).append("'.\n") - } - val constraintIndex = cursor.getString(3) - if (!fkParentTables.containsKey(constraintIndex)) { - fkParentTables[constraintIndex] = cursor.getString(2) - } - } - - append("Number of different violations discovered: ") - append(fkParentTables.keys.size).append("\n") - append("Number of rows in violation: ") - append(rowCount).append("\n") - append("Violation(s) detected in the following constraint(s):\n") - - for ((key, value) in fkParentTables) { - append("\tParent Table = ") - append(value) - append(", Foreign Key Constraint Index = ") - append(key).append("\n") - } - } -} diff --git a/app/src/main/java/androidx/room/util/FileUtil.kt b/app/src/main/java/androidx/room/util/FileUtil.kt deleted file mode 100644 index c1f843b7b8..0000000000 --- a/app/src/main/java/androidx/room/util/FileUtil.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2019 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:JvmName("FileUtil") -@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - -package androidx.room.util - -import android.annotation.SuppressLint -import android.os.Build -import androidx.annotation.RestrictTo -import java.io.IOException -import java.nio.channels.Channels -import java.nio.channels.FileChannel -import java.nio.channels.ReadableByteChannel - -/** - * Copies data from the input channel to the output file channel. - * - * @param input the input channel to copy. - * @param output the output channel to copy. - * @throws IOException if there is an I/O error. - */ -@SuppressLint("LambdaLast") -@Throws(IOException::class) -fun copy(input: ReadableByteChannel, output: FileChannel) { - try { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { - output.transferFrom(input, 0, Long.MAX_VALUE) - } else { - val inputStream = Channels.newInputStream(input) - val outputStream = Channels.newOutputStream(output) - var length: Int - val buffer = ByteArray(1024 * 4) - - // TODO: Use Kotlin stdlib IO APIs - while (inputStream.read(buffer).also { length = it } > 0) { - outputStream.write(buffer, 0, length) - } - } - output.force(false) - } finally { - input.close() - output.close() - } -} diff --git a/app/src/main/java/androidx/room/util/FtsTableInfo.kt b/app/src/main/java/androidx/room/util/FtsTableInfo.kt deleted file mode 100644 index b533798cdd..0000000000 --- a/app/src/main/java/androidx/room/util/FtsTableInfo.kt +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2018 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 androidx.room.util - -import androidx.annotation.RestrictTo -import androidx.annotation.VisibleForTesting -import androidx.sqlite.db.SupportSQLiteDatabase -import java.util.ArrayDeque - -/** - * A data class that holds the information about an FTS table. - * - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -class FtsTableInfo( - /** - * The table name - */ - @JvmField - val name: String, - - /** - * The column names - */ - @JvmField - val columns: Set, - - /** - * The set of options. Each value in the set contains the option in the following format: - * . - */ - @JvmField - val options: Set -) { - constructor(name: String, columns: Set, createSql: String) : - this(name, columns, parseOptions(createSql)) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is FtsTableInfo) return false - val that = other - if (name != that.name) return false - if (columns != that.columns) return false - return options == that.options - } - - override fun hashCode(): Int { - var result = name.hashCode() - result = 31 * result + (columns.hashCode()) - result = 31 * result + (options.hashCode()) - return result - } - - override fun toString(): String { - return ("FtsTableInfo{name='$name', columns=$columns, options=$options'}") - } - - companion object { - // A set of valid FTS Options - private val FTS_OPTIONS = arrayOf( - "tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=", - "order=", "prefix=", "uncompress=" - ) - - /** - * Reads the table information from the given database. - * - * @param database The database to read the information from. - * @param tableName The table name. - * @return A FtsTableInfo containing the columns and options for the provided table name. - */ - @JvmStatic - fun read(database: SupportSQLiteDatabase, tableName: String): FtsTableInfo { - val columns = readColumns(database, tableName) - val options = readOptions(database, tableName) - return FtsTableInfo(tableName, columns, options) - } - - private fun readColumns(database: SupportSQLiteDatabase, tableName: String): Set { - return buildSet { - database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor -> - if (cursor.columnCount > 0) { - val nameIndex = cursor.getColumnIndex("name") - while (cursor.moveToNext()) { - add(cursor.getString(nameIndex)) - } - } - } - } - } - - private fun readOptions(database: SupportSQLiteDatabase, tableName: String): Set { - val sql = database.query( - "SELECT * FROM sqlite_master WHERE `name` = '$tableName'" - ).useCursor { cursor -> - if (cursor.moveToFirst()) { - cursor.getString(cursor.getColumnIndexOrThrow("sql")) - } else { - "" - } - } - return parseOptions(sql) - } - - /** - * Parses FTS options from the create statement of an FTS table. - * - * This method assumes the given create statement is a valid well-formed SQLite statement as - * defined in the [CREATE VIRTUAL TABLE - * syntax diagram](https://www.sqlite.org/lang_createvtab.html). - * - * @param createStatement the "CREATE VIRTUAL TABLE" statement. - * @return the set of FTS option key and values in the create statement. - */ - @VisibleForTesting - @JvmStatic - fun parseOptions(createStatement: String): Set { - if (createStatement.isEmpty()) { - return emptySet() - } - - // Module arguments are within the parenthesis followed by the module name. - val argsString = createStatement.substring( - createStatement.indexOf('(') + 1, - createStatement.lastIndexOf(')') - ) - - // Split the module argument string by the comma delimiter, keeping track of quotation - // so that if the delimiter is found within a string literal we don't substring at the - // wrong index. SQLite supports four ways of quoting keywords, see: - // https://www.sqlite.org/lang_keywords.html - val args = mutableListOf() - val quoteStack = ArrayDeque() - var lastDelimiterIndex = -1 - argsString.forEachIndexed { i, value -> - when (value) { - '\'', '"', '`' -> - if (quoteStack.isEmpty()) { - quoteStack.push(value) - } else if (quoteStack.peek() == value) { - quoteStack.pop() - } - '[' -> if (quoteStack.isEmpty()) { - quoteStack.push(value) - } - ']' -> if (!quoteStack.isEmpty() && quoteStack.peek() == '[') { - quoteStack.pop() - } - ',' -> if (quoteStack.isEmpty()) { - args.add(argsString.substring(lastDelimiterIndex + 1, i).trim { it <= ' ' }) - lastDelimiterIndex = i - } - } - } - - // Add final argument. - args.add(argsString.substring(lastDelimiterIndex + 1).trim()) - - // Match args against valid options, otherwise they are column definitions. - val options = args.filter { arg -> - FTS_OPTIONS.any { validOption -> - arg.startsWith(validOption) - } - }.toSet() - return options - } - } -} diff --git a/app/src/main/java/androidx/room/util/RelationUtil.kt b/app/src/main/java/androidx/room/util/RelationUtil.kt deleted file mode 100644 index cefa581102..0000000000 --- a/app/src/main/java/androidx/room/util/RelationUtil.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2022 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:JvmName("RelationUtil") -@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - -package androidx.room.util - -import androidx.annotation.RestrictTo -import androidx.collection.ArrayMap -import androidx.collection.LongSparseArray -import androidx.room.RoomDatabase - -/** - * Utility function used in generated code to recursively fetch relationships when the amount of - * keys exceed [RoomDatabase.MAX_BIND_PARAMETER_CNT]. - * - * @param map - The map containing the relationship keys to fill-in. - * @param isRelationCollection - True if [V] is a [Collection] which means it is non null. - * @param fetchBlock - A lambda for calling the generated _fetchRelationship function. - */ -fun recursiveFetchHashMap( - map: HashMap, - isRelationCollection: Boolean, - fetchBlock: (HashMap) -> Unit -) { - val tmpMap = HashMap(RoomDatabase.MAX_BIND_PARAMETER_CNT) - var count = 0 - for (key in map.keys) { - // Safe because `V` is a nullable type arg when isRelationCollection == false and vice versa - @Suppress("UNCHECKED_CAST") - if (isRelationCollection) { - tmpMap[key] = map[key] as V - } else { - tmpMap[key] = null as V - } - count++ - if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) { - // recursively load that batch - fetchBlock(tmpMap) - // for non collection relation, put the loaded batch in the original map, - // not needed when dealing with collections since references are passed - if (!isRelationCollection) { - map.putAll(tmpMap) - } - tmpMap.clear() - count = 0 - } - } - if (count > 0) { - // load the last batch - fetchBlock(tmpMap) - // for non collection relation, put the last batch in the original map - if (!isRelationCollection) { - map.putAll(tmpMap) - } - } -} - -/** - * Same as [recursiveFetchHashMap] but for [LongSparseArray]. - */ -fun recursiveFetchLongSparseArray( - map: LongSparseArray, - isRelationCollection: Boolean, - fetchBlock: (LongSparseArray) -> Unit -) { - val tmpMap = LongSparseArray(RoomDatabase.MAX_BIND_PARAMETER_CNT) - var count = 0 - var mapIndex = 0 - val limit = map.size() - while (mapIndex < limit) { - if (isRelationCollection) { - tmpMap.put(map.keyAt(mapIndex), map.valueAt(mapIndex)) - } else { - // Safe because `V` is a nullable type arg when isRelationCollection == false - @Suppress("UNCHECKED_CAST") - tmpMap.put(map.keyAt(mapIndex), null as V) - } - mapIndex++ - count++ - if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) { - fetchBlock(tmpMap) - if (!isRelationCollection) { - map.putAll(tmpMap) - } - tmpMap.clear() - count = 0 - } - } - if (count > 0) { - fetchBlock(tmpMap) - if (!isRelationCollection) { - map.putAll(tmpMap) - } - } -} - -/** - * Same as [recursiveFetchHashMap] but for [ArrayMap]. - */ -fun recursiveFetchArrayMap( - map: ArrayMap, - isRelationCollection: Boolean, - fetchBlock: (ArrayMap) -> Unit -) { - val tmpMap = ArrayMap(RoomDatabase.MAX_BIND_PARAMETER_CNT) - var count = 0 - var mapIndex = 0 - val limit = map.size - while (mapIndex < limit) { - if (isRelationCollection) { - tmpMap[map.keyAt(mapIndex)] = map.valueAt(mapIndex) - } else { - tmpMap[map.keyAt(mapIndex)] = null - } - mapIndex++ - count++ - if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) { - fetchBlock(tmpMap) - if (!isRelationCollection) { - // Cast needed to disambiguate from putAll(SimpleArrayMap) - map.putAll(tmpMap as Map) - } - tmpMap.clear() - count = 0 - } - } - if (count > 0) { - fetchBlock(tmpMap) - if (!isRelationCollection) { - // Cast needed to disambiguate from putAll(SimpleArrayMap) - map.putAll(tmpMap as Map) - } - } -} diff --git a/app/src/main/java/androidx/room/util/StringUtil.kt b/app/src/main/java/androidx/room/util/StringUtil.kt deleted file mode 100644 index f8565fad97..0000000000 --- a/app/src/main/java/androidx/room/util/StringUtil.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2016 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:JvmName("StringUtil") -@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - -package androidx.room.util - -import android.util.Log -import androidx.annotation.RestrictTo -import java.lang.NumberFormatException -import java.lang.StringBuilder - -@Suppress("unused") -@JvmField -val EMPTY_STRING_ARRAY = arrayOfNulls(0) - -/** - * Returns a new StringBuilder to be used while producing SQL queries. - * - * @return A new or recycled StringBuilder - */ -fun newStringBuilder(): StringBuilder { - // TODO pool: - return StringBuilder() -} - -/** - * Adds bind variable placeholders (?) to the given string. Each placeholder is separated - * by a comma. - * - * @param builder The StringBuilder for the query - * @param count Number of placeholders - */ -fun appendPlaceholders(builder: StringBuilder, count: Int) { - for (i in 0 until count) { - builder.append("?") - if (i < count - 1) { - builder.append(",") - } - } -} - -/** - * Splits a comma separated list of integers to integer list. - * - * - * If an input is malformed, it is omitted from the result. - * - * @param input Comma separated list of integers. - * @return A List containing the integers or null if the input is null. - */ -fun splitToIntList(input: String?): List? { - return input?.split(',')?.mapNotNull { item -> - try { - item.toInt() - } catch (ex: NumberFormatException) { - Log.e("ROOM", "Malformed integer list", ex) - null - } - } -} - -/** - * Joins the given list of integers into a comma separated list. - * - * @param input The list of integers. - * @return Comma separated string composed of integers in the list. If the list is null, return - * value is null. - */ -fun joinIntoString(input: List?): String? { - return input?.joinToString(",") -} diff --git a/app/src/main/java/androidx/room/util/TableInfo.kt b/app/src/main/java/androidx/room/util/TableInfo.kt deleted file mode 100644 index 7cded96ab1..0000000000 --- a/app/src/main/java/androidx/room/util/TableInfo.kt +++ /dev/null @@ -1,645 +0,0 @@ -/* - * 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 androidx.room.util - -import android.database.Cursor -import android.os.Build -import androidx.annotation.IntDef -import androidx.annotation.RestrictTo -import androidx.annotation.VisibleForTesting -import androidx.room.ColumnInfo -import androidx.room.ColumnInfo.SQLiteTypeAffinity -import androidx.sqlite.db.SupportSQLiteDatabase -import java.util.Locale -import java.util.TreeMap - -/** - * A data class that holds the information about a table. - * - * It directly maps to the result of `PRAGMA table_info()`. Check the - * [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info) - * documentation for more details. - * - * Even though SQLite column names are case insensitive, this class uses case sensitive matching. - * - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -// if you change this class, you must change TableInfoValidationWriter.kt -class TableInfo( - /** - * The table name. - */ - @JvmField - val name: String, - @JvmField - val columns: Map, - @JvmField - val foreignKeys: Set, - @JvmField - val indices: Set? = null -) { - /** - * Identifies from where the info object was created. - */ - @Retention(AnnotationRetention.SOURCE) - @IntDef(value = [CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE]) - internal annotation class CreatedFrom() - - /** - * For backward compatibility with dbs created with older versions. - */ - @SuppressWarnings("unused") - constructor( - name: String, - columns: Map, - foreignKeys: Set - ) : this(name, columns, foreignKeys, emptySet()) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is TableInfo) return false - if (name != other.name) return false - if (columns != other.columns) { - return false - } - if (foreignKeys != other.foreignKeys) { - return false - } - return if (indices == null || other.indices == null) { - // if one us is missing index information, seems like we couldn't acquire the - // information so we better skip. - true - } else indices == other.indices - } - - override fun hashCode(): Int { - var result = name.hashCode() - result = 31 * result + columns.hashCode() - result = 31 * result + foreignKeys.hashCode() - // skip index, it is not reliable for comparison. - return result - } - - override fun toString(): String { - return ("TableInfo{name='$name', columns=$columns, foreignKeys=$foreignKeys, " + - "indices=$indices}") - } - - companion object { - /** - * Identifier for when the info is created from an unknown source. - */ - const val CREATED_FROM_UNKNOWN = 0 - - /** - * Identifier for when the info is created from an entity definition, such as generated code - * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file. - */ - const val CREATED_FROM_ENTITY = 1 - - /** - * Identifier for when the info is created from the database itself, reading information - * from a PRAGMA, such as table_info. - */ - const val CREATED_FROM_DATABASE = 2 - - /** - * Reads the table information from the given database. - * - * @param database The database to read the information from. - * @param tableName The table name. - * @return A TableInfo containing the schema information for the provided table name. - */ - @JvmStatic - fun read(database: SupportSQLiteDatabase, tableName: String): TableInfo { - return readTableInfo( - database = database, - tableName = tableName - ) - } - } - - /** - * Holds the information about a database column. - */ - class Column( - /** - * The column name. - */ - @JvmField - val name: String, - /** - * The column type affinity. - */ - @JvmField - val type: String, - /** - * Whether or not the column can be NULL. - */ - @JvmField - val notNull: Boolean, - @JvmField - val primaryKeyPosition: Int, - @JvmField - val defaultValue: String?, - @CreatedFrom - @JvmField - val createdFrom: Int - ) { - /** - * The column type after it is normalized to one of the basic types according to - * https://www.sqlite.org/datatype3.html Section 3.1. - * - * - * This is the value Room uses for equality check. - */ - @SQLiteTypeAffinity - @JvmField - val affinity: Int = findAffinity(type) - - @Deprecated("Use {@link Column#Column(String, String, boolean, int, String, int)} instead.") - constructor(name: String, type: String, notNull: Boolean, primaryKeyPosition: Int) : this( - name, - type, - notNull, - primaryKeyPosition, - null, - CREATED_FROM_UNKNOWN - ) - - /** - * Implements https://www.sqlite.org/datatype3.html section 3.1 - * - * @param type The type that was given to the sqlite - * @return The normalized type which is one of the 5 known affinities - */ - @SQLiteTypeAffinity - private fun findAffinity(type: String?): Int { - if (type == null) { - return ColumnInfo.BLOB - } - val uppercaseType = type.uppercase(Locale.US) - if (uppercaseType.contains("INT")) { - return ColumnInfo.INTEGER - } - if (uppercaseType.contains("CHAR") || - uppercaseType.contains("CLOB") || - uppercaseType.contains("TEXT") - ) { - return ColumnInfo.TEXT - } - if (uppercaseType.contains("BLOB")) { - return ColumnInfo.BLOB - } - if (uppercaseType.contains("REAL") || - uppercaseType.contains("FLOA") || - uppercaseType.contains("DOUB") - ) { - return ColumnInfo.REAL - } - // sqlite returns NUMERIC here but it is like a catch all. We already - // have UNDEFINED so it is better to use UNDEFINED for consistency. - return ColumnInfo.UNDEFINED - } - - companion object { - /** - * Checks if the default values provided match. Handles the special case in which the - * default value is surrounded by parenthesis (e.g. encountered in b/182284899). - * - * Surrounding parenthesis are removed by SQLite when reading from the database, hence - * this function will check if they are present in the actual value, if so, it will - * compare the two values by ignoring the surrounding parenthesis. - * - */ - @VisibleForTesting - @JvmStatic - fun defaultValueEquals(current: String, other: String?): Boolean { - if (current == other) { - return true - } else if (containsSurroundingParenthesis(current)) { - return current.substring(1, current.length - 1).trim() == other - } - return false - } - - /** - * Checks for potential surrounding parenthesis, if found, removes them and checks if - * remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant, - * and returns true. - */ - private fun containsSurroundingParenthesis(current: String): Boolean { - if (current.isEmpty()) { - return false - } - var surroundingParenthesis = 0 - current.forEachIndexed { i, c -> - if (i == 0 && c != '(') { - return false - } - if (c == '(') { - surroundingParenthesis++ - } else if (c == ')') { - surroundingParenthesis-- - if (surroundingParenthesis == 0 && i != current.length - 1) { - return false - } - } - } - return surroundingParenthesis == 0 - } - } - - // TODO: problem probably here - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Column) return false - if (Build.VERSION.SDK_INT >= 20) { - if (primaryKeyPosition != other.primaryKeyPosition) return false - } else { - if (isPrimaryKey != other.isPrimaryKey) return false - } - if (name != other.name) return false - if (notNull != other.notNull) return false - // Only validate default value if it was defined in an entity, i.e. if the info - // from the compiler itself has it. b/136019383 - if ( - createdFrom == CREATED_FROM_ENTITY && - other.createdFrom == CREATED_FROM_DATABASE && - defaultValue != null && - !defaultValueEquals(defaultValue, other.defaultValue) - ) { - return false - } else if ( - createdFrom == CREATED_FROM_DATABASE && - other.createdFrom == CREATED_FROM_ENTITY && - other.defaultValue != null && - !defaultValueEquals(other.defaultValue, defaultValue) - ) { - return false - } else if ( - createdFrom != CREATED_FROM_UNKNOWN && - createdFrom == other.createdFrom && - (if (defaultValue != null) - !defaultValueEquals(defaultValue, other.defaultValue) - else other.defaultValue != null) - ) { - return false - } - return affinity == other.affinity - } - - /** - * Returns whether this column is part of the primary key or not. - * - * @return True if this column is part of the primary key, false otherwise. - */ - val isPrimaryKey: Boolean - get() = primaryKeyPosition > 0 - - override fun hashCode(): Int { - var result = name.hashCode() - result = 31 * result + affinity - result = 31 * result + if (notNull) 1231 else 1237 - result = 31 * result + primaryKeyPosition - // Default value is not part of the hashcode since we conditionally check it for - // equality which would break the equals + hashcode contract. - // result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0); - return result - } - - override fun toString(): String { - return ("Column{name='$name', type='$type', affinity='$affinity', " + - "notNull=$notNull, primaryKeyPosition=$primaryKeyPosition, " + - "defaultValue='${defaultValue ?: "undefined"}'}") - } - } - - /** - * Holds the information about an SQLite foreign key - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - class ForeignKey( - @JvmField - val referenceTable: String, - @JvmField - val onDelete: String, - @JvmField - val onUpdate: String, - @JvmField - val columnNames: List, - @JvmField - val referenceColumnNames: List - ) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ForeignKey) return false - if (referenceTable != other.referenceTable) return false - if (onDelete != other.onDelete) return false - if (onUpdate != other.onUpdate) return false - return if (columnNames != other.columnNames) false else referenceColumnNames == - other.referenceColumnNames - } - - override fun hashCode(): Int { - var result = referenceTable.hashCode() - result = 31 * result + onDelete.hashCode() - result = 31 * result + onUpdate.hashCode() - result = 31 * result + columnNames.hashCode() - result = 31 * result + referenceColumnNames.hashCode() - return result - } - - override fun toString(): String { - return ("ForeignKey{referenceTable='$referenceTable', onDelete='$onDelete +', " + - "onUpdate='$onUpdate', columnNames=$columnNames, " + - "referenceColumnNames=$referenceColumnNames}") - } - } - - /** - * Temporary data holder for a foreign key row in the pragma result. We need this to ensure - * sorting in the generated foreign key object. - */ - internal class ForeignKeyWithSequence( - val id: Int, - val sequence: Int, - val from: String, - val to: String - ) : Comparable { - override fun compareTo(other: ForeignKeyWithSequence): Int { - val idCmp = id - other.id - return if (idCmp == 0) { - sequence - other.sequence - } else { - idCmp - } - } - } - - /** - * Holds the information about an SQLite index - * - */ - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - class Index( - @JvmField - val name: String, - @JvmField - val unique: Boolean, - @JvmField - val columns: List, - @JvmField - var orders: List - ) { - init { - orders = orders.ifEmpty { - List(columns.size) { androidx.room.Index.Order.ASC.name } - } - } - - companion object { - // should match the value in Index.kt - const val DEFAULT_PREFIX = "index_" - } - - @Deprecated("Use {@link #Index(String, boolean, List, List)}") - constructor(name: String, unique: Boolean, columns: List) : this( - name, - unique, - columns, - List(columns.size) { androidx.room.Index.Order.ASC.name } - ) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Index) return false - if (unique != other.unique) { - return false - } - if (columns != other.columns) { - return false - } - if (orders != other.orders) { - return false - } - return if (name.startsWith(DEFAULT_PREFIX)) { - other.name.startsWith(DEFAULT_PREFIX) - } else { - name == other.name - } - } - - override fun hashCode(): Int { - var result = if (name.startsWith(DEFAULT_PREFIX)) { - DEFAULT_PREFIX.hashCode() - } else { - name.hashCode() - } - result = 31 * result + if (unique) 1 else 0 - result = 31 * result + columns.hashCode() - result = 31 * result + orders.hashCode() - return result - } - - override fun toString(): String { - return ("Index{name='$name', unique=$unique, columns=$columns, orders=$orders'}") - } - } -} - -internal fun readTableInfo(database: SupportSQLiteDatabase, tableName: String): TableInfo { - val columns = readColumns(database, tableName) - val foreignKeys = readForeignKeys(database, tableName) - val indices = readIndices(database, tableName) - return TableInfo(tableName, columns, foreignKeys, indices) -} - -private fun readForeignKeys( - database: SupportSQLiteDatabase, - tableName: String -): Set { - // this seems to return everything in order but it is not documented so better be safe - database.query("PRAGMA foreign_key_list(`$tableName`)").useCursor { cursor -> - val idColumnIndex = cursor.getColumnIndex("id") - val seqColumnIndex = cursor.getColumnIndex("seq") - val tableColumnIndex = cursor.getColumnIndex("table") - val onDeleteColumnIndex = cursor.getColumnIndex("on_delete") - val onUpdateColumnIndex = cursor.getColumnIndex("on_update") - val ordered = readForeignKeyFieldMappings(cursor) - - // Reset cursor as readForeignKeyFieldMappings has moved it - cursor.moveToPosition(-1) - return buildSet { - while (cursor.moveToNext()) { - val seq = cursor.getInt(seqColumnIndex) - if (seq != 0) { - continue - } - val id = cursor.getInt(idColumnIndex) - val myColumns = mutableListOf() - val refColumns = mutableListOf() - - ordered.filter { - it.id == id - }.forEach { key -> - myColumns.add(key.from) - refColumns.add(key.to) - } - - add( - TableInfo.ForeignKey( - referenceTable = cursor.getString(tableColumnIndex), - onDelete = cursor.getString(onDeleteColumnIndex), - onUpdate = cursor.getString(onUpdateColumnIndex), - columnNames = myColumns, - referenceColumnNames = refColumns - ) - ) - } - } - } -} - -private fun readForeignKeyFieldMappings(cursor: Cursor): List { - val idColumnIndex = cursor.getColumnIndex("id") - val seqColumnIndex = cursor.getColumnIndex("seq") - val fromColumnIndex = cursor.getColumnIndex("from") - val toColumnIndex = cursor.getColumnIndex("to") - - return buildList { - while (cursor.moveToNext()) { - add( - TableInfo.ForeignKeyWithSequence( - id = cursor.getInt(idColumnIndex), - sequence = cursor.getInt(seqColumnIndex), - from = cursor.getString(fromColumnIndex), - to = cursor.getString(toColumnIndex) - ) - ) - } - }.sorted() -} - -private fun readColumns( - database: SupportSQLiteDatabase, - tableName: String -): Map { - database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor -> - if (cursor.columnCount <= 0) { - return emptyMap() - } - - val nameIndex = cursor.getColumnIndex("name") - val typeIndex = cursor.getColumnIndex("type") - val notNullIndex = cursor.getColumnIndex("notnull") - val pkIndex = cursor.getColumnIndex("pk") - val defaultValueIndex = cursor.getColumnIndex("dflt_value") - - return buildMap { - while (cursor.moveToNext()) { - val name = cursor.getString(nameIndex) - val type = cursor.getString(typeIndex) - val notNull = 0 != cursor.getInt(notNullIndex) - val primaryKeyPosition = cursor.getInt(pkIndex) - val defaultValue = cursor.getString(defaultValueIndex) - put( - key = name, - value = TableInfo.Column( - name = name, - type = type, - notNull = notNull, - primaryKeyPosition = primaryKeyPosition, - defaultValue = defaultValue, - createdFrom = TableInfo.CREATED_FROM_DATABASE - ) - ) - } - } - } -} - -/** - * @return null if we cannot read the indices due to older sqlite implementations. - */ -private fun readIndices(database: SupportSQLiteDatabase, tableName: String): Set? { - database.query("PRAGMA index_list(`$tableName`)").useCursor { cursor -> - val nameColumnIndex = cursor.getColumnIndex("name") - val originColumnIndex = cursor.getColumnIndex("origin") - val uniqueIndex = cursor.getColumnIndex("unique") - if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) { - // we cannot read them so better not validate any index. - return null - } - return buildSet { - while (cursor.moveToNext()) { - val origin = cursor.getString(originColumnIndex) - if ("c" != origin) { - // Ignore auto-created indices - continue - } - val name = cursor.getString(nameColumnIndex) - val unique = cursor.getInt(uniqueIndex) == 1 - // Read index but if we cannot read it properly so better not read it - val index = readIndex(database, name, unique) ?: return null - add(index) - } - } - } -} - -/** - * @return null if we cannot read the index due to older sqlite implementations. - */ -private fun readIndex( - database: SupportSQLiteDatabase, - name: String, - unique: Boolean -): TableInfo.Index? { - return database.query("PRAGMA index_xinfo(`$name`)").useCursor { cursor -> - val seqnoColumnIndex = cursor.getColumnIndex("seqno") - val cidColumnIndex = cursor.getColumnIndex("cid") - val nameColumnIndex = cursor.getColumnIndex("name") - val descColumnIndex = cursor.getColumnIndex("desc") - if ( - seqnoColumnIndex == -1 || - cidColumnIndex == -1 || - nameColumnIndex == -1 || - descColumnIndex == -1 - ) { - // we cannot read them so better not validate any index. - return null - } - val columnsMap = TreeMap() - val ordersMap = TreeMap() - while (cursor.moveToNext()) { - val cid = cursor.getInt(cidColumnIndex) - if (cid < 0) { - // Ignore SQLite row ID - continue - } - val seq = cursor.getInt(seqnoColumnIndex) - val columnName = cursor.getString(nameColumnIndex) - val order = if (cursor.getInt(descColumnIndex) > 0) "DESC" else "ASC" - columnsMap[seq] = columnName - ordersMap[seq] = order - } - val columns = columnsMap.values.toList() - val orders = ordersMap.values.toList() - TableInfo.Index(name, unique, columns, orders) - } -} diff --git a/app/src/main/java/androidx/room/util/UUIDUtil.kt b/app/src/main/java/androidx/room/util/UUIDUtil.kt deleted file mode 100644 index cc5186574a..0000000000 --- a/app/src/main/java/androidx/room/util/UUIDUtil.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2021 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:JvmName("UUIDUtil") -@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - -package androidx.room.util - -import androidx.annotation.RestrictTo -import java.nio.ByteBuffer -import java.util.UUID - -/** - * UUID / byte[] two-way conversion utility for Room - * - */ - -/** - * Converts a 16-bytes array BLOB into a UUID pojo - * - * @param bytes byte array stored in database as BLOB - * @return a UUID object created based on the provided byte array - */ -fun convertByteToUUID(bytes: ByteArray): UUID { - val buffer = ByteBuffer.wrap(bytes) - val firstLong = buffer.long - val secondLong = buffer.long - return UUID(firstLong, secondLong) -} - -/** - * Converts a UUID pojo into a 16-bytes array to store into database as BLOB - * - * @param uuid the UUID pojo - * @return a byte array to store into database - */ -fun convertUUIDToByte(uuid: UUID): ByteArray { - val bytes = ByteArray(16) - val buffer = ByteBuffer.wrap(bytes) - buffer.putLong(uuid.mostSignificantBits) - buffer.putLong(uuid.leastSignificantBits) - return buffer.array() -} diff --git a/app/src/main/java/androidx/room/util/ViewInfo.kt b/app/src/main/java/androidx/room/util/ViewInfo.kt deleted file mode 100644 index f07a80a258..0000000000 --- a/app/src/main/java/androidx/room/util/ViewInfo.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2018 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 androidx.room.util - -import androidx.annotation.RestrictTo -import androidx.sqlite.db.SupportSQLiteDatabase - -/** - * A data class that holds the information about a view. - * - * - * This derives information from sqlite_master. - * - * - * Even though SQLite column names are case insensitive, this class uses case sensitive matching. - * - */ -@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -class ViewInfo( - /** - * The view name - */ - @JvmField - val name: String, - /** - * The SQL of CREATE VIEW. - */ - @JvmField - val sql: String? -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ViewInfo) return false - return ((name == other.name) && if (sql != null) sql == other.sql else other.sql == null) - } - - override fun hashCode(): Int { - var result = name.hashCode() - result = 31 * result + (sql?.hashCode() ?: 0) - return result - } - - override fun toString(): String { - return ("ViewInfo{" + "name='" + name + '\'' + ", sql='" + sql + '\'' + '}') - } - - companion object { - /** - * Reads the view information from the given database. - * - * @param database The database to read the information from. - * @param viewName The view name. - * @return A ViewInfo containing the schema information for the provided view name. - */ - @JvmStatic - fun read(database: SupportSQLiteDatabase, viewName: String): ViewInfo { - return database.query( - "SELECT name, sql FROM sqlite_master " + - "WHERE type = 'view' AND name = '$viewName'" - ).useCursor { cursor -> - if (cursor.moveToFirst()) { - ViewInfo(cursor.getString(0), cursor.getString(1)) - } else { - ViewInfo(viewName, null) - } - } - } - } -} diff --git a/app/src/main/java/eu/faircode/email/DB.java b/app/src/main/java/eu/faircode/email/DB.java index 8b4f89e4d1..38582ad31d 100644 --- a/app/src/main/java/eu/faircode/email/DB.java +++ b/app/src/main/java/eu/faircode/email/DB.java @@ -563,7 +563,7 @@ public abstract class DB extends RoomDatabase { if (BuildConfig.DEBUG && false) builder.setQueryCallback(new QueryCallback() { @Override - public void onQuery(@NonNull String sqlQuery, @NonNull List bindArgs) { + public void onQuery(@NonNull String sqlQuery, @NonNull List bindArgs) { Log.i("query=" + sqlQuery); } }, Helper.getParallelExecutor());