From 70a2af170f3396b9362f224091f97cb3620daea4 Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 31 Jan 2021 18:26:44 +0100 Subject: [PATCH] Updated ROOM --- app/build.gradle | 4 +- .../main/java/androidx/room/AutoCloser.java | 311 +++++++ .../room/AutoClosingRoomOpenHelper.java | 875 ++++++++++++++++++ .../AutoClosingRoomOpenHelperFactory.java | 48 + .../androidx/room/DatabaseConfiguration.java | 199 +++- .../androidx/room/DelegatingOpenHelper.java | 36 + .../androidx/room/ExperimentalRoomApi.java | 29 + .../androidx/room/InvalidationTracker.java | 75 +- .../room/MultiInstanceInvalidationClient.java | 27 +- .../room/QueryInterceptorDatabase.java | 302 ++++++ .../room/QueryInterceptorOpenHelper.java | 78 ++ .../QueryInterceptorOpenHelperFactory.java | 50 + .../room/QueryInterceptorProgram.java | 82 ++ .../room/QueryInterceptorStatement.java | 128 +++ app/src/main/java/androidx/room/Room.java | 7 +- .../main/java/androidx/room/RoomDatabase.java | 479 +++++++++- .../androidx/room/SQLiteCopyOpenHelper.java | 86 +- .../room/SQLiteCopyOpenHelperFactory.java | 8 + .../androidx/room/TransactionExecutor.java | 2 + .../main/java/androidx/room/package-info.java | 6 +- .../java/androidx/room/util/CopyLock.java | 2 +- .../java/androidx/room/util/CursorUtil.java | 62 +- .../java/androidx/room/util/FtsTableInfo.java | 4 +- .../java/androidx/room/util/TableInfo.java | 18 +- .../java/androidx/room/util/ViewInfo.java | 4 +- 25 files changed, 2848 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/androidx/room/AutoCloser.java create mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java create mode 100644 app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java create mode 100644 app/src/main/java/androidx/room/DelegatingOpenHelper.java create mode 100644 app/src/main/java/androidx/room/ExperimentalRoomApi.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorDatabase.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorProgram.java create mode 100644 app/src/main/java/androidx/room/QueryInterceptorStatement.java diff --git a/app/build.gradle b/app/build.gradle index 66318fa2b4..fb51ef5d7c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -235,6 +235,7 @@ configurations.all { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + def annotation_version = "1.1.0-beta01" def core_version = "1.5.0-alpha05" def appcompat_version = "1.3.0-beta01" def activity_version = "1.2.0-rc01" @@ -251,7 +252,7 @@ dependencies { def lifecycle_version = "2.3.0-rc01" def lifecycle_extensions_version = "2.2.0" def sqlite_version = "2.1.0" - def room_version = "2.2.6" // 2.3.0-beta01 + def room_version = "2.3.0-beta01" def paging_version = "2.1.2" // 3.0.0-alpha11 def preference_version = "1.1.1" def work_version = "2.5.0" @@ -278,6 +279,7 @@ dependencies { def apache_poi = "3.17" // https://developer.android.com/jetpack/androidx/releases/ + implementation "androidx.annotation:annotation-experimental:$annotation_version" // https://mvnrepository.com/artifact/androidx.core/core implementation "androidx.core:core:$core_version" diff --git a/app/src/main/java/androidx/room/AutoCloser.java b/app/src/main/java/androidx/room/AutoCloser.java new file mode 100644 index 0000000000..ba157ec69a --- /dev/null +++ b/app/src/main/java/androidx/room/AutoCloser.java @@ -0,0 +1,311 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.room; + +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.arch.core.util.Function; +import androidx.room.util.SneakyThrow; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * AutoCloser is responsible for automatically opening (using + * delegateOpenHelper) and closing (on a timer started when there are no remaining references) a + * SupportSqliteDatabase. + * + * It is important to ensure that the ref count is incremented when using a returned database. + */ +final class AutoCloser { + + @Nullable + private SupportSQLiteOpenHelper mDelegateOpenHelper = null; + + @NonNull + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + // Package private for access from mAutoCloser + @Nullable + Runnable mOnAutoCloseCallback = null; + + // Package private for access from mAutoCloser + @NonNull + final Object mLock = new Object(); + + // Package private for access from mAutoCloser + final long mAutoCloseTimeoutInMs; + + // Package private for access from mExecuteAutoCloser + @NonNull + final Executor mExecutor; + + // Package private for access from mAutoCloser + @GuardedBy("mLock") + int mRefCount = 0; + + // Package private for access from mAutoCloser + @GuardedBy("mLock") + long mLastDecrementRefCountTimeStamp = SystemClock.uptimeMillis(); + + // The unwrapped SupportSqliteDatabase + // Package private for access from mAutoCloser + @GuardedBy("mLock") + @Nullable + SupportSQLiteDatabase mDelegateDatabase; + + private boolean mManuallyClosed = false; + + private final Runnable mExecuteAutoCloser = new Runnable() { + @Override + public void run() { + mExecutor.execute(mAutoCloser); + } + }; + + // Package private for access from mExecuteAutoCloser + @NonNull + final Runnable mAutoCloser = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + if (SystemClock.uptimeMillis() - mLastDecrementRefCountTimeStamp + < mAutoCloseTimeoutInMs) { + // An increment + decrement beat us to closing the db. We + // will not close the database, and there should be at least + // one more auto-close scheduled. + return; + } + + if (mRefCount != 0) { + // An increment beat us to closing the db. We don't close the + // db, and another closer will be scheduled once the ref + // count is decremented. + return; + } + + if (mOnAutoCloseCallback != null) { + mOnAutoCloseCallback.run(); + } else { + throw new IllegalStateException("mOnAutoCloseCallback is null but it should" + + " have been set before use. Please file a bug " + + "against Room at: https://issuetracker.google" + + ".com/issues/new?component=413107&template=1096568"); + } + + if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) { + try { + mDelegateDatabase.close(); + } catch (IOException e) { + SneakyThrow.reThrow(e); + } + mDelegateDatabase = null; + } + } + } + }; + + + /** + * Construct an AutoCloser. + * + * @param autoCloseTimeoutAmount time for auto close timer + * @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount + * @param autoCloseExecutor the executor on which the auto close operation will happen + */ + AutoCloser(long autoCloseTimeoutAmount, + @NonNull TimeUnit autoCloseTimeUnit, + @NonNull Executor autoCloseExecutor) { + mAutoCloseTimeoutInMs = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount); + mExecutor = autoCloseExecutor; + } + + /** + * Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the + * delegateOpenHelper after construction. + * + * @param delegateOpenHelper the open helper that is used to create + * new SupportSqliteDatabases + */ + public void init(@NonNull SupportSQLiteOpenHelper delegateOpenHelper) { + if (mDelegateOpenHelper != null) { + Log.e(Room.LOG_TAG, "AutoCloser initialized multiple times. Please file a bug against" + + " room at: https://issuetracker.google" + + ".com/issues/new?component=413107&template=1096568"); + return; + } + this.mDelegateOpenHelper = delegateOpenHelper; + } + + /** + * Execute a ref counting function. The function will receive an unwrapped open database and + * this database will stay open until at least after function returns. If there are no more + * references in use for the db once function completes, an auto close operation will be + * scheduled. + */ + @Nullable + public V executeRefCountingFunction(@NonNull Function function) { + try { + SupportSQLiteDatabase db = incrementCountAndEnsureDbIsOpen(); + return function.apply(db); + } finally { + decrementCountAndScheduleClose(); + } + } + + /** + * Confirms that autoCloser is no longer running and confirms that mDelegateDatabase is set + * and open. mDelegateDatabase will not be auto closed until + * decrementRefCountAndScheduleClose is called. decrementRefCountAndScheduleClose must be + * called once for each call to incrementCountAndEnsureDbIsOpen. + * + * If this throws an exception, decrementCountAndScheduleClose must still be called! + * + * @return the *unwrapped* SupportSQLiteDatabase. + */ + @NonNull + public SupportSQLiteDatabase incrementCountAndEnsureDbIsOpen() { + //TODO(rohitsat): avoid synchronized(mLock) when possible. We should be able to avoid it + // when refCount is not hitting zero or if there is no auto close scheduled if we use + // Atomics. + synchronized (mLock) { + // If there is a scheduled autoclose operation, we should remove it from the handler. + mHandler.removeCallbacks(mExecuteAutoCloser); + + mRefCount++; + + if (mManuallyClosed) { + throw new IllegalStateException("Attempting to open already closed database."); + } + + if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) { + return mDelegateDatabase; + } + + // Get the database while holding `mLock` so no other threads try to create it or + // destroy it. + if (mDelegateOpenHelper != null) { + mDelegateDatabase = mDelegateOpenHelper.getWritableDatabase(); + } else { + throw new IllegalStateException("AutoCloser has not been initialized. Please file " + + "a bug against Room at: " + + "https://issuetracker.google.com/issues/new?component=413107&template=1096568"); + } + + return mDelegateDatabase; + } + } + + /** + * Decrements the ref count and schedules a close if there are no other references to the db. + * This must only be called after a corresponding incrementCountAndEnsureDbIsOpen call. + */ + public void decrementCountAndScheduleClose() { + //TODO(rohitsat): avoid synchronized(mLock) when possible + synchronized (mLock) { + if (mRefCount <= 0) { + throw new IllegalStateException("ref count is 0 or lower but we're supposed to " + + "decrement"); + } + + // decrement refCount + mRefCount--; + + // if refcount is zero, schedule close operation + if (mRefCount == 0) { + if (mDelegateDatabase == null) { + // No db to close, this can happen due to exceptions when creating db... + return; + } + mHandler.postDelayed(mExecuteAutoCloser, mAutoCloseTimeoutInMs); + } + } + } + + /** + * Returns the underlying database. This does not ensure that the database is open; the + * caller is responsible for ensuring that the database is open and the ref count is non-zero. + * + * This is primarily meant for use cases where we don't want to open the database (isOpen) or + * we know that the database is already open (KeepAliveCursor). + */ + @Nullable // Since the db might be closed + public SupportSQLiteDatabase getDelegateDatabase() { + synchronized (mLock) { + return mDelegateDatabase; + } + } + + /** + * Close the database if it is still active. + * + * @throws IOException if an exception is encountered when closing the underlying db. + */ + public void closeDatabaseIfOpen() throws IOException { + synchronized (mLock) { + mManuallyClosed = true; + + if (mDelegateDatabase != null) { + mDelegateDatabase.close(); + } + mDelegateDatabase = null; + } + } + + /** + * The auto closer is still active if the database has not been closed. This means that + * whether or not the underlying database is closed, when active we will re-open it on the + * next access. + * + * @return a boolean indicating whether the auto closer is still active + */ + public boolean isActive() { + return !mManuallyClosed; + } + + /** + * Returns the current ref count for this auto closer. This is only visible for testing. + * + * @return current ref count + */ + @VisibleForTesting + public int getRefCountForTest() { + synchronized (mLock) { + return mRefCount; + } + } + + /** + * Sets a callback that will be run every time the database is auto-closed. This callback + * needs to be lightweight since it is run while holding a lock. + * + * @param onAutoClose the callback to run + */ + public void setAutoCloseCallback(Runnable onAutoClose) { + mOnAutoCloseCallback = onAutoClose; + } +} diff --git a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java new file mode 100644 index 0000000000..b3a3853f3a --- /dev/null +++ b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java @@ -0,0 +1,875 @@ +/* + * 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.annotation.SuppressLint; +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.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; + }); + } + + @SuppressLint("UnsafeNewApiCall") + @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."); + } + + @SuppressLint("UnsafeNewApiCall") + @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(); + } + } + + /** + * 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); + } + + @SuppressLint("UnsafeNewApiCall") + @RequiresApi(api = Build.VERSION_CODES.Q) + @Override + public void setNotificationUris(@NonNull ContentResolver cr, + @NonNull List uris) { + mDelegate.setNotificationUris(cr, uris); + } + + @SuppressLint("UnsafeNewApiCall") + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Override + public Uri getNotificationUri() { + return mDelegate.getNotificationUri(); + } + + @SuppressLint("UnsafeNewApiCall") + @RequiresApi(api = Build.VERSION_CODES.Q) + @Nullable + @Override + public List getNotificationUris() { + return mDelegate.getNotificationUris(); + } + + @Override + public boolean getWantsAllOnMoveCalls() { + return mDelegate.getWantsAllOnMoveCalls(); + } + + @SuppressLint("UnsafeNewApiCall") + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void setExtras(Bundle extras) { + mDelegate.setExtras(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/AutoClosingRoomOpenHelperFactory.java b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java new file mode 100644 index 0000000000..004f60aa35 --- /dev/null +++ b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelperFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.room; + +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +/** + * Factory class for AutoClosingRoomOpenHelper + */ +final class AutoClosingRoomOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { + @NonNull + private final SupportSQLiteOpenHelper.Factory mDelegate; + + @NonNull + private final AutoCloser mAutoCloser; + + AutoClosingRoomOpenHelperFactory( + @NonNull SupportSQLiteOpenHelper.Factory factory, + @NonNull AutoCloser autoCloser) { + mDelegate = factory; + mAutoCloser = autoCloser; + } + + /** + * @return AutoClosingRoomOpenHelper instances. + */ + @Override + @NonNull + public AutoClosingRoomOpenHelper create( + @NonNull SupportSQLiteOpenHelper.Configuration configuration) { + return new AutoClosingRoomOpenHelper(mDelegate.create(configuration), mAutoCloser); + } +} diff --git a/app/src/main/java/androidx/room/DatabaseConfiguration.java b/app/src/main/java/androidx/room/DatabaseConfiguration.java index f994445016..d19c0cbeb2 100644 --- a/app/src/main/java/androidx/room/DatabaseConfiguration.java +++ b/app/src/main/java/androidx/room/DatabaseConfiguration.java @@ -16,6 +16,7 @@ package androidx.room; +import android.annotation.SuppressLint; import android.content.Context; import androidx.annotation.NonNull; @@ -24,8 +25,11 @@ import androidx.annotation.RestrictTo; 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; /** @@ -59,6 +63,12 @@ public class DatabaseConfiguration { @Nullable public final List callbacks; + @Nullable + public final RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback; + + @NonNull + public final List typeConverters; + /** * Whether Room should throw an exception for queries run on the main thread. */ @@ -116,13 +126,21 @@ public class DatabaseConfiguration { @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, boolean, boolean, boolean, Set, String, File)} + * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List)} * * @param context The application context. * @param name Name of the database, can be null if it is in memory. @@ -152,7 +170,7 @@ public class DatabaseConfiguration { @Nullable Set migrationNotRequiredFrom) { this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false, - requireMigration, false, migrationNotRequiredFrom, null, null); + requireMigration, false, migrationNotRequiredFrom, null, null, null, null, null); } /** @@ -160,7 +178,8 @@ public class DatabaseConfiguration { * * @deprecated Use {@link #DatabaseConfiguration(Context, String, * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, - * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File)} + * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, List)} * * @param context The application context. * @param name Name of the database, can be null if it is in memory. @@ -197,12 +216,17 @@ public class DatabaseConfiguration { this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks, allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor, multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade, - migrationNotRequiredFrom, null, null); + migrationNotRequiredFrom, 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, boolean, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, 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. @@ -223,6 +247,7 @@ public class DatabaseConfiguration { * * @hide */ + @Deprecated @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(@NonNull Context context, @Nullable String name, @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, @@ -238,6 +263,169 @@ public class DatabaseConfiguration { @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); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, 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); + } + + /** + * Creates a database configuration with the given values. + * + * @deprecated Use {@link #DatabaseConfiguration(Context, String, + * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean, + * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File, + * Callable, RoomDatabase.PrepackagedDatabaseCallback, 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); + } + + /** + * 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. + * + * @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, + 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.sqliteOpenHelperFactory = sqliteOpenHelperFactory; this.context = context; this.name = name; @@ -253,6 +441,9 @@ public class DatabaseConfiguration { this.mMigrationNotRequiredFrom = migrationNotRequiredFrom; this.copyFromAssetPath = copyFromAssetPath; this.copyFromFile = copyFromFile; + this.copyFromInputStream = copyFromInputStream; + this.prepackagedDatabaseCallback = prepackagedDatabaseCallback; + this.typeConverters = typeConverters == null ? Collections.emptyList() : typeConverters; } /** diff --git a/app/src/main/java/androidx/room/DelegatingOpenHelper.java b/app/src/main/java/androidx/room/DelegatingOpenHelper.java new file mode 100644 index 0000000000..7c84fc5487 --- /dev/null +++ b/app/src/main/java/androidx/room/DelegatingOpenHelper.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * Package private interface for OpenHelpers which delegate to other open helpers. + * + * TODO(b/175612939): delete this interface once implementations are merged. + */ +interface DelegatingOpenHelper { + + /** + * Returns the delegate open helper (which may itself be a DelegatingOpenHelper) so + * configurations on specific instances can be applied. + * + * @return the delegate + */ + @NonNull + SupportSQLiteOpenHelper getDelegate(); +} diff --git a/app/src/main/java/androidx/room/ExperimentalRoomApi.java b/app/src/main/java/androidx/room/ExperimentalRoomApi.java new file mode 100644 index 0000000000..7396429d1b --- /dev/null +++ b/app/src/main/java/androidx/room/ExperimentalRoomApi.java @@ -0,0 +1,29 @@ +/* + * 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.RequiresOptIn; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + + +/** + * APIs marked with ExperimentalRoomApi are experimental and may change. + */ +@Target({ElementType.METHOD}) +@RequiresOptIn() +@interface ExperimentalRoomApi {} diff --git a/app/src/main/java/androidx/room/InvalidationTracker.java b/app/src/main/java/androidx/room/InvalidationTracker.java index 8d160e6fd4..7dda1da913 100644 --- a/app/src/main/java/androidx/room/InvalidationTracker.java +++ b/app/src/main/java/androidx/room/InvalidationTracker.java @@ -20,6 +20,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; +import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; @@ -89,6 +90,9 @@ public class InvalidationTracker { @NonNull private Map> mViewTables; + @Nullable + AutoCloser mAutoCloser = null; + @SuppressWarnings("WeakerAccess") /* synthetic access */ final RoomDatabase mDatabase; @@ -159,6 +163,23 @@ public class InvalidationTracker { } } + /** + * 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. *

@@ -182,6 +203,13 @@ public class InvalidationTracker { } } + void onAutoCloseCallback() { + synchronized (this) { + mInitialized = false; + mObservedTableTracker.resetTriggerState(); + } + } + void startMultiInstanceInvalidation(Context context, String name) { mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, this, mDatabase.getQueryExecutor()); @@ -249,6 +277,9 @@ public class InvalidationTracker { *

* 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. */ @@ -305,6 +336,15 @@ public class InvalidationTracker { 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. *

@@ -322,6 +362,9 @@ public class InvalidationTracker { /** * 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. */ @@ -360,8 +403,8 @@ public class InvalidationTracker { public void run() { final Lock closeLock = mDatabase.getCloseLock(); Set invalidatedTableIds = null; + closeLock.lock(); try { - closeLock.lock(); if (!ensureInitialization()) { return; @@ -383,7 +426,7 @@ public class InvalidationTracker { // 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.beginTransaction(); + db.beginTransactionNonExclusive(); try { invalidatedTableIds = checkUpdatedTable(); db.setTransactionSuccessful(); @@ -399,6 +442,10 @@ public class InvalidationTracker { exception); } finally { closeLock.unlock(); + + if (mAutoCloser != null) { + mAutoCloser.decrementCountAndScheduleClose(); + } } if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) { synchronized (mObserverMap) { @@ -439,6 +486,13 @@ public class InvalidationTracker { 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); } } @@ -451,6 +505,10 @@ public class InvalidationTracker { @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(); } @@ -495,7 +553,7 @@ public class InvalidationTracker { return; } final int limit = tablesToSync.length; - database.beginTransaction(); + beginTransactionInternal(database); try { for (int tableId = 0; tableId < limit; tableId++) { switch (tablesToSync[tableId]) { @@ -785,6 +843,17 @@ public class InvalidationTracker { 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, you must call onSyncCompleted. * diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java index 3dbb8e0475..c8a7b5dc9d 100644 --- a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java +++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java @@ -138,22 +138,6 @@ class MultiInstanceInvalidationClient { } }; - private final Runnable mTearDownRunnable = new Runnable() { - @Override - public void run() { - 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); - } - }; - /** * @param context The Context to be used for binding * {@link IMultiInstanceInvalidationService}. @@ -196,7 +180,16 @@ class MultiInstanceInvalidationClient { void stop() { if (mStopped.compareAndSet(false, true)) { - mExecutor.execute(mTearDownRunnable); + 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/QueryInterceptorDatabase.java b/app/src/main/java/androidx/room/QueryInterceptorDatabase.java new file mode 100644 index 0000000000..c5ef6bc4f9 --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorDatabase.java @@ -0,0 +1,302 @@ +/* + * 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.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(); + } +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java b/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java new file mode 100644 index 0000000000..9916294cdf --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorOpenHelper.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.room; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.util.concurrent.Executor; + +final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper { + + private final SupportSQLiteOpenHelper mDelegate; + private final RoomDatabase.QueryCallback mQueryCallback; + private final Executor mQueryCallbackExecutor; + + QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper, + @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor + queryCallbackExecutor) { + mDelegate = supportSQLiteOpenHelper; + mQueryCallback = queryCallback; + mQueryCallbackExecutor = queryCallbackExecutor; + } + + @Nullable + @Override + public String getDatabaseName() { + return mDelegate.getDatabaseName(); + } + + @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) + @Override + public void setWriteAheadLoggingEnabled(boolean enabled) { + mDelegate.setWriteAheadLoggingEnabled(enabled); + } + + @Override + public SupportSQLiteDatabase getWritableDatabase() { + return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback, + mQueryCallbackExecutor); + } + + @Override + public SupportSQLiteDatabase getReadableDatabase() { + return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback, + mQueryCallbackExecutor); + } + + @Override + public void close() { + mDelegate.close(); + } + + @Override + @NonNull + public SupportSQLiteOpenHelper getDelegate() { + return mDelegate; + } + +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java b/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java new file mode 100644 index 0000000000..5d94cd10ac --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.room; + +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteOpenHelper; + +import java.util.concurrent.Executor; + +/** + * Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper. + */ +@SuppressWarnings("AcronymName") +final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { + + private final SupportSQLiteOpenHelper.Factory mDelegate; + private final RoomDatabase.QueryCallback mQueryCallback; + private final Executor mQueryCallbackExecutor; + + @SuppressWarnings("LambdaLast") + QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory, + @NonNull RoomDatabase.QueryCallback queryCallback, + @NonNull Executor queryCallbackExecutor) { + mDelegate = factory; + mQueryCallback = queryCallback; + mQueryCallbackExecutor = queryCallbackExecutor; + } + + @NonNull + @Override + public SupportSQLiteOpenHelper create( + @NonNull SupportSQLiteOpenHelper.Configuration configuration) { + return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback, + mQueryCallbackExecutor); + } +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorProgram.java b/app/src/main/java/androidx/room/QueryInterceptorProgram.java new file mode 100644 index 0000000000..2b9c554420 --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorProgram.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.room; + +import androidx.sqlite.db.SupportSQLiteProgram; + +import java.util.ArrayList; +import java.util.List; + +/** + * A program implementing an {@link SupportSQLiteProgram} API to record bind arguments. + */ +final class QueryInterceptorProgram implements SupportSQLiteProgram { + private List mBindArgsCache = new ArrayList<>(); + + @Override + public void bindNull(int index) { + saveArgsToCache(index, null); + } + + @Override + public void bindLong(int index, long value) { + saveArgsToCache(index, value); + } + + @Override + public void bindDouble(int index, double value) { + saveArgsToCache(index, value); + } + + @Override + public void bindString(int index, String value) { + saveArgsToCache(index, value); + } + + @Override + public void bindBlob(int index, byte[] value) { + saveArgsToCache(index, value); + } + + @Override + public void clearBindings() { + mBindArgsCache.clear(); + } + + @Override + public void close() { } + + private void saveArgsToCache(int bindIndex, Object value) { + // The index into bind methods are 1...n + int index = bindIndex - 1; + if (index >= mBindArgsCache.size()) { + for (int i = mBindArgsCache.size(); i <= index; i++) { + mBindArgsCache.add(null); + } + } + mBindArgsCache.set(index, value); + } + + /** + * Returns the list of arguments associated with the query. + * + * @return argument list. + */ + List getBindArgs() { + return mBindArgsCache; + } +} diff --git a/app/src/main/java/androidx/room/QueryInterceptorStatement.java b/app/src/main/java/androidx/room/QueryInterceptorStatement.java new file mode 100644 index 0000000000..8825252e5d --- /dev/null +++ b/app/src/main/java/androidx/room/QueryInterceptorStatement.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.room; + +import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteStatement; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Implements an instance of {@link SupportSQLiteStatement} for SQLite queries. + */ +final class QueryInterceptorStatement implements SupportSQLiteStatement { + + private final SupportSQLiteStatement mDelegate; + private final RoomDatabase.QueryCallback mQueryCallback; + private final String mSqlStatement; + private final List mBindArgsCache = new ArrayList<>(); + private final Executor mQueryCallbackExecutor; + + QueryInterceptorStatement(@NonNull SupportSQLiteStatement compileStatement, + @NonNull RoomDatabase.QueryCallback queryCallback, String sqlStatement, + @NonNull Executor queryCallbackExecutor) { + mDelegate = compileStatement; + mQueryCallback = queryCallback; + mSqlStatement = sqlStatement; + mQueryCallbackExecutor = queryCallbackExecutor; + } + + @Override + public void execute() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + mDelegate.execute(); + } + + @Override + public int executeUpdateDelete() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + return mDelegate.executeUpdateDelete(); + } + + @Override + public long executeInsert() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + return mDelegate.executeInsert(); + } + + @Override + public long simpleQueryForLong() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + return mDelegate.simpleQueryForLong(); + } + + @Override + public String simpleQueryForString() { + mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache)); + return mDelegate.simpleQueryForString(); + } + + @Override + public void bindNull(int index) { + saveArgsToCache(index, mBindArgsCache.toArray()); + mDelegate.bindNull(index); + } + + @Override + public void bindLong(int index, long value) { + saveArgsToCache(index, value); + mDelegate.bindLong(index, value); + } + + @Override + public void bindDouble(int index, double value) { + saveArgsToCache(index, value); + mDelegate.bindDouble(index, value); + } + + @Override + public void bindString(int index, String value) { + saveArgsToCache(index, value); + mDelegate.bindString(index, value); + } + + @Override + public void bindBlob(int index, byte[] value) { + saveArgsToCache(index, value); + mDelegate.bindBlob(index, value); + } + + @Override + public void clearBindings() { + mBindArgsCache.clear(); + mDelegate.clearBindings(); + } + + @Override + public void close() throws IOException { + mDelegate.close(); + } + + private void saveArgsToCache(int bindIndex, Object value) { + int index = bindIndex - 1; + if (index >= mBindArgsCache.size()) { + // Add null entries to the list until we have the desired # of indices + for (int i = mBindArgsCache.size(); i <= index; i++) { + mBindArgsCache.add(null); + } + } + mBindArgsCache.set(index, value); + } +} diff --git a/app/src/main/java/androidx/room/Room.java b/app/src/main/java/androidx/room/Room.java index 2e4dedc7e4..6a570c4dc0 100644 --- a/app/src/main/java/androidx/room/Room.java +++ b/app/src/main/java/androidx/room/Room.java @@ -80,14 +80,17 @@ public class Room { String name = klass.getCanonicalName(); final String postPackageName = fullPackage.isEmpty() ? name - : (name.substring(fullPackage.length() + 1)); + : 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( - fullPackage.isEmpty() ? implName : fullPackage + "." + implName); + fullClassName, true, klass.getClassLoader()); return aClass.newInstance(); } catch (ClassNotFoundException e) { throw new RuntimeException("cannot find implementation for " diff --git a/app/src/main/java/androidx/room/RoomDatabase.java b/app/src/main/java/androidx/room/RoomDatabase.java index b038c89cb2..9b652bf528 100644 --- a/app/src/main/java/androidx/room/RoomDatabase.java +++ b/app/src/main/java/androidx/room/RoomDatabase.java @@ -26,6 +26,7 @@ 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; @@ -42,7 +43,9 @@ 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; @@ -51,8 +54,8 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -89,14 +92,18 @@ public abstract class RoomDatabase { boolean mWriteAheadLoggingEnabled; /** - * @deprecated Will be hidden in the next release. + * @hide */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @Nullable @Deprecated protected List mCallbacks; 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. @@ -127,8 +134,8 @@ public abstract class RoomDatabase { return mSuspendingTransactionId; } - - private final Map mBackingFieldMap = new ConcurrentHashMap<>(); + private final Map mBackingFieldMap = + Collections.synchronizedMap(new HashMap<>()); /** * Gets the map for storing extension properties of Kotlin type. @@ -140,6 +147,23 @@ public abstract class RoomDatabase { 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. *

@@ -149,6 +173,7 @@ public abstract class RoomDatabase { */ public RoomDatabase() { mInvalidationTracker = createInvalidationTracker(); + mTypeConverters = new HashMap<>(); } /** @@ -159,10 +184,23 @@ public abstract class RoomDatabase { @CallSuper public void init(@NonNull DatabaseConfiguration configuration) { mOpenHelper = createOpenHelper(configuration); - if (mOpenHelper instanceof SQLiteCopyOpenHelper) { - SQLiteCopyOpenHelper copyOpenHelper = (SQLiteCopyOpenHelper) mOpenHelper; + + // 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; @@ -177,6 +215,65 @@ public abstract class RoomDatabase { mInvalidationTracker.startMultiInstanceInvalidation(configuration.context, configuration.name); } + + 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."); + } + } + } + + /** + * 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; } /** @@ -210,6 +307,21 @@ public abstract class RoomDatabase { @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(); + } + /** * Deletes all rows from all the tables that are registered to this database as * {@link Database#entities()}. @@ -231,6 +343,12 @@ public abstract class RoomDatabase { * @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(); } @@ -241,8 +359,8 @@ public abstract class RoomDatabase { public void close() { if (isOpen()) { final Lock closeLock = mCloseLock.writeLock(); + closeLock.lock(); try { - closeLock.lock(); mInvalidationTracker.stopMultiInstanceInvalidation(); mOpenHelper.close(); } finally { @@ -348,10 +466,27 @@ public abstract class RoomDatabase { */ @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); - database.beginTransaction(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN + && database.isWriteAheadLoggingEnabled()) { + database.beginTransactionNonExclusive(); + } else { + database.beginTransaction(); + } } /** @@ -361,6 +496,17 @@ public abstract class RoomDatabase { */ @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 @@ -540,6 +686,10 @@ public abstract class RoomDatabase { private final String mName; private final Context mContext; private ArrayList mCallbacks; + private PrepackagedDatabaseCallback mPrepackagedDatabaseCallback; + private QueryCallback mQueryCallback; + private Executor mQueryCallbackExecutor; + private List mTypeConverters; /** The Executor used to run database queries. This should be background-threaded. */ private Executor mQueryExecutor; @@ -551,6 +701,10 @@ public abstract class RoomDatabase { private boolean mMultiInstanceInvalidation; private boolean mRequireMigration; private boolean mAllowDestructiveMigrationOnDowngrade; + + private long mAutoCloseTimeout = -1L; + private TimeUnit mAutoCloseTimeUnit; + /** * Migrations, mapped by from-to pairs. */ @@ -565,6 +719,7 @@ public abstract class RoomDatabase { private String mCopyFromAssetPath; private File mCopyFromFile; + private Callable mCopyFromInputStream; Builder(@NonNull Context context, @NonNull Class klass, @Nullable String name) { mContext = context; @@ -601,6 +756,37 @@ public abstract class RoomDatabase { 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. *

@@ -612,6 +798,9 @@ public abstract class RoomDatabase { * 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. @@ -624,6 +813,108 @@ public abstract class RoomDatabase { 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}. @@ -873,6 +1164,87 @@ public abstract class RoomDatabase { 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. *

@@ -915,28 +1287,59 @@ public abstract class RoomDatabase { } } + SupportSQLiteOpenHelper.Factory factory; + + AutoCloser autoCloser = null; + if (mFactory == null) { - mFactory = new FrameworkSQLiteOpenHelperFactory(); + factory = new FrameworkSQLiteOpenHelperFactory(); + } else { + factory = mFactory; } - if (mCopyFromAssetPath != null || mCopyFromFile != null) { + 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."); } - if (mCopyFromAssetPath != null && mCopyFromFile != null) { - throw new IllegalArgumentException("Both createFromAsset() and " - + "createFromFile() was called on this Builder but the database can " - + "only be created using one of the two configurations."); + + 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."); } - mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile, - mFactory); + factory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile, + mCopyFromInputStream, factory); + } + + if (mQueryCallback != null) { + factory = new QueryInterceptorOpenHelperFactory(factory, mQueryCallback, + mQueryCallbackExecutor); } + DatabaseConfiguration configuration = new DatabaseConfiguration( mContext, mName, - mFactory, + factory, mMigrationContainer, mCallbacks, mAllowMainThreadQueries, @@ -948,7 +1351,10 @@ public abstract class RoomDatabase { mAllowDestructiveMigrationOnDowngrade, mMigrationsNotRequiredFrom, mCopyFromAssetPath, - mCopyFromFile); + mCopyFromFile, + mCopyFromInputStream, + mPrepackagedDatabaseCallback, + mTypeConverters); T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX); db.init(configuration); return db; @@ -1081,4 +1487,41 @@ public abstract class RoomDatabase { 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/SQLiteCopyOpenHelper.java b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java index f8240ceed3..b7d244ebb0 100644 --- a/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java +++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java @@ -28,20 +28,23 @@ 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 { +class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper { @NonNull private final Context mContext; @@ -49,6 +52,8 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { private final String mCopyFromAssetPath; @Nullable private final File mCopyFromFile; + @Nullable + private final Callable mCopyFromInputStream; private final int mDatabaseVersion; @NonNull private final SupportSQLiteOpenHelper mDelegate; @@ -61,11 +66,13 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { @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; } @@ -84,7 +91,7 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { @Override public synchronized SupportSQLiteDatabase getWritableDatabase() { if (!mVerified) { - verifyDatabaseFile(); + verifyDatabaseFile(true); mVerified = true; } return mDelegate.getWritableDatabase(); @@ -93,7 +100,7 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { @Override public synchronized SupportSQLiteDatabase getReadableDatabase() { if (!mVerified) { - verifyDatabaseFile(); + verifyDatabaseFile(false); mVerified = true; } return mDelegate.getReadableDatabase(); @@ -105,13 +112,19 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { 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() { + private void verifyDatabaseFile(boolean writable) { String databaseName = getDatabaseName(); File databaseFile = mContext.getDatabasePath(databaseName); boolean processLevelLock = mDatabaseConfiguration == null @@ -125,7 +138,7 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { if (!databaseFile.exists()) { try { // No database file found, copy and be done. - copyDatabaseFile(databaseFile); + copyDatabaseFile(databaseFile, writable); return; } catch (IOException e) { throw new RuntimeException("Unable to copy database file.", e); @@ -157,7 +170,7 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { if (mContext.deleteDatabase(databaseName)) { try { - copyDatabaseFile(databaseFile); + 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. @@ -172,14 +185,23 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { } } - private void copyDatabaseFile(File destinationFile) throws IOException { + 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 and copyFromFile == null!"); + 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 @@ -196,10 +218,58 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper { + 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) { + String databaseName = databaseFile.getName(); + 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(databaseName) + .callback(new Callback(version) { + @Override + public void onCreate(@NonNull SupportSQLiteDatabase db) { + } + + @Override + public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion, + int newVersion) { + } + }) + .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 index 22178e80ee..84eebe4b72 100644 --- a/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java +++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java @@ -21,6 +21,8 @@ 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 @@ -32,24 +34,30 @@ class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { 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/TransactionExecutor.java b/app/src/main/java/androidx/room/TransactionExecutor.java index 6a6bc1260d..53648d377b 100644 --- a/app/src/main/java/androidx/room/TransactionExecutor.java +++ b/app/src/main/java/androidx/room/TransactionExecutor.java @@ -38,8 +38,10 @@ class TransactionExecutor implements Executor { mExecutor = executor; } + @Override public synchronized void execute(final Runnable command) { mTasks.offer(new Runnable() { + @Override public void run() { try { command.run(); diff --git a/app/src/main/java/androidx/room/package-info.java b/app/src/main/java/androidx/room/package-info.java index 3a14e974ff..4507a93e2d 100644 --- a/app/src/main/java/androidx/room/package-info.java +++ b/app/src/main/java/androidx/room/package-info.java @@ -57,7 +57,7 @@ *
  * // File: Song.java
  * {@literal @}Entity
- * public class User {
+ * public class Song {
  *   {@literal @}PrimaryKey
  *   private int id;
  *   private String name;
@@ -82,7 +82,7 @@
  * // File: MusicDatabase.java
  * {@literal @}Database(entities = {Song.java})
  * public abstract class MusicDatabase extends RoomDatabase {
- *   public abstract SongDao userDao();
+ *   public abstract SongDao songDao();
  * }
  * 
* You can create an instance of {@code MusicDatabase} as follows: @@ -118,7 +118,7 @@ * String header; * } * // DAO - * {@literal @}Query("SELECT id, name || '-' || release_year AS header FROM user") + * {@literal @}Query("SELECT id, name || '-' || release_year AS header FROM song") * public IdAndSongHeader[] loadSongHeaders(); * * If there is a mismatch between the query result and the POJO, Room will print a warning during diff --git a/app/src/main/java/androidx/room/util/CopyLock.java b/app/src/main/java/androidx/room/util/CopyLock.java index 3750315f34..8db020c932 100644 --- a/app/src/main/java/androidx/room/util/CopyLock.java +++ b/app/src/main/java/androidx/room/util/CopyLock.java @@ -43,7 +43,7 @@ import java.util.concurrent.locks.ReentrantLock; * Thread locking within the same JVM process is done via a map of String key to ReentrantLock * objects. *
  • - * Multi-process locking is done via a dummy file whose name contains the key and FileLock + * Multi-process locking is done via a lock file whose name contains the key and FileLock * objects. * * @hide diff --git a/app/src/main/java/androidx/room/util/CursorUtil.java b/app/src/main/java/androidx/room/util/CursorUtil.java index 05b8c59170..09da6e2bb9 100644 --- a/app/src/main/java/androidx/room/util/CursorUtil.java +++ b/app/src/main/java/androidx/room/util/CursorUtil.java @@ -18,9 +18,14 @@ 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 @@ -84,11 +89,15 @@ public class CursorUtil { * @return The index of the column, or -1 if not found. */ public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) { - final int index = c.getColumnIndex(name); + int index = c.getColumnIndex(name); if (index >= 0) { return index; } - return c.getColumnIndex("`" + name + "`"); + index = c.getColumnIndex("`" + name + "`"); + if (index >= 0) { + return index; + } + return findColumnIndexBySuffix(c, name); } /** @@ -101,11 +110,56 @@ public class CursorUtil { * @throws IllegalArgumentException if the column does not exist. */ public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) { - final int index = c.getColumnIndex(name); + final int index = getColumnIndex(c, name); if (index >= 0) { return index; } - return c.getColumnIndexOrThrow("`" + name + "`"); + 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/util/FtsTableInfo.java b/app/src/main/java/androidx/room/util/FtsTableInfo.java index c076a0238b..aa7305f0ca 100644 --- a/app/src/main/java/androidx/room/util/FtsTableInfo.java +++ b/app/src/main/java/androidx/room/util/FtsTableInfo.java @@ -34,7 +34,7 @@ import java.util.Set; * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -public class FtsTableInfo { +public final class FtsTableInfo { // A set of valid FTS Options private static final String[] FTS_OPTIONS = new String[] { @@ -192,7 +192,7 @@ public class FtsTableInfo { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof FtsTableInfo)) return false; FtsTableInfo that = (FtsTableInfo) o; diff --git a/app/src/main/java/androidx/room/util/TableInfo.java b/app/src/main/java/androidx/room/util/TableInfo.java index a28aea49d3..f884f3f1b9 100644 --- a/app/src/main/java/androidx/room/util/TableInfo.java +++ b/app/src/main/java/androidx/room/util/TableInfo.java @@ -52,8 +52,8 @@ import java.util.TreeMap; @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources", "SimplifiableIfStatement"}) -// if you change this class, you must change TableInfoWriter.kt -public class TableInfo { +// if you change this class, you must change TableInfoValidationWriter.kt +public final class TableInfo { /** * Identifies from where the info object was created. @@ -118,7 +118,7 @@ public class TableInfo { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof TableInfo)) return false; TableInfo tableInfo = (TableInfo) o; @@ -340,7 +340,7 @@ public class TableInfo { * Holds the information about a database column. */ @SuppressWarnings("WeakerAccess") - public static class Column { + public static final class Column { /** * The column name. */ @@ -439,7 +439,7 @@ public class TableInfo { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof Column)) return false; Column column = (Column) o; if (Build.VERSION.SDK_INT >= 20) { @@ -512,7 +512,7 @@ public class TableInfo { * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - public static class ForeignKey { + public static final class ForeignKey { @NonNull public final String referenceTable; @NonNull @@ -537,7 +537,7 @@ public class TableInfo { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof ForeignKey)) return false; ForeignKey that = (ForeignKey) o; @@ -608,7 +608,7 @@ public class TableInfo { * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) - public static class Index { + public static final class Index { // should match the value in Index.kt public static final String DEFAULT_PREFIX = "index_"; public final String name; @@ -624,7 +624,7 @@ public class TableInfo { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof Index)) return false; Index index = (Index) o; if (unique != index.unique) { diff --git a/app/src/main/java/androidx/room/util/ViewInfo.java b/app/src/main/java/androidx/room/util/ViewInfo.java index 5e4b9b2bbe..81e6aa901c 100644 --- a/app/src/main/java/androidx/room/util/ViewInfo.java +++ b/app/src/main/java/androidx/room/util/ViewInfo.java @@ -31,7 +31,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase; * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) -public class ViewInfo { +public final class ViewInfo { /** * The view name @@ -51,7 +51,7 @@ public class ViewInfo { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + 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);