From 58a63a366de1fce372b918782e258e612293c0ec Mon Sep 17 00:00:00 2001 From: M66B <259573+M66B@users.noreply.github.com> Date: Wed, 1 May 2024 21:19:38 +0200 Subject: [PATCH] Updated room --- 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 + 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.java => DelegatingOpenHelper.kt} | 17 +- app/src/main/java/androidx/room/DeleteMe.kt | 17 - ....java => EntityDeletionOrUpdateAdapter.kt} | 87 +- .../androidx/room/EntityInsertionAdapter.java | 251 --- .../androidx/room/EntityInsertionAdapter.kt | 228 +++ .../androidx/room/EntityUpsertionAdapter.kt | 223 +++ ...talRoomApi.java => ExperimentalRoomApi.kt} | 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 +++++++++ .../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.java => TransactionExecutor.kt} | 52 +- app/src/main/java/androidx/room/UUIDUtil.java | 64 - app/src/main/java/androidx/room/ViewInfo.java | 97 - .../AutoMigrationSpec.kt} | 15 +- .../Migration.kt} | 55 +- .../{ => paging}/LimitOffsetDataSource.java | 9 +- .../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, 7907 insertions(+), 9104 deletions(-) delete mode 100644 app/src/main/java/androidx/room/AutoCloser.java create mode 100644 app/src/main/java/androidx/room/AutoCloser.kt delete mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java create mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt delete mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java create mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.kt delete mode 100644 app/src/main/java/androidx/room/CopyLock.java delete mode 100644 app/src/main/java/androidx/room/CursorUtil.java delete mode 100644 app/src/main/java/androidx/room/DBUtil.java delete mode 100644 app/src/main/java/androidx/room/DatabaseConfiguration.java create mode 100644 app/src/main/java/androidx/room/DatabaseConfiguration.kt rename app/src/main/java/androidx/room/{DelegatingOpenHelper.java => DelegatingOpenHelper.kt} (65%) delete mode 100644 app/src/main/java/androidx/room/DeleteMe.kt rename app/src/main/java/androidx/room/{EntityDeletionOrUpdateAdapter.java => EntityDeletionOrUpdateAdapter.kt} (53%) delete mode 100644 app/src/main/java/androidx/room/EntityInsertionAdapter.java create mode 100644 app/src/main/java/androidx/room/EntityInsertionAdapter.kt create mode 100644 app/src/main/java/androidx/room/EntityUpsertionAdapter.kt rename app/src/main/java/androidx/room/{ExperimentalRoomApi.java => ExperimentalRoomApi.kt} (73%) delete mode 100644 app/src/main/java/androidx/room/FileUtil.java delete mode 100644 app/src/main/java/androidx/room/FtsTableInfo.java delete mode 100644 app/src/main/java/androidx/room/InvalidationLiveDataContainer.java create mode 100644 app/src/main/java/androidx/room/InvalidationLiveDataContainer.kt delete mode 100644 app/src/main/java/androidx/room/InvalidationTracker.java create mode 100644 app/src/main/java/androidx/room/InvalidationTracker.kt delete mode 100644 app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java create mode 100644 app/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt delete mode 100644 app/src/main/java/androidx/room/MultiInstanceInvalidationService.java create mode 100644 app/src/main/java/androidx/room/MultiInstanceInvalidationService.kt delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorDatabase.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorDatabase.kt delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.kt delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorProgram.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorProgram.kt delete mode 100644 app/src/main/java/androidx/room/QueryInterceptorStatement.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorStatement.kt delete mode 100644 app/src/main/java/androidx/room/Room.java create mode 100644 app/src/main/java/androidx/room/Room.kt delete mode 100644 app/src/main/java/androidx/room/RoomDatabase.java create mode 100644 app/src/main/java/androidx/room/RoomDatabase.kt delete mode 100644 app/src/main/java/androidx/room/RoomOpenHelper.java create mode 100644 app/src/main/java/androidx/room/RoomOpenHelper.kt delete mode 100644 app/src/main/java/androidx/room/RoomSQLiteQuery.java create mode 100644 app/src/main/java/androidx/room/RoomSQLiteQuery.kt delete mode 100644 app/src/main/java/androidx/room/RoomTrackingLiveData.java create mode 100644 app/src/main/java/androidx/room/RoomTrackingLiveData.kt delete mode 100644 app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java create mode 100644 app/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt delete mode 100644 app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java create mode 100644 app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.kt delete mode 100644 app/src/main/java/androidx/room/SharedSQLiteStatement.java create mode 100644 app/src/main/java/androidx/room/SharedSQLiteStatement.kt delete mode 100644 app/src/main/java/androidx/room/SneakyThrow.java delete mode 100644 app/src/main/java/androidx/room/StringUtil.java delete mode 100644 app/src/main/java/androidx/room/TableInfo.java rename app/src/main/java/androidx/room/{TransactionExecutor.java => TransactionExecutor.kt} (53%) delete mode 100644 app/src/main/java/androidx/room/UUIDUtil.java delete mode 100644 app/src/main/java/androidx/room/ViewInfo.java rename app/src/main/java/androidx/room/{AutoMigrationSpec.java => migration/AutoMigrationSpec.kt} (77%) rename app/src/main/java/androidx/room/{Migration.java => migration/Migration.kt} (56%) rename app/src/main/java/androidx/room/{ => paging}/LimitOffsetDataSource.java (98%) create mode 100644 app/src/main/java/androidx/room/util/CursorUtil.kt create mode 100644 app/src/main/java/androidx/room/util/DBUtil.kt create mode 100644 app/src/main/java/androidx/room/util/FileUtil.kt create mode 100644 app/src/main/java/androidx/room/util/FtsTableInfo.kt create mode 100644 app/src/main/java/androidx/room/util/RelationUtil.kt create mode 100644 app/src/main/java/androidx/room/util/StringUtil.kt create mode 100644 app/src/main/java/androidx/room/util/TableInfo.kt create mode 100644 app/src/main/java/androidx/room/util/UUIDUtil.kt create mode 100644 app/src/main/java/androidx/room/util/ViewInfo.kt diff --git a/app/build.gradle b/app/build.gradle index 893b6b6e5b..21a1ac7cc0 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.4.3" + details.useVersion "2.6.1" } 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.4.3" // 2.5.2/2.6.1/2.7.0-alpha01 + def room_version = "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 deleted file mode 100644 index ba157ec69a..0000000000 --- a/app/src/main/java/androidx/room/AutoCloser.java +++ /dev/null @@ -1,311 +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 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 new file mode 100644 index 0000000000..885f035cd0 --- /dev/null +++ b/app/src/main/java/androidx/room/AutoCloser.kt @@ -0,0 +1,227 @@ +/* + * 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 deleted file mode 100644 index 548720d140..0000000000 --- a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java +++ /dev/null @@ -1,878 +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.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 new file mode 100644 index 0000000000..387fd5c2aa --- /dev/null +++ b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt @@ -0,0 +1,570 @@ +/* + * 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 deleted file mode 100644 index 004f60aa35..0000000000 --- a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java +++ /dev/null @@ -1,48 +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.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 new file mode 100644 index 0000000000..59ee28606a --- /dev/null +++ b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.kt @@ -0,0 +1,35 @@ +/* + * 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/CopyLock.java b/app/src/main/java/androidx/room/CopyLock.java deleted file mode 100644 index 8db020c932..0000000000 --- a/app/src/main/java/androidx/room/CopyLock.java +++ /dev/null @@ -1,112 +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.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 deleted file mode 100644 index 09da6e2bb9..0000000000 --- a/app/src/main/java/androidx/room/CursorUtil.java +++ /dev/null @@ -1,167 +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. - */ - -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 deleted file mode 100644 index 8f876ae8ee..0000000000 --- a/app/src/main/java/androidx/room/DBUtil.java +++ /dev/null @@ -1,243 +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. - */ - -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 deleted file mode 100644 index 01e96bc152..0000000000 --- a/app/src/main/java/androidx/room/DatabaseConfiguration.java +++ /dev/null @@ -1,629 +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.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 new file mode 100644 index 0000000000..0d4aa4fc17 --- /dev/null +++ b/app/src/main/java/androidx/room/DatabaseConfiguration.kt @@ -0,0 +1,680 @@ +/* + * 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.java b/app/src/main/java/androidx/room/DelegatingOpenHelper.kt similarity index 65% rename from app/src/main/java/androidx/room/DelegatingOpenHelper.java rename to app/src/main/java/androidx/room/DelegatingOpenHelper.kt index 7c84fc5487..b3eac5e5c7 100644 --- a/app/src/main/java/androidx/room/DelegatingOpenHelper.java +++ b/app/src/main/java/androidx/room/DelegatingOpenHelper.kt @@ -13,24 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.room; +package androidx.room -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteOpenHelper; +import androidx.sqlite.db.SupportSQLiteOpenHelper /** - * Package private interface for OpenHelpers which delegate to other open helpers. + * Internal interface for OpenHelpers which delegate to other open helpers. * * TODO(b/175612939): delete this interface once implementations are merged. */ -interface DelegatingOpenHelper { - +internal interface DelegatingOpenHelper { /** - * Returns the delegate open helper (which may itself be a DelegatingOpenHelper) so + * The delegate open helper (which may itself be a DelegatingOpenHelper) so * configurations on specific instances can be applied. - * - * @return the delegate */ - @NonNull - SupportSQLiteOpenHelper getDelegate(); + val delegate: SupportSQLiteOpenHelper } diff --git a/app/src/main/java/androidx/room/DeleteMe.kt b/app/src/main/java/androidx/room/DeleteMe.kt deleted file mode 100644 index 4474168c82..0000000000 --- a/app/src/main/java/androidx/room/DeleteMe.kt +++ /dev/null @@ -1,17 +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. - */ - -// 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.java b/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt similarity index 53% rename from app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java rename to app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt index 154103cfcb..9bfd1d2d82 100644 --- a/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java +++ b/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.kt @@ -13,49 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package androidx.room -package androidx.room; - -import androidx.annotation.RestrictTo; -import androidx.sqlite.db.SupportSQLiteStatement; +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. * - * @param The type parameter of the entity to be deleted - * @hide - */ + * @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 +*/ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -@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); - } - +abstract class EntityDeletionOrUpdateAdapter ( + database: RoomDatabase +) : SharedSQLiteStatement(database) { /** * Create the deletion or update query * * @return An SQL query that can delete or update instances of T. */ - @Override - protected abstract String createQuery(); + abstract override fun createQuery(): String /** * 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 void bind(SupportSQLiteStatement statement, T entity); + protected abstract fun bind(statement: SupportSQLiteStatement, entity: T) /** * Deletes or updates the given entities in the database and returns the affected row count. @@ -63,13 +54,13 @@ public abstract class EntityDeletionOrUpdateAdapter extends SharedSQLiteState * @param entity The entity to delete or update * @return The number of affected rows */ - public final int handle(T entity) { - final SupportSQLiteStatement stmt = acquire(); - try { - bind(stmt, entity); - return stmt.executeUpdateDelete(); + fun handle(entity: T): Int { + val stmt: SupportSQLiteStatement = acquire() + return try { + bind(stmt, entity) + stmt.executeUpdateDelete() } finally { - release(stmt); + release(stmt) } } @@ -79,17 +70,17 @@ public abstract class EntityDeletionOrUpdateAdapter extends SharedSQLiteState * @param entities Entities to delete or update * @return The number of affected rows */ - public final int handleMultiple(Iterable entities) { - final SupportSQLiteStatement stmt = acquire(); - try { - int total = 0; - for (T entity : entities) { - bind(stmt, entity); - total += stmt.executeUpdateDelete(); + fun handleMultiple(entities: Iterable): Int { + val stmt: SupportSQLiteStatement = acquire() + return try { + var total = 0 + entities.forEach { entity -> + bind(stmt, entity) + total += stmt.executeUpdateDelete() } - return total; + total } finally { - release(stmt); + release(stmt) } } @@ -99,17 +90,17 @@ public abstract class EntityDeletionOrUpdateAdapter extends SharedSQLiteState * @param entities Entities to delete or update * @return The number of affected rows */ - public final int handleMultiple(T[] entities) { - final SupportSQLiteStatement stmt = acquire(); - try { - int total = 0; - for (T entity : entities) { - bind(stmt, entity); - total += stmt.executeUpdateDelete(); + fun handleMultiple(entities: Array): Int { + val stmt: SupportSQLiteStatement = acquire() + return try { + var total = 0 + entities.forEach { entity -> + bind(stmt, entity) + total += stmt.executeUpdateDelete() } - return total; + 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 deleted file mode 100644 index 3046d6cd0b..0000000000 --- a/app/src/main/java/androidx/room/EntityInsertionAdapter.java +++ /dev/null @@ -1,251 +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.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 new file mode 100644 index 0000000000..bee98b8330 --- /dev/null +++ b/app/src/main/java/androidx/room/EntityInsertionAdapter.kt @@ -0,0 +1,228 @@ +/* + * 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 new file mode 100644 index 0000000000..db31e112f0 --- /dev/null +++ b/app/src/main/java/androidx/room/EntityUpsertionAdapter.kt @@ -0,0 +1,223 @@ +/* + * 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.java b/app/src/main/java/androidx/room/ExperimentalRoomApi.kt similarity index 73% rename from app/src/main/java/androidx/room/ExperimentalRoomApi.java rename to app/src/main/java/androidx/room/ExperimentalRoomApi.kt index 14896bfadb..20ed03b72f 100644 --- a/app/src/main/java/androidx/room/ExperimentalRoomApi.java +++ b/app/src/main/java/androidx/room/ExperimentalRoomApi.kt @@ -13,17 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package androidx.room; - -import androidx.annotation.RequiresOptIn; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Target; +package androidx.room +import androidx.annotation.RequiresOptIn /** * APIs marked with ExperimentalRoomApi are experimental and may change. */ -@Target({ElementType.TYPE, ElementType.METHOD}) -@RequiresOptIn() -public @interface ExperimentalRoomApi {} +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.FUNCTION +) +@Suppress("UnsafeOptInUsageError") +@RequiresOptIn +@Retention(AnnotationRetention.BINARY) +annotation class ExperimentalRoomApi diff --git a/app/src/main/java/androidx/room/FileUtil.java b/app/src/main/java/androidx/room/FileUtil.java deleted file mode 100644 index a221ff741a..0000000000 --- a/app/src/main/java/androidx/room/FileUtil.java +++ /dev/null @@ -1,71 +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.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 deleted file mode 100644 index aa7305f0ca..0000000000 --- a/app/src/main/java/androidx/room/FtsTableInfo.java +++ /dev/null @@ -1,220 +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 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 deleted file mode 100644 index e1e8155f8a..0000000000 --- a/app/src/main/java/androidx/room/InvalidationLiveDataContainer.java +++ /dev/null @@ -1,59 +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.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 new file mode 100644 index 0000000000..de7ae79b5b --- /dev/null +++ b/app/src/main/java/androidx/room/InvalidationLiveDataContainer.kt @@ -0,0 +1,55 @@ +/* + * 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 deleted file mode 100644 index 99e69154b1..0000000000 --- a/app/src/main/java/androidx/room/InvalidationTracker.java +++ /dev/null @@ -1,902 +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.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 new file mode 100644 index 0000000000..38067b702f --- /dev/null +++ b/app/src/main/java/androidx/room/InvalidationTracker.kt @@ -0,0 +1,839 @@ +/* + * 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/MultiInstanceInvalidationClient.java b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java deleted file mode 100644 index cd847acc89..0000000000 --- a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java +++ /dev/null @@ -1,196 +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.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 new file mode 100644 index 0000000000..fce2ec7676 --- /dev/null +++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.kt @@ -0,0 +1,129 @@ +/* + * 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 deleted file mode 100644 index 0c82f93fa0..0000000000 --- a/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java +++ /dev/null @@ -1,137 +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.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 new file mode 100644 index 0000000000..3bac1ea5eb --- /dev/null +++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationService.kt @@ -0,0 +1,122 @@ +/* + * 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 deleted file mode 100644 index 386b583587..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorDatabase.java +++ /dev/null @@ -1,312 +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.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 new file mode 100644 index 0000000000..0f1ec17328 --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorDatabase.kt @@ -0,0 +1,145 @@ +/* + * 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 deleted file mode 100644 index 9916294cdf..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java +++ /dev/null @@ -1,78 +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.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 new file mode 100644 index 0000000000..c15672f810 --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt @@ -0,0 +1,41 @@ +/* + * 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 deleted file mode 100644 index 5d94cd10ac..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java +++ /dev/null @@ -1,50 +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.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 new file mode 100644 index 0000000000..1d86465e0a --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.kt @@ -0,0 +1,39 @@ +/* + * 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 deleted file mode 100644 index 2b9c554420..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorProgram.java +++ /dev/null @@ -1,82 +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; - -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 new file mode 100644 index 0000000000..49aea347ba --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorProgram.kt @@ -0,0 +1,63 @@ +/* + * 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 deleted file mode 100644 index 8825252e5d..0000000000 --- a/app/src/main/java/androidx/room/QueryInterceptorStatement.java +++ /dev/null @@ -1,128 +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.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 new file mode 100644 index 0000000000..37eca347a8 --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorStatement.kt @@ -0,0 +1,109 @@ +/* + * 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 deleted file mode 100644 index d2520998d1..0000000000 --- a/app/src/main/java/androidx/room/Room.java +++ /dev/null @@ -1,115 +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.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 new file mode 100644 index 0000000000..d0cb58543e --- /dev/null +++ b/app/src/main/java/androidx/room/Room.kt @@ -0,0 +1,116 @@ +/* + * 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 deleted file mode 100644 index 54427d6eea..0000000000 --- a/app/src/main/java/androidx/room/RoomDatabase.java +++ /dev/null @@ -1,1678 +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.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 new file mode 100644 index 0000000000..bc86569549 --- /dev/null +++ b/app/src/main/java/androidx/room/RoomDatabase.kt @@ -0,0 +1,1547 @@ +/* + * 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 deleted file mode 100644 index 3dbe7ddeac..0000000000 --- a/app/src/main/java/androidx/room/RoomOpenHelper.java +++ /dev/null @@ -1,277 +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.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 new file mode 100644 index 0000000000..879f98d79c --- /dev/null +++ b/app/src/main/java/androidx/room/RoomOpenHelper.kt @@ -0,0 +1,244 @@ +/* + * 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 deleted file mode 100644 index 689352c032..0000000000 --- a/app/src/main/java/androidx/room/RoomSQLiteQuery.java +++ /dev/null @@ -1,299 +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.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 new file mode 100644 index 0000000000..9a2e832b0f --- /dev/null +++ b/app/src/main/java/androidx/room/RoomSQLiteQuery.kt @@ -0,0 +1,233 @@ +/* + * 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 deleted file mode 100644 index 1e3daa74ea..0000000000 --- a/app/src/main/java/androidx/room/RoomTrackingLiveData.java +++ /dev/null @@ -1,173 +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.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 new file mode 100644 index 0000000000..171b57d16e --- /dev/null +++ b/app/src/main/java/androidx/room/RoomTrackingLiveData.kt @@ -0,0 +1,126 @@ +/* + * 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 deleted file mode 100644 index 300145cde3..0000000000 --- a/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java +++ /dev/null @@ -1,289 +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.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 new file mode 100644 index 0000000000..3672ab0225 --- /dev/null +++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt @@ -0,0 +1,242 @@ +/* + * 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 deleted file mode 100644 index 84eebe4b72..0000000000 --- a/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java +++ /dev/null @@ -1,64 +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.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 new file mode 100644 index 0000000000..9ecb35bdc5 --- /dev/null +++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.kt @@ -0,0 +1,45 @@ +/* + * 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 deleted file mode 100644 index 20c06c8b50..0000000000 --- a/app/src/main/java/androidx/room/SharedSQLiteStatement.java +++ /dev/null @@ -1,100 +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, {@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 new file mode 100644 index 0000000000..710426d5b8 --- /dev/null +++ b/app/src/main/java/androidx/room/SharedSQLiteStatement.kt @@ -0,0 +1,87 @@ +/* + * 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 deleted file mode 100644 index 76968ab344..0000000000 --- a/app/src/main/java/androidx/room/SneakyThrow.java +++ /dev/null @@ -1,47 +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.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 deleted file mode 100644 index 2b30eb64de..0000000000 --- a/app/src/main/java/androidx/room/StringUtil.java +++ /dev/null @@ -1,118 +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.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 deleted file mode 100644 index 2d3c8be329..0000000000 --- a/app/src/main/java/androidx/room/TableInfo.java +++ /dev/null @@ -1,743 +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.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.java b/app/src/main/java/androidx/room/TransactionExecutor.kt similarity index 53% rename from app/src/main/java/androidx/room/TransactionExecutor.java rename to app/src/main/java/androidx/room/TransactionExecutor.kt index 53648d377b..93ba86d78d 100644 --- a/app/src/main/java/androidx/room/TransactionExecutor.java +++ b/app/src/main/java/androidx/room/TransactionExecutor.kt @@ -13,52 +13,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package androidx.room -package androidx.room; - -import androidx.annotation.NonNull; - -import java.util.ArrayDeque; -import java.util.concurrent.Executor; +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. */ -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() { +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 { try { - command.run(); + command.run() } finally { - scheduleNext(); + scheduleNext() } + }) + if (active == null) { + scheduleNext() } - }); - if (mActive == null) { - scheduleNext(); } } - @SuppressWarnings("WeakerAccess") - synchronized void scheduleNext() { - if ((mActive = mTasks.poll()) != null) { - mExecutor.execute(mActive); + fun scheduleNext() { + synchronized(syncLock) { + if (tasks.poll().also { active = it } != null) { + executor.execute(active) + } } } } diff --git a/app/src/main/java/androidx/room/UUIDUtil.java b/app/src/main/java/androidx/room/UUIDUtil.java deleted file mode 100644 index 275a95ca76..0000000000 --- a/app/src/main/java/androidx/room/UUIDUtil.java +++ /dev/null @@ -1,64 +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. - */ - -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 deleted file mode 100644 index 81e6aa901c..0000000000 --- a/app/src/main/java/androidx/room/ViewInfo.java +++ /dev/null @@ -1,97 +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 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/AutoMigrationSpec.java b/app/src/main/java/androidx/room/migration/AutoMigrationSpec.kt similarity index 77% rename from app/src/main/java/androidx/room/AutoMigrationSpec.java rename to app/src/main/java/androidx/room/migration/AutoMigrationSpec.kt index 1eea1897a8..99b2ea0e2d 100644 --- a/app/src/main/java/androidx/room/AutoMigrationSpec.java +++ b/app/src/main/java/androidx/room/migration/AutoMigrationSpec.kt @@ -13,26 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package androidx.room.migration -package androidx.room.migration; -import androidx.annotation.NonNull; -import androidx.room.AutoMigration; -import androidx.sqlite.db.SupportSQLiteDatabase; +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. * - * @see AutoMigration + * For details, see [androidx.room.AutoMigration] */ -public interface AutoMigrationSpec { - +interface AutoMigrationSpec { /** * Invoked after the migration is completed. * @param db The SQLite database. */ - default void onPostMigrate(@NonNull SupportSQLiteDatabase db) {} + fun onPostMigrate(db: SupportSQLiteDatabase) {} } diff --git a/app/src/main/java/androidx/room/Migration.java b/app/src/main/java/androidx/room/migration/Migration.kt similarity index 56% rename from app/src/main/java/androidx/room/Migration.java rename to app/src/main/java/androidx/room/migration/Migration.kt index 4aa7a7e86a..63b20d2241 100644 --- a/app/src/main/java/androidx/room/Migration.java +++ b/app/src/main/java/androidx/room/migration/Migration.kt @@ -13,51 +13,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package androidx.room.migration -package androidx.room.migration; - -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteDatabase /** * Base class for a database migration. - *

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

    + * + * Creates a new migration between [startVersion] and [endVersion]. + * + * Each migration can move between 2 versions that are defined by [startVersion] and + * [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. */ -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; - } - +abstract class Migration( + @JvmField + val startVersion: Int, + @JvmField + val endVersion: Int +) { /** * Should run the necessary migrations. - *

    - * 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 {@code Migration}s. * - * @param database The database instance + * The Migration 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. + * + * @param db The database instance */ - public abstract void migrate(@NonNull SupportSQLiteDatabase database); + abstract fun migrate(db: SupportSQLiteDatabase) } diff --git a/app/src/main/java/androidx/room/LimitOffsetDataSource.java b/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java similarity index 98% rename from app/src/main/java/androidx/room/LimitOffsetDataSource.java rename to app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java index d3513473af..2b5c391dbc 100644 --- a/app/src/main/java/androidx/room/LimitOffsetDataSource.java +++ b/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java @@ -46,7 +46,6 @@ import java.util.concurrent.atomic.AtomicBoolean; * * @param Data type returned by the data source. * - * @hide */ @SuppressWarnings("deprecation") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @@ -117,8 +116,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(); @@ -154,7 +153,7 @@ public abstract class LimitOffsetDataSource extends androidx.paging.Positiona @NonNull LoadInitialCallback callback) { registerObserverIfNecessary(); List list = Collections.emptyList(); - int totalCount = 0; + int totalCount; int firstLoadPosition = 0; RoomSQLiteQuery sqLiteQuery = null; Cursor cursor = null; @@ -172,8 +171,6 @@ 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(); @@ -196,8 +193,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/util/CursorUtil.kt b/app/src/main/java/androidx/room/util/CursorUtil.kt new file mode 100644 index 0000000000..6e94283440 --- /dev/null +++ b/app/src/main/java/androidx/room/util/CursorUtil.kt @@ -0,0 +1,183 @@ +/* + * 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 new file mode 100644 index 0000000000..99a5d96b48 --- /dev/null +++ b/app/src/main/java/androidx/room/util/DBUtil.kt @@ -0,0 +1,213 @@ +/* + * 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 new file mode 100644 index 0000000000..c1f843b7b8 --- /dev/null +++ b/app/src/main/java/androidx/room/util/FileUtil.kt @@ -0,0 +1,58 @@ +/* + * 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 new file mode 100644 index 0000000000..b533798cdd --- /dev/null +++ b/app/src/main/java/androidx/room/util/FtsTableInfo.kt @@ -0,0 +1,181 @@ +/* + * 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 new file mode 100644 index 0000000000..cefa581102 --- /dev/null +++ b/app/src/main/java/androidx/room/util/RelationUtil.kt @@ -0,0 +1,149 @@ +/* + * 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 new file mode 100644 index 0000000000..f8565fad97 --- /dev/null +++ b/app/src/main/java/androidx/room/util/StringUtil.kt @@ -0,0 +1,85 @@ +/* + * 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 new file mode 100644 index 0000000000..7cded96ab1 --- /dev/null +++ b/app/src/main/java/androidx/room/util/TableInfo.kt @@ -0,0 +1,645 @@ +/* + * 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 new file mode 100644 index 0000000000..cc5186574a --- /dev/null +++ b/app/src/main/java/androidx/room/util/UUIDUtil.kt @@ -0,0 +1,55 @@ +/* + * 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 new file mode 100644 index 0000000000..f07a80a258 --- /dev/null +++ b/app/src/main/java/androidx/room/util/ViewInfo.kt @@ -0,0 +1,82 @@ +/* + * 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 38582ad31d..8b4f89e4d1 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());