Updated room

This commit is contained in:
M66B 2024-05-01 21:19:38 +02:00
parent d636a73bc3
commit 58a63a366d
74 changed files with 7907 additions and 9104 deletions

View File

@ -516,7 +516,7 @@ configurations.configureEach {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == "androidx.room") {
//print("Pinning " + details.requested.group + ":" + details.requested.name + "\n")
details.useVersion "2.4.3"
details.useVersion "2.6.1"
} else if (details.requested.group == "androidx.lifecycle" &&
details.requested.name != "lifecycle-extensions") {
//print("Pinning " + details.requested.group + ":" + details.requested.name + "\n")
@ -558,7 +558,7 @@ dependencies {
def documentfile_version = "1.1.0-alpha01"
def lifecycle_version = "2.7.0" // 2.8.0-rc01
def lifecycle_extensions_version = "2.2.0"
def room_version = "2.4.3" // 2.5.2/2.6.1/2.7.0-alpha01
def room_version = "2.6.1" // 2.7.0-alpha01
def sqlite_version = "2.4.0" // 2.5.0-alpha01
def requery_version = "3.39.2"
def paging_version = "2.1.2" // 3.3.0-rc01

View File

@ -1,311 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.arch.core.util.Function;
import androidx.room.util.SneakyThrow;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
* AutoCloser is responsible for automatically opening (using
* delegateOpenHelper) and closing (on a timer started when there are no remaining references) a
* SupportSqliteDatabase.
*
* It is important to ensure that the ref count is incremented when using a returned database.
*/
final class AutoCloser {
@Nullable
private SupportSQLiteOpenHelper mDelegateOpenHelper = null;
@NonNull
private final Handler mHandler = new Handler(Looper.getMainLooper());
// Package private for access from mAutoCloser
@Nullable
Runnable mOnAutoCloseCallback = null;
// Package private for access from mAutoCloser
@NonNull
final Object mLock = new Object();
// Package private for access from mAutoCloser
final long mAutoCloseTimeoutInMs;
// Package private for access from mExecuteAutoCloser
@NonNull
final Executor mExecutor;
// Package private for access from mAutoCloser
@GuardedBy("mLock")
int mRefCount = 0;
// Package private for access from mAutoCloser
@GuardedBy("mLock")
long mLastDecrementRefCountTimeStamp = SystemClock.uptimeMillis();
// The unwrapped SupportSqliteDatabase
// Package private for access from mAutoCloser
@GuardedBy("mLock")
@Nullable
SupportSQLiteDatabase mDelegateDatabase;
private boolean mManuallyClosed = false;
private final Runnable mExecuteAutoCloser = new Runnable() {
@Override
public void run() {
mExecutor.execute(mAutoCloser);
}
};
// Package private for access from mExecuteAutoCloser
@NonNull
final Runnable mAutoCloser = new Runnable() {
@Override
public void run() {
synchronized (mLock) {
if (SystemClock.uptimeMillis() - mLastDecrementRefCountTimeStamp
< mAutoCloseTimeoutInMs) {
// An increment + decrement beat us to closing the db. We
// will not close the database, and there should be at least
// one more auto-close scheduled.
return;
}
if (mRefCount != 0) {
// An increment beat us to closing the db. We don't close the
// db, and another closer will be scheduled once the ref
// count is decremented.
return;
}
if (mOnAutoCloseCallback != null) {
mOnAutoCloseCallback.run();
} else {
throw new IllegalStateException("mOnAutoCloseCallback is null but it should"
+ " have been set before use. Please file a bug "
+ "against Room at: https://issuetracker.google"
+ ".com/issues/new?component=413107&template=1096568");
}
if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) {
try {
mDelegateDatabase.close();
} catch (IOException e) {
SneakyThrow.reThrow(e);
}
mDelegateDatabase = null;
}
}
}
};
/**
* Construct an AutoCloser.
*
* @param autoCloseTimeoutAmount time for auto close timer
* @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount
* @param autoCloseExecutor the executor on which the auto close operation will happen
*/
AutoCloser(long autoCloseTimeoutAmount,
@NonNull TimeUnit autoCloseTimeUnit,
@NonNull Executor autoCloseExecutor) {
mAutoCloseTimeoutInMs = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount);
mExecutor = autoCloseExecutor;
}
/**
* Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the
* delegateOpenHelper after construction.
*
* @param delegateOpenHelper the open helper that is used to create
* new SupportSqliteDatabases
*/
public void init(@NonNull SupportSQLiteOpenHelper delegateOpenHelper) {
if (mDelegateOpenHelper != null) {
Log.e(Room.LOG_TAG, "AutoCloser initialized multiple times. Please file a bug against"
+ " room at: https://issuetracker.google"
+ ".com/issues/new?component=413107&template=1096568");
return;
}
this.mDelegateOpenHelper = delegateOpenHelper;
}
/**
* Execute a ref counting function. The function will receive an unwrapped open database and
* this database will stay open until at least after function returns. If there are no more
* references in use for the db once function completes, an auto close operation will be
* scheduled.
*/
@Nullable
public <V> V executeRefCountingFunction(@NonNull Function<SupportSQLiteDatabase, V> 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;
}
}

View File

@ -0,0 +1,227 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import java.io.IOException
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
/**
* AutoCloser is responsible for automatically opening (using
* delegateOpenHelper) and closing (on a timer started when there are no remaining references) a
* SupportSqliteDatabase.
*
* It is important to ensure that the ref count is incremented when using a returned database.
*
* @param autoCloseTimeoutAmount time for auto close timer
* @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount
* @param autoCloseExecutor the executor on which the auto close operation will happen
*/
internal class AutoCloser(
autoCloseTimeoutAmount: Long,
autoCloseTimeUnit: TimeUnit,
autoCloseExecutor: Executor
) {
lateinit var delegateOpenHelper: SupportSQLiteOpenHelper
private val handler = Handler(Looper.getMainLooper())
internal var onAutoCloseCallback: Runnable? = null
private val lock = Any()
private var autoCloseTimeoutInMs: Long = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount)
private val executor: Executor = autoCloseExecutor
@GuardedBy("lock")
internal var refCount = 0
@GuardedBy("lock")
internal var lastDecrementRefCountTimeStamp = SystemClock.uptimeMillis()
// The unwrapped SupportSqliteDatabase
@GuardedBy("lock")
internal var delegateDatabase: SupportSQLiteDatabase? = null
private var manuallyClosed = false
private val executeAutoCloser = Runnable { executor.execute(autoCloser) }
private val autoCloser = Runnable {
synchronized(lock) {
if (SystemClock.uptimeMillis() - lastDecrementRefCountTimeStamp
< autoCloseTimeoutInMs
) {
// An increment + decrement beat us to closing the db. We
// will not close the database, and there should be at least
// one more auto-close scheduled.
return@Runnable
}
if (refCount != 0) {
// An increment beat us to closing the db. We don't close the
// db, and another closer will be scheduled once the ref
// count is decremented.
return@Runnable
}
onAutoCloseCallback?.run() ?: error(
"onAutoCloseCallback is null but it should" +
" have been set before use. Please file a bug " +
"against Room at: $autoCloseBug"
)
delegateDatabase?.let {
if (it.isOpen) {
it.close()
}
}
delegateDatabase = null
}
}
/**
* Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the
* delegateOpenHelper after construction.
*
* @param delegateOpenHelper the open helper that is used to create
* new SupportSqliteDatabases
*/
fun init(delegateOpenHelper: SupportSQLiteOpenHelper) {
this.delegateOpenHelper = delegateOpenHelper
}
/**
* Execute a ref counting function. The function will receive an unwrapped open database and
* this database will stay open until at least after function returns. If there are no more
* references in use for the db once function completes, an auto close operation will be
* scheduled.
*/
fun <V> executeRefCountingFunction(block: (SupportSQLiteDatabase) -> V): V =
try {
block(incrementCountAndEnsureDbIsOpen())
} finally {
decrementCountAndScheduleClose()
}
/**
* Confirms that autoCloser is no longer running and confirms that delegateDatabase is set
* and open. delegateDatabase will not be auto closed until
* decrementRefCountAndScheduleClose is called. decrementRefCountAndScheduleClose must be
* called once for each call to incrementCountAndEnsureDbIsOpen.
*
* If this throws an exception, decrementCountAndScheduleClose must still be called!
*
* @return the *unwrapped* SupportSQLiteDatabase.
*/
fun incrementCountAndEnsureDbIsOpen(): SupportSQLiteDatabase {
// TODO(rohitsat): avoid synchronized(lock) when possible. We should be able to avoid it
// when refCount is not hitting zero or if there is no auto close scheduled if we use
// Atomics.
synchronized(lock) {
// If there is a scheduled autoclose operation, we should remove it from the handler.
handler.removeCallbacks(executeAutoCloser)
refCount++
check(!manuallyClosed) { "Attempting to open already closed database." }
delegateDatabase?.let {
if (it.isOpen) {
return it
}
}
return delegateOpenHelper.writableDatabase.also { delegateDatabase = it }
}
}
/**
* Decrements the ref count and schedules a close if there are no other references to the db.
* This must only be called after a corresponding incrementCountAndEnsureDbIsOpen call.
*/
fun decrementCountAndScheduleClose() {
// TODO(rohitsat): avoid synchronized(lock) when possible
synchronized(lock) {
check(refCount > 0) {
"ref count is 0 or lower but we're supposed to decrement"
}
// decrement refCount
refCount--
// if refcount is zero, schedule close operation
if (refCount == 0) {
if (delegateDatabase == null) {
// No db to close, this can happen due to exceptions when creating db...
return
}
handler.postDelayed(executeAutoCloser, autoCloseTimeoutInMs)
}
}
}
/**
* Close the database if it is still active.
*
* @throws IOException if an exception is encountered when closing the underlying db.
*/
@Throws(IOException::class)
fun closeDatabaseIfOpen() {
synchronized(lock) {
manuallyClosed = true
delegateDatabase?.close()
delegateDatabase = null
}
}
/**
* The auto closer is still active if the database has not been closed. This means that
* whether or not the underlying database is closed, when active we will re-open it on the
* next access.
*
* @return a boolean indicating whether the auto closer is still active
*/
val isActive: Boolean
get() = !manuallyClosed
/**
* Returns the current ref count for this auto closer. This is only visible for testing.
*
* @return current ref count
*/
@get:VisibleForTesting
internal val refCountForTest: Int
get() {
synchronized(lock) { return refCount }
}
/**
* Sets a callback that will be run every time the database is auto-closed. This callback
* needs to be lightweight since it is run while holding a lock.
*
* @param onAutoClose the callback to run
*/
fun setAutoCloseCallback(onAutoClose: Runnable) {
onAutoCloseCallback = onAutoClose
}
companion object {
const val autoCloseBug = "https://issuetracker.google.com/issues/new?component=" +
"413107&template=1096568"
}
}

View File

@ -1,878 +0,0 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.database.SQLException;
import android.database.sqlite.SQLiteTransactionListener;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.arch.core.util.Function;
import androidx.room.util.SneakyThrow;
import androidx.sqlite.db.SupportSQLiteCompat;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.SupportSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* A SupportSQLiteOpenHelper that has autoclose enabled for database connections.
*/
final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
@NonNull
private final SupportSQLiteOpenHelper mDelegateOpenHelper;
@NonNull
private final AutoClosingSupportSQLiteDatabase mAutoClosingDb;
@NonNull
private final AutoCloser mAutoCloser;
AutoClosingRoomOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
@NonNull AutoCloser autoCloser) {
mDelegateOpenHelper = supportSQLiteOpenHelper;
mAutoCloser = autoCloser;
autoCloser.init(mDelegateOpenHelper);
mAutoClosingDb = new AutoClosingSupportSQLiteDatabase(mAutoCloser);
}
@Nullable
@Override
public String getDatabaseName() {
return mDelegateOpenHelper.getDatabaseName();
}
@Override
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void setWriteAheadLoggingEnabled(boolean enabled) {
mDelegateOpenHelper.setWriteAheadLoggingEnabled(enabled);
}
@NonNull
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public SupportSQLiteDatabase getWritableDatabase() {
// Note we don't differentiate between writable db and readable db
// We try to open the db so the open callbacks run
mAutoClosingDb.pokeOpen();
return mAutoClosingDb;
}
@NonNull
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public SupportSQLiteDatabase getReadableDatabase() {
// Note we don't differentiate between writable db and readable db
// We try to open the db so the open callbacks run
mAutoClosingDb.pokeOpen();
return mAutoClosingDb;
}
@Override
public void close() {
try {
mAutoClosingDb.close();
} catch (IOException e) {
SneakyThrow.reThrow(e);
}
}
/**
* package protected to pass it to invalidation tracker...
*/
@NonNull
AutoCloser getAutoCloser() {
return this.mAutoCloser;
}
@NonNull
SupportSQLiteDatabase getAutoClosingDb() {
return this.mAutoClosingDb;
}
@Override
@NonNull
public SupportSQLiteOpenHelper getDelegate() {
return mDelegateOpenHelper;
}
/**
* SupportSQLiteDatabase that also keeps refcounts and autocloses the database
*/
static final class AutoClosingSupportSQLiteDatabase implements SupportSQLiteDatabase {
@NonNull
private final AutoCloser mAutoCloser;
AutoClosingSupportSQLiteDatabase(@NonNull AutoCloser autoCloser) {
mAutoCloser = autoCloser;
}
void pokeOpen() {
mAutoCloser.executeRefCountingFunction(db -> null);
}
@Override
public SupportSQLiteStatement compileStatement(String sql) {
return new AutoClosingSupportSqliteStatement(sql, mAutoCloser);
}
@Override
public void beginTransaction() {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransaction();
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionNonExclusive() {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionNonExclusive();
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionWithListener(transactionListener);
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionWithListenerNonExclusive(
SQLiteTransactionListener transactionListener) {
// We assume that after every successful beginTransaction() call there *will* always
// be a corresponding endTransaction() call. Without a corresponding
// endTransactionCall we will never close the db.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionWithListenerNonExclusive(transactionListener);
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void endTransaction() {
if (mAutoCloser.getDelegateDatabase() == null) {
// This should never happen.
throw new IllegalStateException("End transaction called but delegateDb is null");
}
try {
mAutoCloser.getDelegateDatabase().endTransaction();
} finally {
mAutoCloser.decrementCountAndScheduleClose();
}
}
@Override
public void setTransactionSuccessful() {
SupportSQLiteDatabase delegate = mAutoCloser.getDelegateDatabase();
if (delegate == null) {
// This should never happen.
throw new IllegalStateException("setTransactionSuccessful called but delegateDb "
+ "is null");
}
delegate.setTransactionSuccessful();
}
@Override
public boolean inTransaction() {
if (mAutoCloser.getDelegateDatabase() == null) {
return false;
}
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction);
}
@Override
public boolean isDbLockedByCurrentThread() {
if (mAutoCloser.getDelegateDatabase() == null) {
return false;
}
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::isDbLockedByCurrentThread);
}
@Override
public boolean yieldIfContendedSafely() {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::yieldIfContendedSafely);
}
@Override
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::yieldIfContendedSafely);
}
@Override
public int getVersion() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getVersion);
}
@Override
public void setVersion(int version) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setVersion(version);
return null;
});
}
@Override
public long getMaximumSize() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getMaximumSize);
}
@Override
public long setMaximumSize(long numBytes) {
return mAutoCloser.executeRefCountingFunction(db -> db.setMaximumSize(numBytes));
}
@Override
public long getPageSize() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPageSize);
}
@Override
public void setPageSize(long numBytes) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setPageSize(numBytes);
return null;
});
}
@Override
public Cursor query(String query) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public Cursor query(String query, Object[] bindArgs) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query, bindArgs);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public Cursor query(SupportSQLiteQuery query) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query, cancellationSignal);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public long insert(String table, int conflictAlgorithm, ContentValues values)
throws SQLException {
return mAutoCloser.executeRefCountingFunction(db -> db.insert(table, conflictAlgorithm,
values));
}
@Override
public int delete(String table, String whereClause, Object[] whereArgs) {
return mAutoCloser.executeRefCountingFunction(
db -> db.delete(table, whereClause, whereArgs));
}
@Override
public int update(String table, int conflictAlgorithm, ContentValues values,
String whereClause, Object[] whereArgs) {
return mAutoCloser.executeRefCountingFunction(db -> db.update(table, conflictAlgorithm,
values, whereClause, whereArgs));
}
@Override
public void execSQL(String sql) throws SQLException {
mAutoCloser.executeRefCountingFunction(db -> {
db.execSQL(sql);
return null;
});
}
@Override
public void execSQL(String sql, Object[] bindArgs) throws SQLException {
mAutoCloser.executeRefCountingFunction(db -> {
db.execSQL(sql, bindArgs);
return null;
});
}
@Override
public boolean isReadOnly() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::isReadOnly);
}
@Override
public boolean isOpen() {
// Get the db without incrementing the reference cause we don't want to open
// the db for an isOpen call.
SupportSQLiteDatabase localDelegate = mAutoCloser.getDelegateDatabase();
if (localDelegate == null) {
return false;
}
return localDelegate.isOpen();
}
@Override
public boolean needUpgrade(int newVersion) {
return mAutoCloser.executeRefCountingFunction(db -> db.needUpgrade(newVersion));
}
@Override
public String getPath() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPath);
}
@Override
public void setLocale(Locale locale) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setLocale(locale);
return null;
});
}
@Override
public void setMaxSqlCacheSize(int cacheSize) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setMaxSqlCacheSize(cacheSize);
return null;
});
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void setForeignKeyConstraintsEnabled(boolean enable) {
mAutoCloser.executeRefCountingFunction(db -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
db.setForeignKeyConstraintsEnabled(enable);
}
return null;
});
}
@Override
public boolean enableWriteAheadLogging() {
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
+ "OpenHelper instead of on the database directly.");
}
@Override
public void disableWriteAheadLogging() {
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
+ "OpenHelper instead of on the database directly.");
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public boolean isWriteAheadLoggingEnabled() {
return mAutoCloser.executeRefCountingFunction(db -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return db.isWriteAheadLoggingEnabled();
}
return false;
});
}
@Override
public List<Pair<String, String>> getAttachedDbs() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getAttachedDbs);
}
@Override
public boolean isDatabaseIntegrityOk() {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::isDatabaseIntegrityOk);
}
@Override
public void close() throws IOException {
mAutoCloser.closeDatabaseIfOpen();
}
@Override
public boolean isExecPerConnectionSQLSupported() {
return false;
}
@Override
public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs) {
}
}
/**
* We need to keep the db alive until the cursor is closed, so we can't decrement our
* reference count until the cursor is closed. The underlying database will not close until
* this cursor is closed.
*/
private static final class KeepAliveCursor implements Cursor {
private final Cursor mDelegate;
private final AutoCloser mAutoCloser;
KeepAliveCursor(Cursor delegate, AutoCloser autoCloser) {
mDelegate = delegate;
mAutoCloser = autoCloser;
}
// close is the only important/changed method here:
@Override
public void close() {
mDelegate.close();
mAutoCloser.decrementCountAndScheduleClose();
}
@Override
public boolean isClosed() {
return mDelegate.isClosed();
}
@Override
public int getCount() {
return mDelegate.getCount();
}
@Override
public int getPosition() {
return mDelegate.getPosition();
}
@Override
public boolean move(int offset) {
return mDelegate.move(offset);
}
@Override
public boolean moveToPosition(int position) {
return mDelegate.moveToPosition(position);
}
@Override
public boolean moveToFirst() {
return mDelegate.moveToFirst();
}
@Override
public boolean moveToLast() {
return mDelegate.moveToLast();
}
@Override
public boolean moveToNext() {
return mDelegate.moveToNext();
}
@Override
public boolean moveToPrevious() {
return mDelegate.moveToPrevious();
}
@Override
public boolean isFirst() {
return mDelegate.isFirst();
}
@Override
public boolean isLast() {
return mDelegate.isLast();
}
@Override
public boolean isBeforeFirst() {
return mDelegate.isBeforeFirst();
}
@Override
public boolean isAfterLast() {
return mDelegate.isAfterLast();
}
@Override
public int getColumnIndex(String columnName) {
return mDelegate.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
return mDelegate.getColumnIndexOrThrow(columnName);
}
@Override
public String getColumnName(int columnIndex) {
return mDelegate.getColumnName(columnIndex);
}
@Override
public String[] getColumnNames() {
return mDelegate.getColumnNames();
}
@Override
public int getColumnCount() {
return mDelegate.getColumnCount();
}
@Override
public byte[] getBlob(int columnIndex) {
return mDelegate.getBlob(columnIndex);
}
@Override
public String getString(int columnIndex) {
return mDelegate.getString(columnIndex);
}
@Override
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
mDelegate.copyStringToBuffer(columnIndex, buffer);
}
@Override
public short getShort(int columnIndex) {
return mDelegate.getShort(columnIndex);
}
@Override
public int getInt(int columnIndex) {
return mDelegate.getInt(columnIndex);
}
@Override
public long getLong(int columnIndex) {
return mDelegate.getLong(columnIndex);
}
@Override
public float getFloat(int columnIndex) {
return mDelegate.getFloat(columnIndex);
}
@Override
public double getDouble(int columnIndex) {
return mDelegate.getDouble(columnIndex);
}
@Override
public int getType(int columnIndex) {
return mDelegate.getType(columnIndex);
}
@Override
public boolean isNull(int columnIndex) {
return mDelegate.isNull(columnIndex);
}
/**
* @deprecated see Cursor.deactivate
*/
@Override
@Deprecated
public void deactivate() {
mDelegate.deactivate();
}
/**
* @deprecated see Cursor.requery
*/
@Override
@Deprecated
public boolean requery() {
return mDelegate.requery();
}
@Override
public void registerContentObserver(ContentObserver observer) {
mDelegate.registerContentObserver(observer);
}
@Override
public void unregisterContentObserver(ContentObserver observer) {
mDelegate.unregisterContentObserver(observer);
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
mDelegate.registerDataSetObserver(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
mDelegate.unregisterDataSetObserver(observer);
}
@Override
public void setNotificationUri(ContentResolver cr, Uri uri) {
mDelegate.setNotificationUri(cr, uri);
}
@RequiresApi(api = Build.VERSION_CODES.Q)
@Override
public void setNotificationUris(@NonNull ContentResolver cr,
@NonNull List<Uri> uris) {
SupportSQLiteCompat.Api29Impl.setNotificationUris(mDelegate, cr, uris);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public Uri getNotificationUri() {
return SupportSQLiteCompat.Api19Impl.getNotificationUri(mDelegate);
}
@RequiresApi(api = Build.VERSION_CODES.Q)
@Nullable
@Override
public List<Uri> getNotificationUris() {
return SupportSQLiteCompat.Api29Impl.getNotificationUris(mDelegate);
}
@Override
public boolean getWantsAllOnMoveCalls() {
return mDelegate.getWantsAllOnMoveCalls();
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public void setExtras(Bundle extras) {
SupportSQLiteCompat.Api23Impl.setExtras(mDelegate, extras);
}
@Override
public Bundle getExtras() {
return mDelegate.getExtras();
}
@Override
public Bundle respond(Bundle extras) {
return mDelegate.respond(extras);
}
}
/**
* We can't close our db if the SupportSqliteStatement is open.
*
* Each of these that are created need to be registered with RefCounter.
*
* On auto-close, RefCounter needs to close each of these before closing the db that these
* were constructed from.
*
* Each of the methods here need to get
*/
//TODO(rohitsat) cache the prepared statement... I'm not sure what the performance implications
// are for the way it's done here, but caching the prepared statement would definitely be more
// complicated since we need to invalidate any of the PreparedStatements that were created
// with this db
private static class AutoClosingSupportSqliteStatement implements SupportSQLiteStatement {
private final String mSql;
private final ArrayList<Object> mBinds = new ArrayList<>();
private final AutoCloser mAutoCloser;
AutoClosingSupportSqliteStatement(
String sql, AutoCloser autoCloser) {
mSql = sql;
mAutoCloser = autoCloser;
}
private <T> T executeSqliteStatementWithRefCount(Function<SupportSQLiteStatement, T> 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();
}
}
}

View File

@ -0,0 +1,570 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.content.ContentResolver
import android.content.ContentValues
import android.database.Cursor
import android.database.SQLException
import android.database.sqlite.SQLiteTransactionListener
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.CancellationSignal
import android.util.Pair
import androidx.annotation.RequiresApi
import androidx.sqlite.db.SupportSQLiteCompat
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteStatement
import java.io.IOException
import java.util.Locale
/**
* A SupportSQLiteOpenHelper that has auto close enabled for database connections.
*/
internal class AutoClosingRoomOpenHelper(
override val delegate: SupportSQLiteOpenHelper,
@JvmField
internal val autoCloser: AutoCloser
) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper {
private val autoClosingDb: AutoClosingSupportSQLiteDatabase
init {
autoCloser.init(delegate)
autoClosingDb = AutoClosingSupportSQLiteDatabase(
autoCloser
)
}
@get:RequiresApi(api = Build.VERSION_CODES.N)
override val writableDatabase: SupportSQLiteDatabase
get() {
autoClosingDb.pokeOpen()
return autoClosingDb
}
@get:RequiresApi(api = Build.VERSION_CODES.N)
override val readableDatabase: SupportSQLiteDatabase
get() {
// Note we don't differentiate between writable db and readable db
// We try to open the db so the open callbacks run
autoClosingDb.pokeOpen()
return autoClosingDb
}
override fun close() {
autoClosingDb.close()
}
/**
* SupportSQLiteDatabase that also keeps refcounts and autocloses the database
*/
internal class AutoClosingSupportSQLiteDatabase(
private val autoCloser: AutoCloser
) : SupportSQLiteDatabase {
fun pokeOpen() {
autoCloser.executeRefCountingFunction<Any?> { null }
}
override fun compileStatement(sql: String): SupportSQLiteStatement {
return AutoClosingSupportSqliteStatement(sql, autoCloser)
}
override fun beginTransaction() {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
try {
db.beginTransaction()
} catch (t: Throwable) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
autoCloser.decrementCountAndScheduleClose()
throw t
}
}
override fun beginTransactionNonExclusive() {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
try {
db.beginTransactionNonExclusive()
} catch (t: Throwable) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
autoCloser.decrementCountAndScheduleClose()
throw t
}
}
override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
try {
db.beginTransactionWithListener(transactionListener)
} catch (t: Throwable) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
autoCloser.decrementCountAndScheduleClose()
throw t
}
}
override fun beginTransactionWithListenerNonExclusive(
transactionListener: SQLiteTransactionListener
) {
// We assume that after every successful beginTransaction() call there *will* always
// be a corresponding endTransaction() call. Without a corresponding
// endTransactionCall we will never close the db.
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
try {
db.beginTransactionWithListenerNonExclusive(transactionListener)
} catch (t: Throwable) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
autoCloser.decrementCountAndScheduleClose()
throw t
}
}
override fun endTransaction() {
checkNotNull(autoCloser.delegateDatabase) {
"End transaction called but delegateDb is null"
}
try {
autoCloser.delegateDatabase!!.endTransaction()
} finally {
autoCloser.decrementCountAndScheduleClose()
}
}
override fun setTransactionSuccessful() {
autoCloser.delegateDatabase?.setTransactionSuccessful() ?: error(
"setTransactionSuccessful called but delegateDb is null"
)
}
override fun inTransaction(): Boolean {
return if (autoCloser.delegateDatabase == null) {
false
} else {
autoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction)
}
}
override val isDbLockedByCurrentThread: Boolean
get() = if (autoCloser.delegateDatabase == null) {
false
} else {
autoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::isDbLockedByCurrentThread
)
}
override fun yieldIfContendedSafely(): Boolean {
return autoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::yieldIfContendedSafely
)
}
override fun yieldIfContendedSafely(sleepAfterYieldDelayMillis: Long): Boolean {
return autoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::yieldIfContendedSafely
)
}
override var version: Int
get() = autoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::version
)
set(version) {
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
db.version = version
null
}
}
override val maximumSize: Long
get() = autoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::maximumSize
)
override fun setMaximumSize(numBytes: Long): Long {
return autoCloser.executeRefCountingFunction {
db: SupportSQLiteDatabase -> db.setMaximumSize(numBytes)
}
}
override var pageSize: Long
get() = autoCloser.executeRefCountingFunction(SupportSQLiteDatabase::pageSize)
set(numBytes) {
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
db.pageSize = numBytes
null
}
}
override fun query(query: String): Cursor {
val result = try {
autoCloser.incrementCountAndEnsureDbIsOpen().query(query)
} catch (throwable: Throwable) {
autoCloser.decrementCountAndScheduleClose()
throw throwable
}
return KeepAliveCursor(result, autoCloser)
}
override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
val result = try {
autoCloser.incrementCountAndEnsureDbIsOpen().query(query, bindArgs)
} catch (throwable: Throwable) {
autoCloser.decrementCountAndScheduleClose()
throw throwable
}
return KeepAliveCursor(result, autoCloser)
}
override fun query(query: SupportSQLiteQuery): Cursor {
val result = try {
autoCloser.incrementCountAndEnsureDbIsOpen().query(query)
} catch (throwable: Throwable) {
autoCloser.decrementCountAndScheduleClose()
throw throwable
}
return KeepAliveCursor(result, autoCloser)
}
@RequiresApi(api = Build.VERSION_CODES.N)
override fun query(
query: SupportSQLiteQuery,
cancellationSignal: CancellationSignal?
): Cursor {
val result = try {
autoCloser.incrementCountAndEnsureDbIsOpen().query(query, cancellationSignal)
} catch (throwable: Throwable) {
autoCloser.decrementCountAndScheduleClose()
throw throwable
}
return KeepAliveCursor(result, autoCloser)
}
@Throws(SQLException::class)
override fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long {
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
db.insert(
table, conflictAlgorithm,
values
)
}
}
override fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int {
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
db.delete(
table,
whereClause,
whereArgs
)
}
}
override fun update(
table: String,
conflictAlgorithm: Int,
values: ContentValues,
whereClause: String?,
whereArgs: Array<out Any?>?
): Int {
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
db.update(
table, conflictAlgorithm,
values, whereClause, whereArgs
)
}
}
@Throws(SQLException::class)
override fun execSQL(sql: String) {
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
db.execSQL(sql)
null
}
}
@Throws(SQLException::class)
override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
db.execSQL(sql, bindArgs)
null
}
}
override val isReadOnly: Boolean
get() = autoCloser.executeRefCountingFunction { obj: SupportSQLiteDatabase ->
obj.isReadOnly
}
override val isOpen: Boolean
get() {
// Get the db without incrementing the reference cause we don't want to open
// the db for an isOpen call.
val localDelegate = autoCloser.delegateDatabase ?: return false
return localDelegate.isOpen
}
override fun needUpgrade(newVersion: Int): Boolean {
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
db.needUpgrade(
newVersion
)
}
}
override val path: String?
get() = autoCloser.executeRefCountingFunction { obj: SupportSQLiteDatabase ->
obj.path
}
override fun setLocale(locale: Locale) {
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
db.setLocale(locale)
null
}
}
override fun setMaxSqlCacheSize(cacheSize: Int) {
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
db.setMaxSqlCacheSize(cacheSize)
null
}
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override fun setForeignKeyConstraintsEnabled(enabled: Boolean) {
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
db.setForeignKeyConstraintsEnabled(enabled)
}
null
}
}
override fun enableWriteAheadLogging(): Boolean {
throw UnsupportedOperationException(
"Enable/disable write ahead logging on the " +
"OpenHelper instead of on the database directly."
)
}
override fun disableWriteAheadLogging() {
throw UnsupportedOperationException(
"Enable/disable write ahead logging on the " +
"OpenHelper instead of on the database directly."
)
}
@get:RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override val isWriteAheadLoggingEnabled: Boolean
get() = autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return@executeRefCountingFunction db.isWriteAheadLoggingEnabled
}
false
}
override val attachedDbs: List<Pair<String, String>>?
get() = autoCloser.executeRefCountingFunction {
obj: SupportSQLiteDatabase -> obj.attachedDbs
}
override val isDatabaseIntegrityOk: Boolean
get() = autoCloser.executeRefCountingFunction {
obj: SupportSQLiteDatabase -> obj.isDatabaseIntegrityOk
}
@Throws(IOException::class)
override fun close() {
autoCloser.closeDatabaseIfOpen()
}
}
/**
* We need to keep the db alive until the cursor is closed, so we can't decrement our
* reference count until the cursor is closed. The underlying database will not close until
* this cursor is closed.
*/
private class KeepAliveCursor(
private val delegate: Cursor,
private val autoCloser: AutoCloser
) : Cursor by delegate {
// close is the only important/changed method here:
override fun close() {
delegate.close()
autoCloser.decrementCountAndScheduleClose()
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun setNotificationUris(
cr: ContentResolver,
uris: List<Uri>
) {
SupportSQLiteCompat.Api29Impl.setNotificationUris(delegate, cr, uris)
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
override fun getNotificationUri(): Uri {
return SupportSQLiteCompat.Api19Impl.getNotificationUri(delegate)
}
@RequiresApi(api = Build.VERSION_CODES.Q)
override fun getNotificationUris(): List<Uri> {
return SupportSQLiteCompat.Api29Impl.getNotificationUris(delegate)
}
@RequiresApi(api = Build.VERSION_CODES.M)
override fun setExtras(extras: Bundle) {
SupportSQLiteCompat.Api23Impl.setExtras(delegate, extras)
}
}
/**
* We can't close our db if the SupportSqliteStatement is open.
*
* Each of these that are created need to be registered with RefCounter.
*
* On auto-close, RefCounter needs to close each of these before closing the db that these
* were constructed from.
*
* Each of the methods here need to get
*/
// TODO(rohitsat) cache the prepared statement... I'm not sure what the performance implications
// are for the way it's done here, but caching the prepared statement would definitely be more
// complicated since we need to invalidate any of the PreparedStatements that were created
// with this db
private class AutoClosingSupportSqliteStatement(
private val sql: String,
private val autoCloser: AutoCloser
) : SupportSQLiteStatement {
private val binds = ArrayList<Any?>()
private fun <T> executeSqliteStatementWithRefCount(
block: (SupportSQLiteStatement) -> T
): T {
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
val statement: SupportSQLiteStatement = db.compileStatement(sql)
doBinds(statement)
block(statement)
}
}
private fun doBinds(supportSQLiteStatement: SupportSQLiteStatement) {
// Replay the binds
binds.forEachIndexed { i, _ ->
val bindIndex = i + 1 // Bind indices are 1 based so we start at 1 not 0
when (val bind = binds[i]) {
null -> {
supportSQLiteStatement.bindNull(bindIndex)
}
is Long -> {
supportSQLiteStatement.bindLong(bindIndex, bind)
}
is Double -> {
supportSQLiteStatement.bindDouble(bindIndex, bind)
}
is String -> {
supportSQLiteStatement.bindString(bindIndex, bind)
}
is ByteArray -> {
supportSQLiteStatement.bindBlob(bindIndex, bind)
}
}
}
}
private fun saveBinds(bindIndex: Int, value: Any?) {
val index = bindIndex - 1
if (index >= binds.size) {
// Add null entries to the list until we have the desired # of indices
for (i in binds.size..index) {
binds.add(null)
}
}
binds[index] = value
}
@Throws(IOException::class)
override fun close() {
// Nothing to do here since we re-compile the statement each time.
}
override fun execute() {
executeSqliteStatementWithRefCount<Any?> { statement: SupportSQLiteStatement ->
statement.execute()
null
}
}
override fun executeUpdateDelete(): Int {
return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement ->
obj.executeUpdateDelete() }
}
override fun executeInsert(): Long {
return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement ->
obj.executeInsert()
}
}
override fun simpleQueryForLong(): Long {
return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement ->
obj.simpleQueryForLong()
}
}
override fun simpleQueryForString(): String? {
return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement ->
obj.simpleQueryForString()
}
}
override fun bindNull(index: Int) {
saveBinds(index, null)
}
override fun bindLong(index: Int, value: Long) {
saveBinds(index, value)
}
override fun bindDouble(index: Int, value: Double) {
saveBinds(index, value)
}
override fun bindString(index: Int, value: String) {
saveBinds(index, value)
}
override fun bindBlob(index: Int, value: ByteArray) {
saveBinds(index, value)
}
override fun clearBindings() {
binds.clear()
}
}
}

View File

@ -1,48 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.NonNull;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
/**
* Factory class for AutoClosingRoomOpenHelper
*/
final class AutoClosingRoomOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
@NonNull
private final SupportSQLiteOpenHelper.Factory mDelegate;
@NonNull
private final AutoCloser mAutoCloser;
AutoClosingRoomOpenHelperFactory(
@NonNull SupportSQLiteOpenHelper.Factory factory,
@NonNull AutoCloser autoCloser) {
mDelegate = factory;
mAutoCloser = autoCloser;
}
/**
* @return AutoClosingRoomOpenHelper instances.
*/
@Override
@NonNull
public AutoClosingRoomOpenHelper create(
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
return new AutoClosingRoomOpenHelper(mDelegate.create(configuration), mAutoCloser);
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.sqlite.db.SupportSQLiteOpenHelper
/**
* Factory class for AutoClosingRoomOpenHelper
*/
internal class AutoClosingRoomOpenHelperFactory(
private val delegate: SupportSQLiteOpenHelper.Factory,
private val autoCloser: AutoCloser
) : SupportSQLiteOpenHelper.Factory {
/**
* @return AutoClosingRoomOpenHelper instances.
*/
override fun create(
configuration: SupportSQLiteOpenHelper.Configuration
): AutoClosingRoomOpenHelper {
return AutoClosingRoomOpenHelper(delegate.create(configuration), autoCloser)
}
}

View File

@ -1,112 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Utility class for in-process and multi-process key-based lock mechanism for safely copying
* database files.
* <p>
* Acquiring the lock will be quick if no other thread or process has a lock with the same key.
* But if the lock is already held then acquiring it will block, until the other thread or process
* releases the lock. Note that the key and lock directory must be the same to achieve
* synchronization.
* <p>
* Locking is done via two levels:
* <ol>
* <li>
* Thread locking within the same JVM process is done via a map of String key to ReentrantLock
* objects.
* <li>
* Multi-process locking is done via a lock file whose name contains the key and FileLock
* objects.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class CopyLock {
// in-process lock map
private static final Map<String, Lock> sThreadLocks = new HashMap<>();
private final File mCopyLockFile;
private final Lock mThreadLock;
private final boolean mFileLevelLock;
private FileChannel mLockChannel;
/**
* Creates a lock with {@code name} and using {@code lockDir} as the directory for the
* lock files.
* @param name the name of this lock.
* @param lockDir the directory where the lock files will be located.
* @param processLock whether to use file for process level locking or not.
*/
public CopyLock(@NonNull String name, @NonNull File lockDir, boolean processLock) {
mCopyLockFile = new File(lockDir, name + ".lck");
mThreadLock = getThreadLock(mCopyLockFile.getAbsolutePath());
mFileLevelLock = processLock;
}
/**
* Attempts to grab the lock, blocking if already held by another thread or process.
*/
public void lock() {
mThreadLock.lock();
if (mFileLevelLock) {
try {
mLockChannel = new FileOutputStream(mCopyLockFile).getChannel();
mLockChannel.lock();
} catch (IOException e) {
throw new IllegalStateException("Unable to grab copy lock.", e);
}
}
}
/**
* Releases the lock.
*/
public void unlock() {
if (mLockChannel != null) {
try {
mLockChannel.close();
} catch (IOException ignored) { }
}
mThreadLock.unlock();
}
private static Lock getThreadLock(String key) {
synchronized (sThreadLocks) {
Lock threadLock = sThreadLocks.get(key);
if (threadLock == null) {
threadLock = new ReentrantLock();
sThreadLocks.put(key, threadLock);
}
return threadLock;
}
}
}

View File

@ -1,167 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import java.util.Arrays;
/**
* Cursor utilities for Room
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class CursorUtil {
/**
* Copies the given cursor into a in-memory cursor and then closes it.
* <p>
* This is useful for iterating over a cursor multiple times without the cost of JNI while
* reading or IO while filling the window at the expense of memory consumption.
*
* @param c the cursor to copy.
* @return a new cursor containing the same data as the given cursor.
*/
@NonNull
public static Cursor copyAndClose(@NonNull Cursor c) {
final MatrixCursor matrixCursor;
try {
matrixCursor = new MatrixCursor(c.getColumnNames(), c.getCount());
while (c.moveToNext()) {
final Object[] row = new Object[c.getColumnCount()];
for (int i = 0; i < c.getColumnCount(); i++) {
switch (c.getType(i)) {
case Cursor.FIELD_TYPE_NULL:
row[i] = null;
break;
case Cursor.FIELD_TYPE_INTEGER:
row[i] = c.getLong(i);
break;
case Cursor.FIELD_TYPE_FLOAT:
row[i] = c.getDouble(i);
break;
case Cursor.FIELD_TYPE_STRING:
row[i] = c.getString(i);
break;
case Cursor.FIELD_TYPE_BLOB:
row[i] = c.getBlob(i);
break;
default:
throw new IllegalStateException();
}
}
matrixCursor.addRow(row);
}
} finally {
c.close();
}
return matrixCursor;
}
/**
* Patches {@link Cursor#getColumnIndex(String)} to work around issues on older devices.
* If the column is not found, it retries with the specified name surrounded by backticks.
*
* @param c The cursor.
* @param name The name of the target column.
* @return The index of the column, or -1 if not found.
*/
public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) {
int index = c.getColumnIndex(name);
if (index >= 0) {
return index;
}
index = c.getColumnIndex("`" + name + "`");
if (index >= 0) {
return index;
}
return findColumnIndexBySuffix(c, name);
}
/**
* Patches {@link Cursor#getColumnIndexOrThrow(String)} to work around issues on older devices.
* If the column is not found, it retries with the specified name surrounded by backticks.
*
* @param c The cursor.
* @param name The name of the target column.
* @return The index of the column.
* @throws IllegalArgumentException if the column does not exist.
*/
public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) {
final int index = getColumnIndex(c, name);
if (index >= 0) {
return index;
}
String availableColumns = "";
try {
availableColumns = Arrays.toString(c.getColumnNames());
} catch (Exception e) {
Log.d("RoomCursorUtil", "Cannot collect column names for debug purposes", e);
}
throw new IllegalArgumentException("column '" + name
+ "' does not exist. Available columns: " + availableColumns);
}
/**
* Finds a column by name by appending `.` in front of it and checking by suffix match.
* Also checks for the version wrapped with `` (backticks).
* workaround for b/157261134 for API levels 25 and below
*
* e.g. "foo" will match "any.foo" and "`any.foo`"
*/
private static int findColumnIndexBySuffix(@NonNull Cursor cursor, @NonNull String name) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
// we need this workaround only on APIs < 26. So just return not found on newer APIs
return -1;
}
if (name.length() == 0) {
return -1;
}
final String[] columnNames = cursor.getColumnNames();
return findColumnIndexBySuffix(columnNames, name);
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static int findColumnIndexBySuffix(String[] columnNames, String name) {
String dotSuffix = "." + name;
String backtickSuffix = "." + name + "`";
for (int index = 0; index < columnNames.length; index++) {
String columnName = columnNames[index];
// do not check if column name is not long enough. 1 char for table name, 1 char for '.'
if (columnName.length() >= name.length() + 2) {
if (columnName.endsWith(dotSuffix)) {
return index;
} else if (columnName.charAt(0) == '`'
&& columnName.endsWith(backtickSuffix)) {
return index;
}
}
}
return -1;
}
private CursorUtil() {
}
}

View File

@ -1,243 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.AbstractWindowedCursor;
import android.database.Cursor;
import android.os.Build;
import android.os.CancellationSignal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.room.RoomDatabase;
import androidx.sqlite.db.SupportSQLiteCompat;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteQuery;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Database utilities for Room
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class DBUtil {
/**
* Performs the SQLiteQuery on the given database.
* <p>
* This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
* {@code true} and either the api level is below a certain threshold or the full result of the
* query does not fit in a single window.
*
* @param db The database to perform the query on.
* @param sqLiteQuery The query to perform.
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
* @return Result of the query.
*
* @deprecated This is only used in the generated code and shouldn't be called directly.
*/
@Deprecated
@NonNull
public static Cursor query(RoomDatabase db, SupportSQLiteQuery sqLiteQuery, boolean maybeCopy) {
return query(db, sqLiteQuery, maybeCopy, null);
}
/**
* Performs the SQLiteQuery on the given database.
* <p>
* This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
* {@code true} and either the api level is below a certain threshold or the full result of the
* query does not fit in a single window.
*
* @param db The database to perform the query on.
* @param sqLiteQuery The query to perform.
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
* @param signal The cancellation signal to be attached to the query.
* @return Result of the query.
*/
@NonNull
public static Cursor query(@NonNull RoomDatabase db, @NonNull SupportSQLiteQuery sqLiteQuery,
boolean maybeCopy, @Nullable CancellationSignal signal) {
final Cursor cursor = db.query(sqLiteQuery, signal);
if (maybeCopy && cursor instanceof AbstractWindowedCursor) {
AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
int rowsInCursor = windowedCursor.getCount(); // Should fill the window.
int rowsInWindow;
if (windowedCursor.hasWindow()) {
rowsInWindow = windowedCursor.getWindow().getNumRows();
} else {
rowsInWindow = rowsInCursor;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
return CursorUtil.copyAndClose(windowedCursor);
}
}
return cursor;
}
/**
* Drops all FTS content sync triggers created by Room.
* <p>
* FTS content sync triggers created by Room are those that are found in the sqlite_master table
* who's names start with 'room_fts_content_sync_'.
*
* @param db The database.
*/
public static void dropFtsSyncTriggers(SupportSQLiteDatabase db) {
List<String> existingTriggers = new ArrayList<>();
Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'");
//noinspection TryFinallyCanBeTryWithResources
try {
while (cursor.moveToNext()) {
existingTriggers.add(cursor.getString(0));
}
} finally {
cursor.close();
}
for (String triggerName : existingTriggers) {
if (triggerName.startsWith("room_fts_content_sync_")) {
db.execSQL("DROP TRIGGER IF EXISTS " + triggerName);
}
}
}
/**
* Checks for foreign key violations by executing a PRAGMA foreign_key_check.
*/
public static void foreignKeyCheck(@NonNull SupportSQLiteDatabase db,
@NonNull String tableName) {
Cursor cursor = db.query("PRAGMA foreign_key_check(`" + tableName + "`)");
try {
if (cursor.getCount() > 0) {
String errorMsg = processForeignKeyCheckFailure(cursor);
throw new IllegalStateException(errorMsg);
}
} finally {
cursor.close();
}
}
/**
* Reads the user version number out of the database header from the given file.
*
* @param databaseFile the database file.
* @return the database version
* @throws IOException if something goes wrong reading the file, such as bad database header or
* missing permissions.
*
* @see <a href="https://www.sqlite.org/fileformat.html#user_version_number">User Version
* Number</a>.
*/
public static int readVersion(@NonNull File databaseFile) throws IOException {
FileChannel input = null;
try {
ByteBuffer buffer = ByteBuffer.allocate(4);
input = new FileInputStream(databaseFile).getChannel();
input.tryLock(60, 4, true);
input.position(60);
int read = input.read(buffer);
if (read != 4) {
throw new IOException("Bad database header, unable to read 4 bytes at offset 60");
}
buffer.rewind();
return buffer.getInt(); // ByteBuffer is big-endian by default
} finally {
if (input != null) {
input.close();
}
}
}
/**
* CancellationSignal is only available from API 16 on. This function will create a new
* instance of the Cancellation signal only if the current API > 16.
*
* @return A new instance of CancellationSignal or null.
*/
@Nullable
public static CancellationSignal createCancellationSignal() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return SupportSQLiteCompat.Api16Impl.createCancellationSignal();
}
return null;
}
/**
* Converts the {@link Cursor} returned in case of a foreign key violation into a detailed
* error message for debugging.
* <p>
* The foreign_key_check pragma returns one row output for each foreign key violation.
* <p>
* The cursor received has four columns for each row output. The first column is the name of
* the child table. The second column is the rowId of the row that contains the foreign key
* violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the
* name of the parent table. The fourth column is the index of the specific foreign key
* constraint that failed.
*
* @param cursor Cursor containing information regarding the FK violation
* @return Error message generated containing debugging information
*/
private static String processForeignKeyCheckFailure(Cursor cursor) {
int rowCount = cursor.getCount();
String childTableName = null;
Map<String, String> fkParentTables = new HashMap<>();
while (cursor.moveToNext()) {
if (childTableName == null) {
childTableName = cursor.getString(0);
}
String constraintIndex = cursor.getString(3);
if (!fkParentTables.containsKey(constraintIndex)) {
fkParentTables.put(constraintIndex, cursor.getString(2));
}
}
StringBuilder sb = new StringBuilder();
sb.append("Foreign key violation(s) detected in '")
.append(childTableName).append("'.\n");
sb.append("Number of different violations discovered: ")
.append(fkParentTables.keySet().size()).append("\n");
sb.append("Number of rows in violation: ")
.append(rowCount).append("\n");
sb.append("Violation(s) detected in the following constraint(s):\n");
for (Map.Entry<String, String> entry : fkParentTables.entrySet()) {
sb.append("\tParent Table = ")
.append(entry.getValue());
sb.append(", Foreign Key Constraint Index = ")
.append(entry.getKey()).append("\n");
}
return sb.toString();
}
private DBUtil() {
}
}

View File

@ -1,629 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.room.migration.AutoMigrationSpec;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.File;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
/**
* Configuration class for a {@link RoomDatabase}.
*/
@SuppressWarnings("WeakerAccess")
public class DatabaseConfiguration {
/**
* The factory to use to access the database.
*/
@NonNull
public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
/**
* The context to use while connecting to the database.
*/
@NonNull
public final Context context;
/**
* The name of the database file or null if it is an in-memory database.
*/
@Nullable
public final String name;
/**
* Collection of available migrations.
*/
@NonNull
public final RoomDatabase.MigrationContainer migrationContainer;
@Nullable
public final List<RoomDatabase.Callback> callbacks;
@Nullable
public final RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback;
@NonNull
public final List<Object> typeConverters;
@NonNull
public final List<AutoMigrationSpec> autoMigrationSpecs;
/**
* Whether Room should throw an exception for queries run on the main thread.
*/
public final boolean allowMainThreadQueries;
/**
* The journal mode for this database.
*/
public final RoomDatabase.JournalMode journalMode;
/**
* The Executor used to execute asynchronous queries.
*/
@NonNull
public final Executor queryExecutor;
/**
* The Executor used to execute asynchronous transactions.
*/
@NonNull
public final Executor transactionExecutor;
/**
* If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and
* synchronized with other instances of the same {@link RoomDatabase} file, including those
* in a separate process.
*/
public final boolean multiInstanceInvalidation;
/**
* Intent that should be bound to acquire the invalidation service or {@code null} if not used.
*
* @see {@link #multiInstanceInvalidation}
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public final Intent multiInstanceInvalidationServiceIntent;
/**
* If true, Room should crash if a migration is missing.
*/
public final boolean requireMigration;
/**
* If true, Room should perform a destructive migration when downgrading without an available
* migration.
*/
public final boolean allowDestructiveMigrationOnDowngrade;
/**
* The collection of schema versions from which migrations aren't required.
*/
private final Set<Integer> mMigrationNotRequiredFrom;
/**
* The assets path to a pre-packaged database to copy from.
*/
@Nullable
public final String copyFromAssetPath;
/**
* The pre-packaged database file to copy from.
*/
@Nullable
public final File copyFromFile;
/**
* The callable to get the input stream from which a pre-package database file will be copied
* from.
*/
@Nullable
public final Callable<InputStream> copyFromInputStream;
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param requireMigration True if Room should require a valid migration if version changes,
* instead of recreating the tables.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
*
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<androidx.room.RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
boolean requireMigration,
@Nullable Set<Integer> migrationNotRequiredFrom) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false,
requireMigration, false, migrationNotRequiredFrom, null, null, null, null, null,
null);
}
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
*
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, null, null, null, null, null, null);
}
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
*
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, null, null, null, null);
}
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
*
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
@NonNull RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream,
null, null, null);
}
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
*
* @hide
*/
@Deprecated
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
@NonNull RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream,
prepackagedDatabaseCallback, null, null);
}
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
* @param typeConverters The type converters.
*
* @hide
*/
@Deprecated
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
@NonNull RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback,
@Nullable List<Object> typeConverters) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream,
prepackagedDatabaseCallback, typeConverters, null);
}
/**
* Creates a database configuration with the given values.
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
* @param typeConverters The type converters.
* @param autoMigrationSpecs The auto migration specs.
*
* @hide
*/
@Deprecated
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
@NonNull RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback,
@Nullable List<Object> typeConverters,
@Nullable List<AutoMigrationSpec> autoMigrationSpecs) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation ? new Intent(context,
MultiInstanceInvalidationService.class) : null,
requireMigration, allowDestructiveMigrationOnDowngrade, migrationNotRequiredFrom,
copyFromAssetPath, copyFromFile, copyFromInputStream, prepackagedDatabaseCallback,
typeConverters, autoMigrationSpecs);
}
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidationServiceIntent The intent to use to bind to the
* invalidation service or {@code null} if not
* used.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
* @param typeConverters The type converters.
* @param autoMigrationSpecs The auto migration specs.
*
* @hide
*/
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
@NonNull RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
@Nullable Intent multiInstanceInvalidationServiceIntent,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback,
@Nullable List<Object> typeConverters,
@Nullable List<AutoMigrationSpec> autoMigrationSpecs) {
this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
this.context = context;
this.name = name;
this.migrationContainer = migrationContainer;
this.callbacks = callbacks;
this.allowMainThreadQueries = allowMainThreadQueries;
this.journalMode = journalMode;
this.queryExecutor = queryExecutor;
this.transactionExecutor = transactionExecutor;
this.multiInstanceInvalidationServiceIntent =
multiInstanceInvalidationServiceIntent;
this.multiInstanceInvalidation = multiInstanceInvalidationServiceIntent != null;
this.requireMigration = requireMigration;
this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade;
this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
this.copyFromAssetPath = copyFromAssetPath;
this.copyFromFile = copyFromFile;
this.copyFromInputStream = copyFromInputStream;
this.prepackagedDatabaseCallback = prepackagedDatabaseCallback;
this.typeConverters = typeConverters == null ? Collections.emptyList() : typeConverters;
this.autoMigrationSpecs = autoMigrationSpecs == null
? Collections.emptyList() : autoMigrationSpecs;
}
/**
* Returns whether a migration is required from the specified version.
*
* @param version The schema version.
* @return True if a valid migration is required, false otherwise.
*
* @deprecated Use {@link #isMigrationRequired(int, int)} which takes
* {@link #allowDestructiveMigrationOnDowngrade} into account.
*/
@Deprecated
public boolean isMigrationRequiredFrom(int version) {
return isMigrationRequired(version, version + 1);
}
/**
* Returns whether a migration is required between two versions.
*
* @param fromVersion The old schema version.
* @param toVersion The new schema version.
* @return True if a valid migration is required, false otherwise.
*/
public boolean isMigrationRequired(int fromVersion, int toVersion) {
// Migrations are not required if its a downgrade AND destructive migration during downgrade
// has been allowed.
final boolean isDowngrade = fromVersion > toVersion;
if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
return false;
}
// Migrations are required between the two versions if we generally require migrations
// AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
// exceptions.
return requireMigration
&& (mMigrationNotRequiredFrom == null
|| !mMigrationNotRequiredFrom.contains(fromVersion));
}
}

View File

@ -0,0 +1,680 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import androidx.annotation.RestrictTo
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteOpenHelper
import java.io.File
import java.io.InputStream
import java.util.concurrent.Callable
import java.util.concurrent.Executor
/**
* Configuration class for a [RoomDatabase].
*/
@Suppress("UNUSED_PARAMETER")
open class DatabaseConfiguration @SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
constructor(
/**
* The context to use while connecting to the database.
*/
@JvmField
val context: Context,
/**
* The name of the database file or null if it is an in-memory database.
*/
@JvmField
val name: String?,
/**
* The factory to use to access the database.
*/
@JvmField
val sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
/**
* Collection of available migrations.
*/
@JvmField
val migrationContainer: RoomDatabase.MigrationContainer,
@JvmField
val callbacks: List<RoomDatabase.Callback>?,
/**
* Whether Room should throw an exception for queries run on the main thread.
*/
@JvmField
val allowMainThreadQueries: Boolean,
/**
* The journal mode for this database.
*/
@JvmField
val journalMode: RoomDatabase.JournalMode,
/**
* The Executor used to execute asynchronous queries.
*/
@JvmField
val queryExecutor: Executor,
/**
* The Executor used to execute asynchronous transactions.
*/
@JvmField
val transactionExecutor: Executor,
/**
* Intent that should be bound to acquire the invalidation service or `null` if not used.
*
* @see [multiInstanceInvalidation]
*/
@field:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@JvmField
val multiInstanceInvalidationServiceIntent: Intent?,
@JvmField
val requireMigration: Boolean,
@JvmField
val allowDestructiveMigrationOnDowngrade: Boolean,
private val migrationNotRequiredFrom: Set<Int>?,
@JvmField
val copyFromAssetPath: String?,
@JvmField
val copyFromFile: File?,
@JvmField
val copyFromInputStream: Callable<InputStream>?,
@JvmField
val prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?,
@JvmField
val typeConverters: List<Any>,
@JvmField
val autoMigrationSpecs: List<AutoMigrationSpec>
) {
/**
* If true, table invalidation in an instance of [RoomDatabase] is broadcast and
* synchronized with other instances of the same [RoomDatabase] file, including those
* in a separate process.
*/
@JvmField
val multiInstanceInvalidation: Boolean = multiInstanceInvalidationServiceIntent != null
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param requireMigration True if Room should require a valid migration if version changes,
* instead of recreating the tables.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated(
"This constructor is deprecated.",
ReplaceWith("DatabaseConfiguration(Context, String, " +
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
"List, List)")
)
constructor(
context: Context,
name: String?,
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
migrationContainer: RoomDatabase.MigrationContainer,
callbacks: List<RoomDatabase.Callback>?,
allowMainThreadQueries: Boolean,
journalMode: RoomDatabase.JournalMode,
queryExecutor: Executor,
requireMigration: Boolean,
migrationNotRequiredFrom: Set<Int>?
) : this(
context = context,
name = name,
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
migrationContainer = migrationContainer,
callbacks = callbacks,
allowMainThreadQueries = allowMainThreadQueries,
journalMode = journalMode,
queryExecutor = queryExecutor,
transactionExecutor = queryExecutor,
multiInstanceInvalidationServiceIntent = null,
allowDestructiveMigrationOnDowngrade = false,
requireMigration = requireMigration,
migrationNotRequiredFrom = migrationNotRequiredFrom,
copyFromAssetPath = null,
copyFromFile = null,
prepackagedDatabaseCallback = null,
copyFromInputStream = null,
typeConverters = emptyList(),
autoMigrationSpecs = emptyList()
)
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated(
"This constructor is deprecated.",
ReplaceWith("DatabaseConfiguration(Context, String, " +
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
"List, List)")
)
constructor(
context: Context,
name: String?,
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
migrationContainer: RoomDatabase.MigrationContainer,
callbacks: List<RoomDatabase.Callback>?,
allowMainThreadQueries: Boolean,
journalMode: RoomDatabase.JournalMode,
queryExecutor: Executor,
transactionExecutor: Executor,
multiInstanceInvalidation: Boolean,
requireMigration: Boolean,
allowDestructiveMigrationOnDowngrade: Boolean,
migrationNotRequiredFrom: Set<Int>?
) : this(
context = context,
name = name,
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
migrationContainer = migrationContainer,
callbacks = callbacks,
allowMainThreadQueries = allowMainThreadQueries,
journalMode = journalMode,
queryExecutor = queryExecutor,
transactionExecutor = transactionExecutor,
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
context,
MultiInstanceInvalidationService::class.java
) else null,
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
requireMigration = requireMigration,
migrationNotRequiredFrom = migrationNotRequiredFrom,
copyFromAssetPath = null,
copyFromFile = null,
prepackagedDatabaseCallback = null,
copyFromInputStream = null,
typeConverters = emptyList(),
autoMigrationSpecs = emptyList()
)
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated(
"This constructor is deprecated.",
ReplaceWith("DatabaseConfiguration(Context, String, " +
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
"List, List)")
)
constructor(
context: Context,
name: String?,
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
migrationContainer: RoomDatabase.MigrationContainer,
callbacks: List<RoomDatabase.Callback>?,
allowMainThreadQueries: Boolean,
journalMode: RoomDatabase.JournalMode,
queryExecutor: Executor,
transactionExecutor: Executor,
multiInstanceInvalidation: Boolean,
requireMigration: Boolean,
allowDestructiveMigrationOnDowngrade: Boolean,
migrationNotRequiredFrom: Set<Int>?,
copyFromAssetPath: String?,
copyFromFile: File?
) : this(
context = context,
name = name,
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
migrationContainer = migrationContainer,
callbacks = callbacks,
allowMainThreadQueries = allowMainThreadQueries,
journalMode = journalMode,
queryExecutor = queryExecutor,
transactionExecutor = transactionExecutor,
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
context,
MultiInstanceInvalidationService::class.java
) else null,
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
requireMigration = requireMigration,
migrationNotRequiredFrom = migrationNotRequiredFrom,
copyFromAssetPath = copyFromAssetPath,
copyFromFile = copyFromFile,
prepackagedDatabaseCallback = null,
copyFromInputStream = null,
typeConverters = emptyList(),
autoMigrationSpecs = emptyList()
)
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated(
"This constructor is deprecated.",
ReplaceWith("DatabaseConfiguration(Context, String, " +
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
"List, List)")
)
constructor(
context: Context,
name: String?,
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
migrationContainer: RoomDatabase.MigrationContainer,
callbacks: List<RoomDatabase.Callback>?,
allowMainThreadQueries: Boolean,
journalMode: RoomDatabase.JournalMode,
queryExecutor: Executor,
transactionExecutor: Executor,
multiInstanceInvalidation: Boolean,
requireMigration: Boolean,
allowDestructiveMigrationOnDowngrade: Boolean,
migrationNotRequiredFrom: Set<Int>?,
copyFromAssetPath: String?,
copyFromFile: File?,
copyFromInputStream: Callable<InputStream>?
) : this(
context = context,
name = name,
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
migrationContainer = migrationContainer,
callbacks = callbacks,
allowMainThreadQueries = allowMainThreadQueries,
journalMode = journalMode,
queryExecutor = queryExecutor,
transactionExecutor = transactionExecutor,
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
context,
MultiInstanceInvalidationService::class.java
) else null,
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
requireMigration = requireMigration,
migrationNotRequiredFrom = migrationNotRequiredFrom,
copyFromAssetPath = copyFromAssetPath,
copyFromFile = copyFromFile,
prepackagedDatabaseCallback = null,
copyFromInputStream = copyFromInputStream,
typeConverters = emptyList(),
autoMigrationSpecs = emptyList()
)
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
*
*/
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated(
"This constructor is deprecated.",
ReplaceWith("DatabaseConfiguration(Context, String, " +
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
"List, List)")
)
constructor(
context: Context,
name: String?,
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
migrationContainer: RoomDatabase.MigrationContainer,
callbacks: List<RoomDatabase.Callback>?,
allowMainThreadQueries: Boolean,
journalMode: RoomDatabase.JournalMode,
queryExecutor: Executor,
transactionExecutor: Executor,
multiInstanceInvalidation: Boolean,
requireMigration: Boolean,
allowDestructiveMigrationOnDowngrade: Boolean,
migrationNotRequiredFrom: Set<Int>?,
copyFromAssetPath: String?,
copyFromFile: File?,
copyFromInputStream: Callable<InputStream>?,
prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?
) : this(
context = context,
name = name,
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
migrationContainer = migrationContainer,
callbacks = callbacks,
allowMainThreadQueries = allowMainThreadQueries,
journalMode = journalMode,
queryExecutor = queryExecutor,
transactionExecutor = transactionExecutor,
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
context,
MultiInstanceInvalidationService::class.java
) else null,
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
requireMigration = requireMigration,
migrationNotRequiredFrom = migrationNotRequiredFrom,
copyFromAssetPath = copyFromAssetPath,
copyFromFile = copyFromFile,
prepackagedDatabaseCallback = prepackagedDatabaseCallback,
copyFromInputStream = copyFromInputStream,
typeConverters = emptyList(),
autoMigrationSpecs = emptyList()
)
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
* @param typeConverters The type converters.
*
*/
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated(
"This constructor is deprecated.",
ReplaceWith("DatabaseConfiguration(Context, String, " +
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
"List, List)")
)
constructor(
context: Context,
name: String?,
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
migrationContainer: RoomDatabase.MigrationContainer,
callbacks: List<RoomDatabase.Callback>?,
allowMainThreadQueries: Boolean,
journalMode: RoomDatabase.JournalMode,
queryExecutor: Executor,
transactionExecutor: Executor,
multiInstanceInvalidation: Boolean,
requireMigration: Boolean,
allowDestructiveMigrationOnDowngrade: Boolean,
migrationNotRequiredFrom: Set<Int>?,
copyFromAssetPath: String?,
copyFromFile: File?,
copyFromInputStream: Callable<InputStream>?,
prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?,
typeConverters: List<Any>
) : this(
context = context,
name = name,
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
migrationContainer = migrationContainer,
callbacks = callbacks,
allowMainThreadQueries = allowMainThreadQueries,
journalMode = journalMode,
queryExecutor = queryExecutor,
transactionExecutor = transactionExecutor,
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
context,
MultiInstanceInvalidationService::class.java
) else null,
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
requireMigration = requireMigration,
migrationNotRequiredFrom = migrationNotRequiredFrom,
copyFromAssetPath = copyFromAssetPath,
copyFromFile = copyFromFile,
prepackagedDatabaseCallback = prepackagedDatabaseCallback,
copyFromInputStream = copyFromInputStream,
typeConverters = typeConverters,
autoMigrationSpecs = emptyList()
)
/**
* Creates a database configuration with the given values.
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
* @param sqliteOpenHelperFactory The open helper factory to use.
* @param migrationContainer The migration container for migrations.
* @param callbacks The list of callbacks for database events.
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
* @param queryExecutor The Executor used to execute asynchronous queries.
* @param transactionExecutor The Executor used to execute asynchronous transactions.
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
* @param requireMigration True if Room should require a valid migration if version changes,
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
* migration is supplied during a downgrade.
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
* aren't required.
* @param copyFromAssetPath The assets path to the pre-packaged database.
* @param copyFromFile The pre-packaged database file.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
* @param typeConverters The type converters.
* @param autoMigrationSpecs The auto migration specs.
*
*/
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Deprecated(
"This constructor is deprecated.",
ReplaceWith("DatabaseConfiguration(Context, String, " +
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
"List, List)")
)
constructor(
context: Context,
name: String?,
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
migrationContainer: RoomDatabase.MigrationContainer,
callbacks: List<RoomDatabase.Callback>?,
allowMainThreadQueries: Boolean,
journalMode: RoomDatabase.JournalMode,
queryExecutor: Executor,
transactionExecutor: Executor,
multiInstanceInvalidation: Boolean,
requireMigration: Boolean,
allowDestructiveMigrationOnDowngrade: Boolean,
migrationNotRequiredFrom: Set<Int>?,
copyFromAssetPath: String?,
copyFromFile: File?,
copyFromInputStream: Callable<InputStream>?,
prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?,
typeConverters: List<Any>,
autoMigrationSpecs: List<AutoMigrationSpec>
) : this(
context = context,
name = name,
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
migrationContainer = migrationContainer,
callbacks = callbacks,
allowMainThreadQueries = allowMainThreadQueries,
journalMode = journalMode,
queryExecutor = queryExecutor,
transactionExecutor = transactionExecutor,
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
context,
MultiInstanceInvalidationService::class.java
) else null,
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
requireMigration = requireMigration,
migrationNotRequiredFrom = migrationNotRequiredFrom,
copyFromAssetPath = copyFromAssetPath,
copyFromFile = copyFromFile,
prepackagedDatabaseCallback = null,
copyFromInputStream = copyFromInputStream,
typeConverters = typeConverters,
autoMigrationSpecs = autoMigrationSpecs
)
/**
* Returns whether a migration is required from the specified version.
*
* @param version The schema version.
* @return True if a valid migration is required, false otherwise.
*
*/
@Deprecated(
"""Use [isMigrationRequired(int, int)] which takes
[allowDestructiveMigrationOnDowngrade] into account.""",
ReplaceWith("isMigrationRequired(version, version + 1)")
)
open fun isMigrationRequiredFrom(version: Int): Boolean {
return isMigrationRequired(version, version + 1)
}
/**
* Returns whether a migration is required between two versions.
*
* @param fromVersion The old schema version.
* @param toVersion The new schema version.
* @return True if a valid migration is required, false otherwise.
*/
open fun isMigrationRequired(fromVersion: Int, toVersion: Int): Boolean {
// Migrations are not required if its a downgrade AND destructive migration during downgrade
// has been allowed.
val isDowngrade = fromVersion > toVersion
if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
return false
} else {
// Migrations are required between the two versions if we generally require migrations
// AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
// exceptions.
return requireMigration && (migrationNotRequiredFrom == null ||
!migrationNotRequiredFrom.contains(fromVersion))
}
}
}

View File

@ -13,24 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
package androidx.room
import androidx.annotation.NonNull;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.SupportSQLiteOpenHelper
/**
* Package private interface for OpenHelpers which delegate to other open helpers.
* Internal interface for OpenHelpers which delegate to other open helpers.
*
* TODO(b/175612939): delete this interface once implementations are merged.
*/
interface DelegatingOpenHelper {
internal interface DelegatingOpenHelper {
/**
* Returns the delegate open helper (which may itself be a DelegatingOpenHelper) so
* The delegate open helper (which may itself be a DelegatingOpenHelper) so
* configurations on specific instances can be applied.
*
* @return the delegate
*/
@NonNull
SupportSQLiteOpenHelper getDelegate();
val delegate: SupportSQLiteOpenHelper
}

View File

@ -1,17 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file exists to trick AGP/lint to work around b/234865137

View File

@ -13,49 +13,40 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
package androidx.room;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteStatement;
import androidx.annotation.RestrictTo
import androidx.sqlite.db.SupportSQLiteStatement
/**
* Implementations of this class knows how to delete or update a particular entity.
* <p>
*
* This is an internal library class and all of its implementations are auto-generated.
*
* @param <T> The type parameter of the entity to be deleted
* @hide
*/
* @constructor Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the
* given database.
*
* @param T The type parameter of the entity to be deleted
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteStatement {
/**
* Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given
* database.
*
* @param database The database to delete / update the item in.
*/
public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
super(database);
}
abstract class EntityDeletionOrUpdateAdapter<T> (
database: RoomDatabase
) : SharedSQLiteStatement(database) {
/**
* Create the deletion or update query
*
* @return An SQL query that can delete or update instances of T.
*/
@Override
protected abstract String createQuery();
abstract override fun createQuery(): String
/**
* Binds the entity into the given statement.
*
* @param statement The SQLite statement that prepared for the query returned from
* createQuery.
* createQuery.
* @param entity The entity of type T.
*/
protected abstract void bind(SupportSQLiteStatement statement, T entity);
protected abstract fun bind(statement: SupportSQLiteStatement, entity: T)
/**
* Deletes or updates the given entities in the database and returns the affected row count.
@ -63,13 +54,13 @@ public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteState
* @param entity The entity to delete or update
* @return The number of affected rows
*/
public final int handle(T entity) {
final SupportSQLiteStatement stmt = acquire();
try {
bind(stmt, entity);
return stmt.executeUpdateDelete();
fun handle(entity: T): Int {
val stmt: SupportSQLiteStatement = acquire()
return try {
bind(stmt, entity)
stmt.executeUpdateDelete()
} finally {
release(stmt);
release(stmt)
}
}
@ -79,17 +70,17 @@ public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteState
* @param entities Entities to delete or update
* @return The number of affected rows
*/
public final int handleMultiple(Iterable<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
int total = 0;
for (T entity : entities) {
bind(stmt, entity);
total += stmt.executeUpdateDelete();
fun handleMultiple(entities: Iterable<T>): Int {
val stmt: SupportSQLiteStatement = acquire()
return try {
var total = 0
entities.forEach { entity ->
bind(stmt, entity)
total += stmt.executeUpdateDelete()
}
return total;
total
} finally {
release(stmt);
release(stmt)
}
}
@ -99,17 +90,17 @@ public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteState
* @param entities Entities to delete or update
* @return The number of affected rows
*/
public final int handleMultiple(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
int total = 0;
for (T entity : entities) {
bind(stmt, entity);
total += stmt.executeUpdateDelete();
fun handleMultiple(entities: Array<out T>): Int {
val stmt: SupportSQLiteStatement = acquire()
return try {
var total = 0
entities.forEach { entity ->
bind(stmt, entity)
total += stmt.executeUpdateDelete()
}
return total;
total
} finally {
release(stmt);
release(stmt)
}
}
}

View File

@ -1,251 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Implementations of this class knows how to insert a particular entity.
* <p>
* This is an internal library class and all of its implementations are auto-generated.
*
* @param <T> The type parameter of the entity to be inserted
* @hide
*/
@SuppressWarnings({"WeakerAccess", "unused"})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public abstract class EntityInsertionAdapter<T> extends SharedSQLiteStatement {
/**
* Creates an InsertionAdapter that can insert the entity type T into the given database.
*
* @param database The database to insert into.
*/
public EntityInsertionAdapter(RoomDatabase database) {
super(database);
}
/**
* Binds the entity into the given statement.
*
* @param statement The SQLite statement that prepared for the query returned from
* createInsertQuery.
* @param entity The entity of type T.
*/
protected abstract void bind(SupportSQLiteStatement statement, T entity);
/**
* Inserts the entity into the database.
*
* @param entity The entity to insert
*/
public final void insert(T entity) {
final SupportSQLiteStatement stmt = acquire();
try {
bind(stmt, entity);
stmt.executeInsert();
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database.
*
* @param entities Entities to insert
*/
public final void insert(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
for (T entity : entities) {
bind(stmt, entity);
stmt.executeInsert();
}
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database.
*
* @param entities Entities to insert
*/
public final void insert(Iterable<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
for (T entity : entities) {
bind(stmt, entity);
stmt.executeInsert();
}
} finally {
release(stmt);
}
}
/**
* Inserts the given entity into the database and returns the row id.
*
* @param entity The entity to insert
* @return The SQLite row id or -1 if no row is inserted
*/
public final long insertAndReturnId(T entity) {
final SupportSQLiteStatement stmt = acquire();
try {
bind(stmt, entity);
return stmt.executeInsert();
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final long[] insertAndReturnIdsArray(Collection<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final long[] result = new long[entities.size()];
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result[index] = stmt.executeInsert();
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final long[] insertAndReturnIdsArray(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final long[] result = new long[entities.length];
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result[index] = stmt.executeInsert();
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final Long[] insertAndReturnIdsArrayBox(Collection<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final Long[] result = new Long[entities.size()];
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result[index] = stmt.executeInsert();
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final Long[] insertAndReturnIdsArrayBox(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final Long[] result = new Long[entities.length];
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result[index] = stmt.executeInsert();
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final List<Long> insertAndReturnIdsList(T[] entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final List<Long> result = new ArrayList<>(entities.length);
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result.add(index, stmt.executeInsert());
index++;
}
return result;
} finally {
release(stmt);
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
public final List<Long> insertAndReturnIdsList(Collection<? extends T> entities) {
final SupportSQLiteStatement stmt = acquire();
try {
final List<Long> result = new ArrayList<>(entities.size());
int index = 0;
for (T entity : entities) {
bind(stmt, entity);
result.add(index, stmt.executeInsert());
index++;
}
return result;
} finally {
release(stmt);
}
}
}

View File

@ -0,0 +1,228 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.annotation.RestrictTo
import androidx.sqlite.db.SupportSQLiteStatement
/**
* Implementations of this class knows how to insert a particular entity.
*
* This is an internal library class and all of its implementations are auto-generated.
*
* @constructor Creates an InsertionAdapter that can insert the entity type T into the given
* database.
*
* @param T The type parameter of the entity to be inserted
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
abstract class EntityInsertionAdapter<T>(database: RoomDatabase) : SharedSQLiteStatement(database) {
/**
* Binds the entity into the given statement.
*
* @param statement The SQLite statement that prepared for the query returned from
* createInsertQuery.
* @param entity The entity of type T.
*/
protected abstract fun bind(statement: SupportSQLiteStatement, entity: T)
/**
* Inserts the entity into the database.
*
* @param entity The entity to insert
*/
fun insert(entity: T) {
val stmt: SupportSQLiteStatement = acquire()
try {
bind(stmt, entity)
stmt.executeInsert()
} finally {
release(stmt)
}
}
/**
* Inserts the given entities into the database.
*
* @param entities Entities to insert
*/
fun insert(entities: Array<out T>) {
val stmt: SupportSQLiteStatement = acquire()
try {
entities.forEach { entity ->
bind(stmt, entity)
stmt.executeInsert()
}
} finally {
release(stmt)
}
}
/**
* Inserts the given entities into the database.
*
* @param entities Entities to insert
*/
fun insert(entities: Iterable<T>) {
val stmt: SupportSQLiteStatement = acquire()
try {
entities.forEach { entity ->
bind(stmt, entity)
stmt.executeInsert()
}
} finally {
release(stmt)
}
}
/**
* Inserts the given entity into the database and returns the row id.
*
* @param entity The entity to insert
* @return The SQLite row id or -1 if no row is inserted
*/
fun insertAndReturnId(entity: T): Long {
val stmt: SupportSQLiteStatement = acquire()
return try {
bind(stmt, entity)
stmt.executeInsert()
} finally {
release(stmt)
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
fun insertAndReturnIdsArray(entities: Collection<T>): LongArray {
val stmt: SupportSQLiteStatement = acquire()
return try {
val result = LongArray(entities.size)
entities.forEachIndexed { index, entity ->
bind(stmt, entity)
result[index] = stmt.executeInsert()
}
result
} finally {
release(stmt)
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
fun insertAndReturnIdsArray(entities: Array<out T>): LongArray {
val stmt: SupportSQLiteStatement = acquire()
return try {
val result = LongArray(entities.size)
entities.forEachIndexed { index, entity ->
bind(stmt, entity)
result[index] = stmt.executeInsert()
}
result
} finally {
release(stmt)
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
fun insertAndReturnIdsArrayBox(entities: Collection<T>): Array<out Long> {
val stmt: SupportSQLiteStatement = acquire()
val iterator = entities.iterator()
return try {
val result = Array(entities.size) {
val entity = iterator.next()
bind(stmt, entity)
stmt.executeInsert()
}
result
} finally {
release(stmt)
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
fun insertAndReturnIdsArrayBox(entities: Array<out T>): Array<out Long> {
val stmt: SupportSQLiteStatement = acquire()
val iterator = entities.iterator()
return try {
val result = Array(entities.size) {
val entity = iterator.next()
bind(stmt, entity)
stmt.executeInsert()
}
result
} finally {
release(stmt)
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
fun insertAndReturnIdsList(entities: Array<out T>): List<Long> {
val stmt: SupportSQLiteStatement = acquire()
return try {
buildList {
entities.forEach { entity ->
bind(stmt, entity)
add(stmt.executeInsert())
}
}
} finally {
release(stmt)
}
}
/**
* Inserts the given entities into the database and returns the row ids.
*
* @param entities Entities to insert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
fun insertAndReturnIdsList(entities: Collection<T>): List<Long> {
val stmt: SupportSQLiteStatement = acquire()
return try {
buildList {
entities.forEach { entity ->
bind(stmt, entity)
add(stmt.executeInsert())
}
}
} finally {
release(stmt)
}
}
}

View File

@ -0,0 +1,223 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.database.sqlite.SQLiteConstraintException
import androidx.annotation.RestrictTo
/**
* The error code defined by SQLite Library for SQLITE_CONSTRAINT_PRIMARYKEY error
* Only used by android of version newer than 19.
*/
private const val SQLITE_CONSTRAINT_PRIMARYKEY = "1555"
/**
* The error code defined by SQLite Library for SQLITE_CONSTRAINT_UNIQUE error.
*/
private const val SQLITE_CONSTRAINT_UNIQUE = "2067"
/**
* For android of version below and including 19, use error message instead of
* error code to check
*/
private const val ErrorMsg = "unique"
/**
* This class knows how to insert an entity. When the insertion fails
* due to a unique constraint conflict (i.e. primary key conflict),
* it will perform an update.
*
* @constructor Creates an EntityUpsertionAdapter that can upsert entity of type T
* into the database using the given insertionAdapter to perform insertion and
* updateAdapter to perform update when the insertion fails
*
* @param T the type param of the entity to be upserted
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class EntityUpsertionAdapter<T>(
private val insertionAdapter: EntityInsertionAdapter<T>,
private val updateAdapter: EntityDeletionOrUpdateAdapter<T>
) {
/**
* Inserts the entity into the database. If a constraint exception is thrown
* i.e. a primary key conflict, update the existing entity.
*
* @param entity The entity to insert
*/
fun upsert(entity: T) {
try {
insertionAdapter.insert(entity)
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entity)
}
}
/**
* Upserts (insert or update) the given entities into the database.
* For each entity, insert if it is not already in the database
* update if there is a constraint conflict.
*
* @param entities array of entities to upsert
*/
fun upsert(entities: Array<out T>) {
entities.forEach { entity ->
try {
insertionAdapter.insert(entity)
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entity)
}
}
}
fun upsert(entities: Iterable<T>) {
entities.forEach { entity ->
try {
insertionAdapter.insert(entity)
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entity)
}
}
}
/**
* Upserts the given entity into the database and returns the row id.
* If the insertion failed, update the existing entity and return -1.
*
* @param entity The entity to upsert
* @return The SQLite row id or -1 if the insertion failed and update
* is performed
*/
fun upsertAndReturnId(entity: T): Long {
return try {
insertionAdapter.insertAndReturnId(entity)
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entity)
-1
}
}
/**
* Upserts the given entities into the database and returns the row ids.
*
* @param entities Entities to upsert
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
*/
fun upsertAndReturnIdsArray(entities: Array<out T>): LongArray {
return LongArray(entities.size) { index ->
try {
insertionAdapter.insertAndReturnId(entities[index])
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entities[index])
-1
}
}
}
fun upsertAndReturnIdsArray(entities: Collection<T>): LongArray {
val iterator = entities.iterator()
return LongArray(entities.size) {
val entity = iterator.next()
try {
insertionAdapter.insertAndReturnId(entity)
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entity)
-1
}
}
}
fun upsertAndReturnIdsList(entities: Array<out T>): List<Long> {
return buildList {
entities.forEach { entity ->
try {
add(insertionAdapter.insertAndReturnId(entity))
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entity)
add(-1)
}
}
}
}
fun upsertAndReturnIdsList(entities: Collection<T>): List<Long> {
return buildList {
entities.forEach { entity ->
try {
add(insertionAdapter.insertAndReturnId(entity))
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entity)
add(-1)
}
}
}
}
fun upsertAndReturnIdsArrayBox(entities: Array<out T>): Array<out Long> {
return Array(entities.size) { index ->
try {
insertionAdapter.insertAndReturnId(entities[index])
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entities[index])
-1
}
}
}
fun upsertAndReturnIdsArrayBox(entities: Collection<T>): Array<out Long> {
val iterator = entities.iterator()
return Array(entities.size) {
val entity = iterator.next()
try {
insertionAdapter.insertAndReturnId(entity)
} catch (ex: SQLiteConstraintException) {
checkUniquenessException(ex)
updateAdapter.handle(entity)
-1
}
}
}
/**
* Verify if the exception is caused by Uniqueness constraint (Primary Key Conflict).
* If yes, upsert should update the existing one. If not, upsert should re-throw the
* exception.
* For android of version newer than KITKAT(19), SQLite supports ErrorCode. Otherwise,
* check with Error Message.
*
* @param ex the exception thrown by the insert attempt
*/
private fun checkUniquenessException(ex: SQLiteConstraintException) {
val message = ex.message ?: throw ex
val hasUniqueConstraintEx =
message.contains(ErrorMsg, ignoreCase = true) ||
message.contains(SQLITE_CONSTRAINT_UNIQUE) ||
message.contains(SQLITE_CONSTRAINT_PRIMARYKEY)
if (!hasUniqueConstraintEx) {
throw ex
}
}
}

View File

@ -13,17 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.RequiresOptIn;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
package androidx.room
import androidx.annotation.RequiresOptIn
/**
* APIs marked with ExperimentalRoomApi are experimental and may change.
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@RequiresOptIn()
public @interface ExperimentalRoomApi {}
@Target(
AnnotationTarget.CLASS,
AnnotationTarget.FUNCTION
)
@Suppress("UnsafeOptInUsageError")
@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
annotation class ExperimentalRoomApi

View File

@ -1,71 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.annotation.SuppressLint;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
/**
* File utilities for Room
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class FileUtil {
/**
* Copies data from the input channel to the output file channel.
*
* @param input the input channel to copy.
* @param output the output channel to copy.
* @throws IOException if there is an I/O error.
*/
@SuppressLint("LambdaLast")
public static void copy(@NonNull ReadableByteChannel input, @NonNull FileChannel output)
throws IOException {
try {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
output.transferFrom(input, 0, Long.MAX_VALUE);
} else {
InputStream inputStream = Channels.newInputStream(input);
OutputStream outputStream = Channels.newOutputStream(output);
int length;
byte[] buffer = new byte[1024 * 4];
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
}
output.force(false);
} finally {
input.close();
output.close();
}
}
private FileUtil() {
}
}

View File

@ -1,220 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.Cursor;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.sqlite.db.SupportSQLiteDatabase;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A data class that holds the information about an FTS table.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public final class FtsTableInfo {
// A set of valid FTS Options
private static final String[] FTS_OPTIONS = new String[] {
"tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
"order=", "prefix=", "uncompress="};
/**
* The table name
*/
public final String name;
/**
* The column names
*/
public final Set<String> columns;
/**
* The set of options. Each value in the set contains the option in the following format:
* &lt;key&gt;=&lt;value&gt;.
*/
public final Set<String> options;
public FtsTableInfo(String name, Set<String> columns, Set<String> options) {
this.name = name;
this.columns = columns;
this.options = options;
}
public FtsTableInfo(String name, Set<String> columns, String createSql) {
this.name = name;
this.columns = columns;
this.options = parseOptions(createSql);
}
/**
* Reads the table information from the given database.
*
* @param database The database to read the information from.
* @param tableName The table name.
* @return A FtsTableInfo containing the columns and options for the provided table name.
*/
public static FtsTableInfo read(SupportSQLiteDatabase database, String tableName) {
Set<String> columns = readColumns(database, tableName);
Set<String> options = readOptions(database, tableName);
return new FtsTableInfo(tableName, columns, options);
}
@SuppressWarnings("TryFinallyCanBeTryWithResources")
private static Set<String> readColumns(SupportSQLiteDatabase database, String tableName) {
Cursor cursor = database.query("PRAGMA table_info(`" + tableName + "`)");
Set<String> columns = new HashSet<>();
try {
if (cursor.getColumnCount() > 0) {
int nameIndex = cursor.getColumnIndex("name");
while (cursor.moveToNext()) {
columns.add(cursor.getString(nameIndex));
}
}
} finally {
cursor.close();
}
return columns;
}
@SuppressWarnings("TryFinallyCanBeTryWithResources")
private static Set<String> readOptions(SupportSQLiteDatabase database, String tableName) {
String sql = "";
Cursor cursor = database.query(
"SELECT * FROM sqlite_master WHERE `name` = '" + tableName + "'");
try {
if (cursor.moveToFirst()) {
sql = cursor.getString(cursor.getColumnIndexOrThrow("sql"));
}
} finally {
cursor.close();
}
return parseOptions(sql);
}
/**
* Parses FTS options from the create statement of an FTS table.
*
* This method assumes the given create statement is a valid well-formed SQLite statement as
* defined in the <a href="https://www.sqlite.org/lang_createvtab.html">CREATE VIRTUAL TABLE
* syntax diagram</a>.
*
* @param createStatement the "CREATE VIRTUAL TABLE" statement.
* @return the set of FTS option key and values in the create statement.
*/
@VisibleForTesting
@SuppressWarnings("WeakerAccess") /* synthetic access */
static Set<String> parseOptions(String createStatement) {
if (createStatement.isEmpty()) {
return new HashSet<>();
}
// Module arguments are within the parenthesis followed by the module name.
String argsString = createStatement.substring(
createStatement.indexOf('(') + 1,
createStatement.lastIndexOf(')'));
// Split the module argument string by the comma delimiter, keeping track of quotation so
// so that if the delimiter is found within a string literal we don't substring at the wrong
// index. SQLite supports four ways of quoting keywords, see:
// https://www.sqlite.org/lang_keywords.html
List<String> args = new ArrayList<>();
ArrayDeque<Character> quoteStack = new ArrayDeque<>();
int lastDelimiterIndex = -1;
for (int i = 0; i < argsString.length(); i++) {
char c = argsString.charAt(i);
switch (c) {
case '\'':
case '"':
case '`':
if (quoteStack.isEmpty()) {
quoteStack.push(c);
} else if (quoteStack.peek() == c) {
quoteStack.pop();
}
break;
case '[':
if (quoteStack.isEmpty()) {
quoteStack.push(c);
}
break;
case ']':
if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
quoteStack.pop();
}
break;
case ',':
if (quoteStack.isEmpty()) {
args.add(argsString.substring(lastDelimiterIndex + 1, i).trim());
lastDelimiterIndex = i;
}
break;
}
}
args.add(argsString.substring(lastDelimiterIndex + 1).trim()); // Add final argument.
// Match args against valid options, otherwise they are column definitions.
HashSet<String> options = new HashSet<>();
for (String arg : args) {
for (String validOption : FTS_OPTIONS) {
if (arg.startsWith(validOption)) {
options.add(arg);
}
}
}
return options;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FtsTableInfo)) return false;
FtsTableInfo that = (FtsTableInfo) o;
if (name != null ? !name.equals(that.name) : that.name != null) return false;
if (columns != null ? !columns.equals(that.columns) : that.columns != null) return false;
return options != null ? options.equals(that.options) : that.options == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (columns != null ? columns.hashCode() : 0);
result = 31 * result + (options != null ? options.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "FtsTableInfo{"
+ "name='" + name + '\''
+ ", columns=" + columns
+ ", options=" + options
+ '}';
}
}

View File

@ -1,59 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LiveData;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
import java.util.concurrent.Callable;
/**
* A helper class that maintains {@link RoomTrackingLiveData} instances for an
* {@link InvalidationTracker}.
* <p>
* We keep a strong reference to active LiveData instances to avoid garbage collection in case
* developer does not hold onto the returned LiveData.
*/
class InvalidationLiveDataContainer {
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final Set<LiveData> mLiveDataSet = Collections.newSetFromMap(
new IdentityHashMap<LiveData, Boolean>()
);
private final RoomDatabase mDatabase;
InvalidationLiveDataContainer(RoomDatabase database) {
mDatabase = database;
}
<T> LiveData<T> create(String[] tableNames, boolean inTransaction,
Callable<T> computeFunction) {
return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
tableNames);
}
void onActive(LiveData liveData) {
mLiveDataSet.add(liveData);
}
void onInactive(LiveData liveData) {
mLiveDataSet.remove(liveData);
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.lifecycle.LiveData
import java.util.Collections
import java.util.IdentityHashMap
import java.util.concurrent.Callable
/**
* A helper class that maintains [RoomTrackingLiveData] instances for an
* [InvalidationTracker].
*
* We keep a strong reference to active LiveData instances to avoid garbage collection in case
* developer does not hold onto the returned LiveData.
*/
internal class InvalidationLiveDataContainer(private val database: RoomDatabase) {
internal val liveDataSet: MutableSet<LiveData<*>> = Collections.newSetFromMap(IdentityHashMap())
fun <T> create(
tableNames: Array<out String>,
inTransaction: Boolean,
computeFunction: Callable<T?>
): LiveData<T> {
return RoomTrackingLiveData(
database,
this,
inTransaction,
computeFunction,
tableNames
)
}
fun onActive(liveData: LiveData<*>) {
liveDataSet.add(liveData)
}
fun onInactive(liveData: LiveData<*>) {
liveDataSet.remove(liveData)
}
}

View File

@ -1,902 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.arch.core.internal.SafeIterableMap;
import androidx.lifecycle.LiveData;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
/**
* InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
* these tables.
*/
// Some details on how the InvalidationTracker works:
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
// tables.
// * Each update (write operation) on one of the observed tables triggers an update into the
// memory table table, flipping the invalidated flag ON.
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
// It works as an Observer, and notifies other instances of table invalidation.
public class InvalidationTracker {
private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
private static final String TABLE_ID_COLUMN_NAME = "table_id";
private static final String INVALIDATED_COLUMN_NAME = "invalidated";
private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
+ "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, "
+ INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)";
@VisibleForTesting
static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
+ " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";
@VisibleForTesting
static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
+ " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";
@NonNull
final HashMap<String, Integer> mTableIdLookup;
final String[] mTableNames;
@NonNull
private Map<String, Set<String>> mViewTables;
@Nullable
AutoCloser mAutoCloser = null;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final RoomDatabase mDatabase;
AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
private volatile boolean mInitialized = false;
@SuppressWarnings("WeakerAccess") /* synthetic access */
volatile SupportSQLiteStatement mCleanupStatement;
private final ObservedTableTracker mObservedTableTracker;
private final InvalidationLiveDataContainer mInvalidationLiveDataContainer;
// should be accessed with synchronization only.
@VisibleForTesting
@SuppressLint("RestrictedApi")
final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();
private MultiInstanceInvalidationClient mMultiInstanceInvalidationClient;
private final Object mSyncTriggersLock = new Object();
/**
* Used by the generated code.
*
* @hide
*/
@SuppressWarnings("WeakerAccess")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public InvalidationTracker(RoomDatabase database, String... tableNames) {
this(database, new HashMap<String, String>(), Collections.<String, Set<String>>emptyMap(),
tableNames);
}
/**
* Used by the generated code.
*
* @hide
*/
@SuppressWarnings("WeakerAccess")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public InvalidationTracker(RoomDatabase database, Map<String, String> shadowTablesMap,
Map<String, Set<String>> viewTables, String... tableNames) {
mDatabase = database;
mObservedTableTracker = new ObservedTableTracker(tableNames.length);
mTableIdLookup = new HashMap<>();
mViewTables = viewTables;
mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase);
final int size = tableNames.length;
mTableNames = new String[size];
for (int id = 0; id < size; id++) {
final String tableName = tableNames[id].toLowerCase(Locale.US);
mTableIdLookup.put(tableName, id);
String shadowTableName = shadowTablesMap.get(tableNames[id]);
if (shadowTableName != null) {
mTableNames[id] = shadowTableName.toLowerCase(Locale.US);
} else {
mTableNames[id] = tableName;
}
}
// Adjust table id lookup for those tables whose shadow table is another already mapped
// table (e.g. external content fts tables).
for (Map.Entry<String, String> shadowTableEntry : shadowTablesMap.entrySet()) {
String shadowTableName = shadowTableEntry.getValue().toLowerCase(Locale.US);
if (mTableIdLookup.containsKey(shadowTableName)) {
String tableName = shadowTableEntry.getKey().toLowerCase(Locale.US);
mTableIdLookup.put(tableName, mTableIdLookup.get(shadowTableName));
}
}
}
/**
* Sets the auto closer for this invalidation tracker so that the invalidation tracker can
* ensure that the database is not closed if there are pending invalidations that haven't yet
* been flushed.
*
* This also adds a callback to the autocloser to ensure that the InvalidationTracker is in
* an ok state once the table is invalidated.
*
* This must be called before the database is used.
*
* @param autoCloser the autocloser associated with the db
*/
void setAutoCloser(AutoCloser autoCloser) {
this.mAutoCloser = autoCloser;
mAutoCloser.setAutoCloseCallback(this::onAutoCloseCallback);
}
/**
* Internal method to initialize table tracking.
* <p>
* You should never call this method, it is called by the generated code.
*/
void internalInit(SupportSQLiteDatabase database) {
synchronized (this) {
if (mInitialized) {
Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
return;
}
// These actions are not in a transaction because temp_store is not allowed to be
// performed on a transaction, and recursive_triggers is not affected by transactions.
database.execSQL("PRAGMA temp_store = MEMORY;");
database.execSQL("PRAGMA recursive_triggers='ON';");
database.execSQL(CREATE_TRACKING_TABLE_SQL);
syncTriggers(database);
mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);
mInitialized = true;
}
}
void onAutoCloseCallback() {
synchronized (this) {
mInitialized = false;
mObservedTableTracker.resetTriggerState();
}
}
void startMultiInstanceInvalidation(Context context, String name, Intent serviceIntent) {
mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name,
serviceIntent, this, mDatabase.getQueryExecutor());
}
void stopMultiInstanceInvalidation() {
if (mMultiInstanceInvalidationClient != null) {
mMultiInstanceInvalidationClient.stop();
mMultiInstanceInvalidationClient = null;
}
}
private static void appendTriggerName(StringBuilder builder, String tableName,
String triggerType) {
builder.append("`")
.append("room_table_modification_trigger_")
.append(tableName)
.append("_")
.append(triggerType)
.append("`");
}
private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
final String tableName = mTableNames[tableId];
StringBuilder stringBuilder = new StringBuilder();
for (String trigger : TRIGGERS) {
stringBuilder.setLength(0);
stringBuilder.append("DROP TRIGGER IF EXISTS ");
appendTriggerName(stringBuilder, tableName, trigger);
writableDb.execSQL(stringBuilder.toString());
}
}
private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
writableDb.execSQL(
"INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
final String tableName = mTableNames[tableId];
StringBuilder stringBuilder = new StringBuilder();
for (String trigger : TRIGGERS) {
stringBuilder.setLength(0);
stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
appendTriggerName(stringBuilder, tableName, trigger);
stringBuilder.append(" AFTER ")
.append(trigger)
.append(" ON `")
.append(tableName)
.append("` BEGIN UPDATE ")
.append(UPDATE_TABLE_NAME)
.append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1")
.append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId)
.append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0")
.append("; END");
writableDb.execSQL(stringBuilder.toString());
}
}
/**
* Adds the given observer to the observers list and it will be notified if any table it
* observes changes.
* <p>
* Database changes are pulled on another thread so in some race conditions, the observer might
* be invoked for changes that were done before it is added.
* <p>
* If the observer already exists, this is a no-op call.
* <p>
* If one of the tables in the Observer does not exist in the database, this method throws an
* {@link IllegalArgumentException}.
* <p>
* This method should be called on a background/worker thread as it performs database
* operations.
*
* @param observer The observer which listens the database for changes.
*/
@SuppressLint("RestrictedApi")
@WorkerThread
public void addObserver(@NonNull Observer observer) {
final String[] tableNames = resolveViews(observer.mTables);
int[] tableIds = new int[tableNames.length];
final int size = tableNames.length;
for (int i = 0; i < size; i++) {
Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
if (tableId == null) {
throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
}
tableIds[i] = tableId;
}
ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);
ObserverWrapper currentObserver;
synchronized (mObserverMap) {
currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
}
if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
syncTriggers();
}
}
private String[] validateAndResolveTableNames(String[] tableNames) {
String[] resolved = resolveViews(tableNames);
for (String tableName : resolved) {
if (!mTableIdLookup.containsKey(tableName.toLowerCase(Locale.US))) {
throw new IllegalArgumentException("There is no table with name " + tableName);
}
}
return resolved;
}
/**
* Resolves the list of tables and views into a list of unique tables that are underlying them.
*
* @param names The names of tables or views.
* @return The names of the underlying tables.
*/
private String[] resolveViews(String[] names) {
Set<String> tables = new HashSet<>();
for (String name : names) {
final String lowercase = name.toLowerCase(Locale.US);
if (mViewTables.containsKey(lowercase)) {
tables.addAll(mViewTables.get(lowercase));
} else {
tables.add(name);
}
}
return tables.toArray(new String[tables.size()]);
}
private static void beginTransactionInternal(SupportSQLiteDatabase database) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& database.isWriteAheadLoggingEnabled()) {
database.beginTransactionNonExclusive();
} else {
database.beginTransaction();
}
}
/**
* Adds an observer but keeps a weak reference back to it.
* <p>
* Note that you cannot remove this observer once added. It will be automatically removed
* when the observer is GC'ed.
*
* @param observer The observer to which InvalidationTracker will keep a weak reference.
* @hide
*/
@SuppressWarnings("unused")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public void addWeakObserver(Observer observer) {
addObserver(new WeakObserver(this, observer));
}
/**
* Removes the observer from the observers list.
* <p>
* This method should be called on a background/worker thread as it performs database
* operations.
*
* @param observer The observer to remove.
*/
@SuppressLint("RestrictedApi")
@SuppressWarnings("WeakerAccess")
@WorkerThread
public void removeObserver(@NonNull final Observer observer) {
ObserverWrapper wrapper;
synchronized (mObserverMap) {
wrapper = mObserverMap.remove(observer);
}
if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
syncTriggers();
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean ensureInitialization() {
if (!mDatabase.isOpen()) {
return false;
}
if (!mInitialized) {
// trigger initialization
mDatabase.getOpenHelper().getWritableDatabase();
}
if (!mInitialized) {
Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
return false;
}
return true;
}
@VisibleForTesting
Runnable mRefreshRunnable = new Runnable() {
@Override
public void run() {
final Lock closeLock = mDatabase.getCloseLock();
Set<Integer> invalidatedTableIds = null;
closeLock.lock();
try {
if (!ensureInitialization()) {
return;
}
if (!mPendingRefresh.compareAndSet(true, false)) {
// no pending refresh
return;
}
if (mDatabase.inTransaction()) {
// current thread is in a transaction. when it ends, it will invoke
// refreshRunnable again. mPendingRefresh is left as false on purpose
// so that the last transaction can flip it on again.
return;
}
// This transaction has to be on the underlying DB rather than the RoomDatabase
// in order to avoid a recursive loop after endTransaction.
SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
db.beginTransactionNonExclusive();
try {
invalidatedTableIds = checkUpdatedTable();
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
} catch (IllegalStateException | SQLiteException exception) {
// may happen if db is closed. just log.
Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
exception);
} finally {
closeLock.unlock();
if (mAutoCloser != null) {
mAutoCloser.decrementCountAndScheduleClose();
}
}
if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
synchronized (mObserverMap) {
for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds);
}
}
}
}
private Set<Integer> checkUpdatedTable() {
HashSet<Integer> invalidatedTableIds = new HashSet<>();
Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));
//noinspection TryFinallyCanBeTryWithResources
try {
while (cursor.moveToNext()) {
final int tableId = cursor.getInt(0);
invalidatedTableIds.add(tableId);
}
} catch (Throwable ex) {
eu.faircode.email.Log.w(ex);
} finally {
cursor.close();
}
if (!invalidatedTableIds.isEmpty()) {
mCleanupStatement.executeUpdateDelete();
}
return invalidatedTableIds;
}
};
/**
* Enqueues a task to refresh the list of updated tables.
* <p>
* This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
* if you have another connection to the database or directly use {@link
* SupportSQLiteDatabase}, you may need to call this manually.
*/
@SuppressWarnings("WeakerAccess")
public void refreshVersionsAsync() {
// TODO we should consider doing this sync instead of async.
if (mPendingRefresh.compareAndSet(false, true)) {
if (mAutoCloser != null) {
// refreshVersionsAsync is called with the ref count incremented from
// RoomDatabase, so the db can't be closed here, but we need to be sure that our
// db isn't closed until refresh is completed. This increment call must be
// matched with a corresponding call in mRefreshRunnable.
mAutoCloser.incrementCountAndEnsureDbIsOpen();
}
mDatabase.getQueryExecutor().execute(mRefreshRunnable);
}
}
/**
* Check versions for tables, and run observers synchronously if tables have been updated.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@WorkerThread
public void refreshVersionsSync() {
if (mAutoCloser != null) {
// This increment call must be matched with a corresponding call in mRefreshRunnable.
mAutoCloser.incrementCountAndEnsureDbIsOpen();
}
syncTriggers();
mRefreshRunnable.run();
}
/**
* Notifies all the registered {@link Observer}s of table changes.
* <p>
* This can be used for notifying invalidation that cannot be detected by this
* {@link InvalidationTracker}, for example, invalidation from another process.
*
* @param tables The invalidated tables.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public void notifyObserversByTableNames(String... tables) {
synchronized (mObserverMap) {
for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
if (!entry.getKey().isRemote()) {
entry.getValue().notifyByTableNames(tables);
}
}
}
}
void syncTriggers(SupportSQLiteDatabase database) {
if (database.inTransaction()) {
// we won't run this inside another transaction.
return;
}
try {
Lock closeLock = mDatabase.getCloseLock();
closeLock.lock();
try {
// Serialize adding and removing table trackers, this is specifically important
// to avoid missing invalidation before a transaction starts but there are
// pending (possibly concurrent) observer changes.
synchronized (mSyncTriggersLock) {
final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
if (tablesToSync == null) {
return;
}
final int limit = tablesToSync.length;
beginTransactionInternal(database);
try {
for (int tableId = 0; tableId < limit; tableId++) {
switch (tablesToSync[tableId]) {
case ObservedTableTracker.ADD:
startTrackingTable(database, tableId);
break;
case ObservedTableTracker.REMOVE:
stopTrackingTable(database, tableId);
break;
}
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
} finally {
closeLock.unlock();
}
} catch (IllegalStateException | SQLiteException exception) {
// may happen if db is closed. just log.
Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
exception);
}
}
/**
* Called by RoomDatabase before each beginTransaction call.
* <p>
* It is important that pending trigger changes are applied to the database before any query
* runs. Otherwise, we may miss some changes.
* <p>
* This api should eventually be public.
*/
void syncTriggers() {
if (!mDatabase.isOpen()) {
return;
}
syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
}
/**
* Creates a LiveData that computes the given function once and for every other invalidation
* of the database.
* <p>
* Holds a strong reference to the created LiveData as long as it is active.
*
* @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
*
* @param computeFunction The function that calculates the value
* @param tableNames The list of tables to observe
* @param <T> The return type
* @return A new LiveData that computes the given function when the given list of tables
* invalidates.
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public <T> LiveData<T> createLiveData(String[] tableNames, Callable<T> computeFunction) {
return createLiveData(tableNames, false, computeFunction);
}
/**
* Creates a LiveData that computes the given function once and for every other invalidation
* of the database.
* <p>
* Holds a strong reference to the created LiveData as long as it is active.
*
* @param tableNames The list of tables to observe
* @param inTransaction True if the computeFunction will be done in a transaction, false
* otherwise.
* @param computeFunction The function that calculates the value
* @param <T> The return type
* @return A new LiveData that computes the given function when the given list of tables
* invalidates.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
Callable<T> computeFunction) {
return mInvalidationLiveDataContainer.create(
validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
}
/**
* Wraps an observer and keeps the table information.
* <p>
* Internally table ids are used which may change from database to database so the table
* related information is kept here rather than in the Observer.
*/
@SuppressWarnings("WeakerAccess")
static class ObserverWrapper {
final int[] mTableIds;
private final String[] mTableNames;
final Observer mObserver;
private final Set<String> mSingleTableSet;
ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) {
mObserver = observer;
mTableIds = tableIds;
mTableNames = tableNames;
if (tableIds.length == 1) {
HashSet<String> set = new HashSet<>();
set.add(mTableNames[0]);
mSingleTableSet = Collections.unmodifiableSet(set);
} else {
mSingleTableSet = null;
}
}
/**
* Notifies the underlying {@link #mObserver} if any of the observed tables are invalidated
* based on the given invalid status set.
*
* @param invalidatedTablesIds The table ids of the tables that are invalidated.
*/
void notifyByTableInvalidStatus(Set<Integer> invalidatedTablesIds) {
Set<String> invalidatedTables = null;
final int size = mTableIds.length;
for (int index = 0; index < size; index++) {
final int tableId = mTableIds[index];
if (invalidatedTablesIds.contains(tableId)) {
if (size == 1) {
// Optimization for a single-table observer
invalidatedTables = mSingleTableSet;
} else {
if (invalidatedTables == null) {
invalidatedTables = new HashSet<>(size);
}
invalidatedTables.add(mTableNames[index]);
}
}
}
if (invalidatedTables != null) {
mObserver.onInvalidated(invalidatedTables);
}
}
/**
* Notifies the underlying {@link #mObserver} if it observes any of the specified
* {@code tables}.
*
* @param tables The invalidated table names.
*/
void notifyByTableNames(String[] tables) {
Set<String> invalidatedTables = null;
if (mTableNames.length == 1) {
for (String table : tables) {
if (table.equalsIgnoreCase(mTableNames[0])) {
// Optimization for a single-table observer
invalidatedTables = mSingleTableSet;
break;
}
}
} else {
HashSet<String> set = new HashSet<>();
for (String table : tables) {
for (String ourTable : mTableNames) {
if (ourTable.equalsIgnoreCase(table)) {
set.add(ourTable);
break;
}
}
}
if (set.size() > 0) {
invalidatedTables = set;
}
}
if (invalidatedTables != null) {
mObserver.onInvalidated(invalidatedTables);
}
}
}
/**
* An observer that can listen for changes in the database.
*/
public abstract static class Observer {
final String[] mTables;
/**
* Observes the given list of tables and views.
*
* @param firstTable The name of the table or view.
* @param rest More names of tables or views.
*/
@SuppressWarnings("unused")
protected Observer(@NonNull String firstTable, String... rest) {
mTables = Arrays.copyOf(rest, rest.length + 1);
mTables[rest.length] = firstTable;
}
/**
* Observes the given list of tables and views.
*
* @param tables The list of tables or views to observe for changes.
*/
public Observer(@NonNull String[] tables) {
// copy tables in case user modifies them afterwards
mTables = Arrays.copyOf(tables, tables.length);
}
/**
* Called when one of the observed tables is invalidated in the database.
*
* @param tables A set of invalidated tables. This is useful when the observer targets
* multiple tables and you want to know which table is invalidated. This will
* be names of underlying tables when you are observing views.
*/
public abstract void onInvalidated(@NonNull Set<String> tables);
boolean isRemote() {
return false;
}
}
/**
* Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
* triggers in the database.
* <p>
* This class is thread safe
*/
static class ObservedTableTracker {
static final int NO_OP = 0; // don't change trigger state for this table
static final int ADD = 1; // add triggers for this table
static final int REMOVE = 2; // remove triggers for this table
// number of observers per table
final long[] mTableObservers;
// trigger state for each table at last sync
// this field is updated when syncAndGet is called.
final boolean[] mTriggerStates;
// when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
final int[] mTriggerStateChanges;
boolean mNeedsSync;
ObservedTableTracker(int tableCount) {
mTableObservers = new long[tableCount];
mTriggerStates = new boolean[tableCount];
mTriggerStateChanges = new int[tableCount];
Arrays.fill(mTableObservers, 0);
Arrays.fill(mTriggerStates, false);
}
/**
* @return true if # of triggers is affected.
*/
boolean onAdded(int... tableIds) {
boolean needTriggerSync = false;
synchronized (this) {
for (int tableId : tableIds) {
final long prevObserverCount = mTableObservers[tableId];
mTableObservers[tableId] = prevObserverCount + 1;
if (prevObserverCount == 0) {
mNeedsSync = true;
needTriggerSync = true;
}
}
}
return needTriggerSync;
}
/**
* @return true if # of triggers is affected.
*/
boolean onRemoved(int... tableIds) {
boolean needTriggerSync = false;
synchronized (this) {
for (int tableId : tableIds) {
final long prevObserverCount = mTableObservers[tableId];
mTableObservers[tableId] = prevObserverCount - 1;
if (prevObserverCount == 1) {
mNeedsSync = true;
needTriggerSync = true;
}
}
}
return needTriggerSync;
}
/**
* If we are re-opening the db we'll need to add all the triggers that we need so change
* the current state to false for all.
*/
void resetTriggerState() {
synchronized (this) {
Arrays.fill(mTriggerStates, false);
mNeedsSync = true;
}
}
/**
* If this returns non-null there are no pending sync operations.
*
* @return int[] An int array where the index for each tableId has the action for that
* table.
*/
@Nullable
int[] getTablesToSync() {
synchronized (this) {
if (!mNeedsSync) {
return null;
}
final int tableCount = mTableObservers.length;
for (int i = 0; i < tableCount; i++) {
final boolean newState = mTableObservers[i] > 0;
if (newState != mTriggerStates[i]) {
mTriggerStateChanges[i] = newState ? ADD : REMOVE;
} else {
mTriggerStateChanges[i] = NO_OP;
}
mTriggerStates[i] = newState;
}
mNeedsSync = false;
return mTriggerStateChanges.clone();
}
}
}
/**
* An Observer wrapper that keeps a weak reference to the given object.
* <p>
* This class will automatically unsubscribe when the wrapped observer goes out of memory.
*/
static class WeakObserver extends Observer {
final InvalidationTracker mTracker;
final WeakReference<Observer> mDelegateRef;
WeakObserver(InvalidationTracker tracker, Observer delegate) {
super(delegate.mTables);
mTracker = tracker;
mDelegateRef = new WeakReference<>(delegate);
}
@Override
public void onInvalidated(@NonNull Set<String> tables) {
final Observer observer = mDelegateRef.get();
if (observer == null) {
mTracker.removeObserver(this);
} else {
observer.onInvalidated(tables);
}
}
}
}

View File

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

View File

@ -1,196 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to
* {@link MultiInstanceInvalidationService}.
*/
class MultiInstanceInvalidationClient {
/**
* The application context.
*/
// synthetic access
@SuppressWarnings("WeakerAccess")
final Context mAppContext;
/**
* The name of the database file.
*/
// synthetic access
@SuppressWarnings("WeakerAccess")
final String mName;
/**
* The client ID assigned by {@link MultiInstanceInvalidationService}.
*/
// synthetic access
@SuppressWarnings("WeakerAccess")
int mClientId;
// synthetic access
@SuppressWarnings("WeakerAccess")
final InvalidationTracker mInvalidationTracker;
// synthetic access
@SuppressWarnings("WeakerAccess")
final InvalidationTracker.Observer mObserver;
// synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
IMultiInstanceInvalidationService mService;
// synthetic access
@SuppressWarnings("WeakerAccess")
final Executor mExecutor;
// synthetic access
@SuppressWarnings("WeakerAccess")
final IMultiInstanceInvalidationCallback mCallback =
new IMultiInstanceInvalidationCallback.Stub() {
@Override
public void onInvalidation(final String[] tables) {
mExecutor.execute(new Runnable() {
@Override
public void run() {
mInvalidationTracker.notifyObserversByTableNames(tables);
}
});
}
};
// synthetic access
@SuppressWarnings("WeakerAccess")
final AtomicBoolean mStopped = new AtomicBoolean(false);
// synthetic access
@SuppressWarnings("WeakerAccess")
final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = IMultiInstanceInvalidationService.Stub.asInterface(service);
mExecutor.execute(mSetUpRunnable);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mExecutor.execute(mRemoveObserverRunnable);
mService = null;
}
};
// synthetic access
@SuppressWarnings("WeakerAccess")
final Runnable mSetUpRunnable = new Runnable() {
@Override
public void run() {
try {
final IMultiInstanceInvalidationService service = mService;
if (service != null) {
mClientId = service.registerCallback(mCallback, mName);
mInvalidationTracker.addObserver(mObserver);
}
} catch (RemoteException e) {
Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e);
}
}
};
// synthetic access
@SuppressWarnings("WeakerAccess")
final Runnable mRemoveObserverRunnable = new Runnable() {
@Override
public void run() {
mInvalidationTracker.removeObserver(mObserver);
}
};
/**
* @param context The Context to be used for binding
* {@link IMultiInstanceInvalidationService}.
* @param name The name of the database file.
* @param serviceIntent The {@link Intent} used for binding
* {@link IMultiInstanceInvalidationService}.
* @param invalidationTracker The {@link InvalidationTracker}
* @param executor The background executor.
*/
MultiInstanceInvalidationClient(Context context, String name, Intent serviceIntent,
InvalidationTracker invalidationTracker, Executor executor) {
mAppContext = context.getApplicationContext();
mName = name;
mInvalidationTracker = invalidationTracker;
mExecutor = executor;
// Use all tables names for observer.
final Set<String> tableNames = invalidationTracker.mTableIdLookup.keySet();
mObserver = new InvalidationTracker.Observer(tableNames.toArray(new String[0])) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
if (mStopped.get()) {
return;
}
try {
final IMultiInstanceInvalidationService service = mService;
if (service != null) {
service.broadcastInvalidation(mClientId, tables.toArray(new String[0]));
}
} catch (RemoteException e) {
Log.w(Room.LOG_TAG, "Cannot broadcast invalidation", e);
}
}
@Override
boolean isRemote() {
return true;
}
};
mAppContext.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
void stop() {
if (mStopped.compareAndSet(false, true)) {
mInvalidationTracker.removeObserver(mObserver);
try {
final IMultiInstanceInvalidationService service = mService;
if (service != null) {
service.unregisterCallback(mCallback, mClientId);
}
} catch (RemoteException e) {
Log.w(Room.LOG_TAG, "Cannot unregister multi-instance invalidation callback", e);
}
mAppContext.unbindService(mServiceConnection);
}
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import androidx.room.Room.LOG_TAG
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean
/**
* Handles all the communication from [RoomDatabase] and [InvalidationTracker] to
* [MultiInstanceInvalidationService].
*
* @param context The Context to be used for binding
* [IMultiInstanceInvalidationService].
* @param name The name of the database file.
* @param serviceIntent The [Intent] used for binding
* [IMultiInstanceInvalidationService].
* @param invalidationTracker The [InvalidationTracker]
* @param executor The background executor.
*/
internal class MultiInstanceInvalidationClient(
context: Context,
val name: String,
serviceIntent: Intent,
val invalidationTracker: InvalidationTracker,
val executor: Executor
) {
private val appContext = context.applicationContext
/**
* The client ID assigned by [MultiInstanceInvalidationService].
*/
var clientId = 0
lateinit var observer: InvalidationTracker.Observer
var service: IMultiInstanceInvalidationService? = null
val callback: IMultiInstanceInvalidationCallback =
object : IMultiInstanceInvalidationCallback.Stub() {
override fun onInvalidation(tables: Array<out String>) {
executor.execute { invalidationTracker.notifyObserversByTableNames(*tables) }
}
}
val stopped = AtomicBoolean(false)
val serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
this@MultiInstanceInvalidationClient.service =
IMultiInstanceInvalidationService.Stub.asInterface(service)
executor.execute(setUpRunnable)
}
override fun onServiceDisconnected(name: ComponentName) {
executor.execute(removeObserverRunnable)
service = null
}
}
val setUpRunnable = Runnable {
try {
service?.let {
clientId = it.registerCallback(callback, name)
invalidationTracker.addObserver(observer)
}
} catch (e: RemoteException) {
Log.w(LOG_TAG, "Cannot register multi-instance invalidation callback", e)
}
}
val removeObserverRunnable = Runnable { invalidationTracker.removeObserver(observer) }
init {
// Use all tables names for observer.
val tableNames: Set<String> = invalidationTracker.tableIdLookup.keys
observer = object : InvalidationTracker.Observer(tableNames.toTypedArray()) {
override fun onInvalidated(tables: Set<String>) {
if (stopped.get()) {
return
}
try {
service?.broadcastInvalidation(clientId, tables.toTypedArray())
} catch (e: RemoteException) {
Log.w(LOG_TAG, "Cannot broadcast invalidation", e)
}
}
override val isRemote: Boolean
get() = true
}
appContext.bindService(
serviceIntent,
serviceConnection,
Context.BIND_AUTO_CREATE
)
}
fun stop() {
if (stopped.compareAndSet(false, true)) {
invalidationTracker.removeObserver(observer)
try {
service?.unregisterCallback(callback, clientId)
} catch (e: RemoteException) {
Log.w(LOG_TAG, "Cannot unregister multi-instance invalidation callback", e)
}
appContext.unbindService(serviceConnection)
}
}
}

View File

@ -1,137 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
/**
* A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances.
* This service runs in the main app process. All the instances of {@link InvalidationTracker}
* (potentially in other processes) has to connect to this service.
*
* <p>The intent to launch it can be specified by
* {@link RoomDatabase.Builder#setMultiInstanceInvalidationServiceIntent}, although the service is
* defined in the manifest by default so there should be no need to override it in a normal
* situation.
*/
@ExperimentalRoomApi
public class MultiInstanceInvalidationService extends Service {
// synthetic access
@SuppressWarnings("WeakerAccess")
int mMaxClientId = 0;
// synthetic access
@SuppressWarnings("WeakerAccess")
final HashMap<Integer, String> mClientNames = new HashMap<>();
// synthetic access
@SuppressWarnings("WeakerAccess")
final RemoteCallbackList<IMultiInstanceInvalidationCallback> mCallbackList =
new RemoteCallbackList<IMultiInstanceInvalidationCallback>() {
@Override
public void onCallbackDied(IMultiInstanceInvalidationCallback callback,
Object cookie) {
mClientNames.remove((int) cookie);
}
};
private final IMultiInstanceInvalidationService.Stub mBinder =
new IMultiInstanceInvalidationService.Stub() {
// Assigns a client ID to the client.
@Override
public int registerCallback(IMultiInstanceInvalidationCallback callback,
String name) {
if (name == null) {
return 0;
}
synchronized (mCallbackList) {
int clientId = ++mMaxClientId;
// Use the client ID as the RemoteCallbackList cookie.
if (mCallbackList.register(callback, clientId)) {
mClientNames.put(clientId, name);
return clientId;
} else {
--mMaxClientId;
return 0;
}
}
}
// Explicitly removes the client.
// The client can die without calling this. In that case, mCallbackList
// .onCallbackDied() can take care of removal.
@Override
public void unregisterCallback(IMultiInstanceInvalidationCallback callback,
int clientId) {
synchronized (mCallbackList) {
mCallbackList.unregister(callback);
mClientNames.remove(clientId);
}
}
// Broadcasts table invalidation to other instances of the same database file.
// The broadcast is not sent to the caller itself.
@Override
public void broadcastInvalidation(int clientId, String[] tables) {
synchronized (mCallbackList) {
String name = mClientNames.get(clientId);
if (name == null) {
Log.w(Room.LOG_TAG, "Remote invalidation client ID not registered");
return;
}
int count = mCallbackList.beginBroadcast();
try {
for (int i = 0; i < count; i++) {
int targetClientId = (int) mCallbackList.getBroadcastCookie(i);
String targetName = mClientNames.get(targetClientId);
if (clientId == targetClientId // This is the caller itself.
|| !name.equals(targetName)) { // Not the same file.
continue;
}
try {
IMultiInstanceInvalidationCallback callback =
mCallbackList.getBroadcastItem(i);
callback.onInvalidation(tables);
} catch (RemoteException e) {
Log.w(Room.LOG_TAG, "Error invoking a remote callback", e);
}
}
} finally {
mCallbackList.finishBroadcast();
}
}
}
};
@Nullable
@Override
public IBinder onBind(@NonNull Intent intent) {
return mBinder;
}
}

View File

@ -0,0 +1,122 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.RemoteCallbackList
import android.os.RemoteException
import android.util.Log
import androidx.room.Room.LOG_TAG
/**
* A [Service] for remote invalidation among multiple [InvalidationTracker] instances.
* This service runs in the main app process. All the instances of [InvalidationTracker]
* (potentially in other processes) has to connect to this service.
*
* The intent to launch it can be specified by
* [RoomDatabase.Builder.setMultiInstanceInvalidationServiceIntent], although the service is
* defined in the manifest by default so there should be no need to override it in a normal
* situation.
*/
@ExperimentalRoomApi
class MultiInstanceInvalidationService : Service() {
internal var maxClientId = 0
internal val clientNames = mutableMapOf<Int, String>()
internal val callbackList: RemoteCallbackList<IMultiInstanceInvalidationCallback> =
object : RemoteCallbackList<IMultiInstanceInvalidationCallback>() {
override fun onCallbackDied(
callback: IMultiInstanceInvalidationCallback,
cookie: Any
) {
clientNames.remove(cookie as Int)
}
}
private val binder: IMultiInstanceInvalidationService.Stub =
object : IMultiInstanceInvalidationService.Stub() {
// Assigns a client ID to the client.
override fun registerCallback(
callback: IMultiInstanceInvalidationCallback,
name: String?
): Int {
if (name == null) {
return 0
}
synchronized(callbackList) {
val clientId = ++maxClientId
// Use the client ID as the RemoteCallbackList cookie.
return if (callbackList.register(callback, clientId)) {
clientNames[clientId] = name
clientId
} else {
--maxClientId
0
}
}
}
// Explicitly removes the client.
// The client can die without calling this. In that case, callbackList
// .onCallbackDied() can take care of removal.
override fun unregisterCallback(
callback: IMultiInstanceInvalidationCallback,
clientId: Int
) {
synchronized(callbackList) {
callbackList.unregister(callback)
clientNames.remove(clientId)
}
}
// Broadcasts table invalidation to other instances of the same database file.
// The broadcast is not sent to the caller itself.
override fun broadcastInvalidation(clientId: Int, tables: Array<out String>) {
synchronized(callbackList) {
val name = clientNames[clientId]
if (name == null) {
Log.w(LOG_TAG, "Remote invalidation client ID not registered")
return
}
val count = callbackList.beginBroadcast()
try {
for (i in 0 until count) {
val targetClientId = callbackList.getBroadcastCookie(i) as Int
val targetName = clientNames[targetClientId]
if (clientId == targetClientId || name != targetName) {
// Skip if this is the caller itself or broadcast is for another
// database.
continue
}
try {
callbackList.getBroadcastItem(i).onInvalidation(tables)
} catch (e: RemoteException) {
Log.w(LOG_TAG, "Error invoking a remote callback", e)
}
}
} finally {
callbackList.finishBroadcast()
}
}
}
}
override fun onBind(intent: Intent): IBinder {
return binder
}
}

View File

@ -1,312 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteTransactionListener;
import android.os.Build;
import android.os.CancellationSignal;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
/**
* Implements {@link SupportSQLiteDatabase} for SQLite queries.
*/
final class QueryInterceptorDatabase implements SupportSQLiteDatabase {
private final SupportSQLiteDatabase mDelegate;
private final RoomDatabase.QueryCallback mQueryCallback;
private final Executor mQueryCallbackExecutor;
QueryInterceptorDatabase(@NonNull SupportSQLiteDatabase supportSQLiteDatabase,
@NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
queryCallbackExecutor) {
mDelegate = supportSQLiteDatabase;
mQueryCallback = queryCallback;
mQueryCallbackExecutor = queryCallbackExecutor;
}
@NonNull
@Override
public SupportSQLiteStatement compileStatement(@NonNull String sql) {
return new QueryInterceptorStatement(mDelegate.compileStatement(sql),
mQueryCallback, sql, mQueryCallbackExecutor);
}
@Override
public void beginTransaction() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
Collections.emptyList()));
mDelegate.beginTransaction();
}
@Override
public void beginTransactionNonExclusive() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
Collections.emptyList()));
mDelegate.beginTransactionNonExclusive();
}
@Override
public void beginTransactionWithListener(@NonNull SQLiteTransactionListener
transactionListener) {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
Collections.emptyList()));
mDelegate.beginTransactionWithListener(transactionListener);
}
@Override
public void beginTransactionWithListenerNonExclusive(
@NonNull SQLiteTransactionListener transactionListener) {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
Collections.emptyList()));
mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
}
@Override
public void endTransaction() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("END TRANSACTION",
Collections.emptyList()));
mDelegate.endTransaction();
}
@Override
public void setTransactionSuccessful() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("TRANSACTION SUCCESSFUL",
Collections.emptyList()));
mDelegate.setTransactionSuccessful();
}
@Override
public boolean inTransaction() {
return mDelegate.inTransaction();
}
@Override
public boolean isDbLockedByCurrentThread() {
return mDelegate.isDbLockedByCurrentThread();
}
@Override
public boolean yieldIfContendedSafely() {
return mDelegate.yieldIfContendedSafely();
}
@Override
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
}
@Override
public int getVersion() {
return mDelegate.getVersion();
}
@Override
public void setVersion(int version) {
mDelegate.setVersion(version);
}
@Override
public long getMaximumSize() {
return mDelegate.getMaximumSize();
}
@Override
public long setMaximumSize(long numBytes) {
return mDelegate.setMaximumSize(numBytes);
}
@Override
public long getPageSize() {
return mDelegate.getPageSize();
}
@Override
public void setPageSize(long numBytes) {
mDelegate.setPageSize(numBytes);
}
@NonNull
@Override
public Cursor query(@NonNull String query) {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
Collections.emptyList()));
return mDelegate.query(query);
}
@NonNull
@Override
public Cursor query(@NonNull String query, @NonNull Object[] bindArgs) {
List<Object> 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<Object> 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<Pair<String, String>> getAttachedDbs() {
return mDelegate.getAttachedDbs();
}
@Override
public boolean isDatabaseIntegrityOk() {
return mDelegate.isDatabaseIntegrityOk();
}
@Override
public void close() throws IOException {
mDelegate.close();
}
@Override
public boolean isExecPerConnectionSQLSupported() {
return false;
}
@Override
public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs) {
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.database.Cursor
import android.database.sqlite.SQLiteTransactionListener
import android.os.CancellationSignal
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteStatement
import java.util.concurrent.Executor
/**
* Implements [SupportSQLiteDatabase] for SQLite queries.
*/
internal class QueryInterceptorDatabase(
private val delegate: SupportSQLiteDatabase,
private val queryCallbackExecutor: Executor,
private val queryCallback: RoomDatabase.QueryCallback
) : SupportSQLiteDatabase by delegate {
override fun compileStatement(sql: String): SupportSQLiteStatement {
return QueryInterceptorStatement(
delegate.compileStatement(sql),
sql,
queryCallbackExecutor,
queryCallback,
)
}
override fun beginTransaction() {
queryCallbackExecutor.execute {
queryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", emptyList())
}
delegate.beginTransaction()
}
override fun beginTransactionNonExclusive() {
queryCallbackExecutor.execute {
queryCallback.onQuery("BEGIN DEFERRED TRANSACTION", emptyList())
}
delegate.beginTransactionNonExclusive()
}
override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) {
queryCallbackExecutor.execute {
queryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", emptyList())
}
delegate.beginTransactionWithListener(transactionListener)
}
override fun beginTransactionWithListenerNonExclusive(
transactionListener: SQLiteTransactionListener
) {
queryCallbackExecutor.execute {
queryCallback.onQuery("BEGIN DEFERRED TRANSACTION", emptyList())
}
delegate.beginTransactionWithListenerNonExclusive(transactionListener)
}
override fun endTransaction() {
queryCallbackExecutor.execute {
queryCallback.onQuery("END TRANSACTION", emptyList())
}
delegate.endTransaction()
}
override fun setTransactionSuccessful() {
queryCallbackExecutor.execute {
queryCallback.onQuery("TRANSACTION SUCCESSFUL", emptyList())
}
delegate.setTransactionSuccessful()
}
override fun query(query: String): Cursor {
queryCallbackExecutor.execute {
queryCallback.onQuery(query, emptyList())
}
return delegate.query(query)
}
override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
queryCallbackExecutor.execute { queryCallback.onQuery(query, bindArgs.toList()) }
return delegate.query(query, bindArgs)
}
override fun query(query: SupportSQLiteQuery): Cursor {
val queryInterceptorProgram = QueryInterceptorProgram()
query.bindTo(queryInterceptorProgram)
queryCallbackExecutor.execute {
queryCallback.onQuery(query.sql, queryInterceptorProgram.bindArgsCache)
}
return delegate.query(query)
}
override fun query(
query: SupportSQLiteQuery,
cancellationSignal: CancellationSignal?
): Cursor {
val queryInterceptorProgram = QueryInterceptorProgram()
query.bindTo(queryInterceptorProgram)
queryCallbackExecutor.execute {
queryCallback.onQuery(
query.sql,
queryInterceptorProgram.bindArgsCache
)
}
return delegate.query(query)
}
// Suppress warning about `SQL` in execSQL not being camel case. This is an override function
// and it can't be renamed.
@Suppress("AcronymName")
override fun execSQL(sql: String) {
queryCallbackExecutor.execute {
queryCallback.onQuery(sql, emptyList())
}
delegate.execSQL(sql)
}
// Suppress warning about `SQL` in execSQL not being camel case. This is an override function
// and it can't be renamed.
@Suppress("AcronymName")
override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
val inputArguments = buildList { addAll(bindArgs) }
queryCallbackExecutor.execute {
queryCallback.onQuery(sql, inputArguments)
}
delegate.execSQL(sql, inputArguments.toTypedArray())
}
}

View File

@ -1,78 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.util.concurrent.Executor;
final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
private final SupportSQLiteOpenHelper mDelegate;
private final RoomDatabase.QueryCallback mQueryCallback;
private final Executor mQueryCallbackExecutor;
QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
@NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
queryCallbackExecutor) {
mDelegate = supportSQLiteOpenHelper;
mQueryCallback = queryCallback;
mQueryCallbackExecutor = queryCallbackExecutor;
}
@Nullable
@Override
public String getDatabaseName() {
return mDelegate.getDatabaseName();
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void setWriteAheadLoggingEnabled(boolean enabled) {
mDelegate.setWriteAheadLoggingEnabled(enabled);
}
@Override
public SupportSQLiteDatabase getWritableDatabase() {
return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback,
mQueryCallbackExecutor);
}
@Override
public SupportSQLiteDatabase getReadableDatabase() {
return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback,
mQueryCallbackExecutor);
}
@Override
public void close() {
mDelegate.close();
}
@Override
@NonNull
public SupportSQLiteOpenHelper getDelegate() {
return mDelegate;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import java.util.concurrent.Executor
internal class QueryInterceptorOpenHelper(
override val delegate: SupportSQLiteOpenHelper,
private val queryCallbackExecutor: Executor,
private val queryCallback: RoomDatabase.QueryCallback
) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper {
override val writableDatabase: SupportSQLiteDatabase
get() = QueryInterceptorDatabase(
delegate.writableDatabase,
queryCallbackExecutor,
queryCallback
)
override val readableDatabase: SupportSQLiteDatabase
get() = QueryInterceptorDatabase(
delegate.readableDatabase,
queryCallbackExecutor,
queryCallback
)
}

View File

@ -1,50 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.NonNull;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.util.concurrent.Executor;
/**
* Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper.
*/
@SuppressWarnings("AcronymName")
final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
private final SupportSQLiteOpenHelper.Factory mDelegate;
private final RoomDatabase.QueryCallback mQueryCallback;
private final Executor mQueryCallbackExecutor;
@SuppressWarnings("LambdaLast")
QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory,
@NonNull RoomDatabase.QueryCallback queryCallback,
@NonNull Executor queryCallbackExecutor) {
mDelegate = factory;
mQueryCallback = queryCallback;
mQueryCallbackExecutor = queryCallbackExecutor;
}
@NonNull
@Override
public SupportSQLiteOpenHelper create(
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback,
mQueryCallbackExecutor);
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.sqlite.db.SupportSQLiteOpenHelper
import java.util.concurrent.Executor
/**
* Implements [SupportSQLiteOpenHelper.Factory] to wrap [QueryInterceptorOpenHelper].
*/
internal class QueryInterceptorOpenHelperFactory(
private val delegate: SupportSQLiteOpenHelper.Factory,
private val queryCallbackExecutor: Executor,
private val queryCallback: RoomDatabase.QueryCallback,
) : SupportSQLiteOpenHelper.Factory by delegate {
override fun create(
configuration: SupportSQLiteOpenHelper.Configuration
): SupportSQLiteOpenHelper {
return QueryInterceptorOpenHelper(
delegate.create(configuration),
queryCallbackExecutor,
queryCallback
)
}
}

View File

@ -1,82 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.sqlite.db.SupportSQLiteProgram;
import java.util.ArrayList;
import java.util.List;
/**
* A program implementing an {@link SupportSQLiteProgram} API to record bind arguments.
*/
final class QueryInterceptorProgram implements SupportSQLiteProgram {
private List<Object> 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<Object> getBindArgs() {
return mBindArgsCache;
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.sqlite.db.SupportSQLiteProgram
/**
* A program implementing an [SupportSQLiteProgram] API to record bind arguments.
*/
internal class QueryInterceptorProgram : SupportSQLiteProgram {
internal val bindArgsCache = mutableListOf<Any?>()
override fun bindNull(index: Int) {
saveArgsToCache(index, null)
}
override fun bindLong(index: Int, value: Long) {
saveArgsToCache(index, value)
}
override fun bindDouble(index: Int, value: Double) {
saveArgsToCache(index, value)
}
override fun bindString(index: Int, value: String) {
saveArgsToCache(index, value)
}
override fun bindBlob(index: Int, value: ByteArray) {
saveArgsToCache(index, value)
}
override fun clearBindings() {
bindArgsCache.clear()
}
override fun close() {}
private fun saveArgsToCache(bindIndex: Int, value: Any?) {
// The index into bind methods are 1...n
val index = bindIndex - 1
if (index >= bindArgsCache.size) {
for (i in bindArgsCache.size..index) {
bindArgsCache.add(null)
}
}
bindArgsCache[index] = value
}
}

View File

@ -1,128 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.NonNull;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Implements an instance of {@link SupportSQLiteStatement} for SQLite queries.
*/
final class QueryInterceptorStatement implements SupportSQLiteStatement {
private final SupportSQLiteStatement mDelegate;
private final RoomDatabase.QueryCallback mQueryCallback;
private final String mSqlStatement;
private final List<Object> 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);
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.sqlite.db.SupportSQLiteStatement
import java.util.concurrent.Executor
/**
* Implements an instance of [SupportSQLiteStatement] for SQLite queries.
*/
internal class QueryInterceptorStatement(
private val delegate: SupportSQLiteStatement,
private val sqlStatement: String,
private val queryCallbackExecutor: Executor,
private val queryCallback: RoomDatabase.QueryCallback,
) : SupportSQLiteStatement by delegate {
private val bindArgsCache = mutableListOf<Any?>()
override fun execute() {
queryCallbackExecutor.execute {
queryCallback.onQuery(sqlStatement, bindArgsCache)
}
delegate.execute()
}
override fun executeUpdateDelete(): Int {
queryCallbackExecutor.execute {
queryCallback.onQuery(sqlStatement, bindArgsCache)
}
return delegate.executeUpdateDelete()
}
override fun executeInsert(): Long {
queryCallbackExecutor.execute {
queryCallback.onQuery(sqlStatement, bindArgsCache)
}
return delegate.executeInsert()
}
override fun simpleQueryForLong(): Long {
queryCallbackExecutor.execute {
queryCallback.onQuery(sqlStatement, bindArgsCache)
}
return delegate.simpleQueryForLong()
}
override fun simpleQueryForString(): String? {
queryCallbackExecutor.execute {
queryCallback.onQuery(sqlStatement, bindArgsCache)
}
return delegate.simpleQueryForString()
}
override fun bindNull(index: Int) {
saveArgsToCache(index, null)
delegate.bindNull(index)
}
override fun bindLong(index: Int, value: Long) {
saveArgsToCache(index, value)
delegate.bindLong(index, value)
}
override fun bindDouble(index: Int, value: Double) {
saveArgsToCache(index, value)
delegate.bindDouble(index, value)
}
override fun bindString(index: Int, value: String) {
saveArgsToCache(index, value)
delegate.bindString(index, value)
}
override fun bindBlob(index: Int, value: ByteArray) {
saveArgsToCache(index, value)
delegate.bindBlob(index, value)
}
override fun clearBindings() {
bindArgsCache.clear()
delegate.clearBindings()
}
private fun saveArgsToCache(bindIndex: Int, value: Any?) {
val index = bindIndex - 1
if (index >= bindArgsCache.size) {
// Add null entries to the list until we have the desired # of indices
repeat(index - bindArgsCache.size + 1) {
bindArgsCache.add(null)
}
}
bindArgsCache[index] = value
}
}

View File

@ -1,115 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
/**
* Utility class for Room.
*/
@SuppressWarnings("unused")
public class Room {
static final String LOG_TAG = "ROOM";
/**
* The master table where room keeps its metadata information.
*/
public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
/**
* Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
* should keep a reference to it and re-use it.
*
* @param context The context for the database. This is usually the Application context.
* @param klass The abstract class which is annotated with {@link Database} and extends
* {@link RoomDatabase}.
* @param name The name of the database file.
* @param <T> The type of the database class.
* @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
*/
@SuppressWarnings("WeakerAccess")
@NonNull
public static <T extends RoomDatabase> RoomDatabase.Builder<T> databaseBuilder(
@NonNull Context context, @NonNull Class<T> klass, @NonNull String name) {
//noinspection ConstantConditions
if (name == null || name.trim().length() == 0) {
throw new IllegalArgumentException("Cannot build a database with null or empty name."
+ " If you are trying to create an in memory database, use Room"
+ ".inMemoryDatabaseBuilder");
}
return new RoomDatabase.Builder<>(context, klass, name);
}
/**
* Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
* database disappears when the process is killed.
* Once a database is built, you should keep a reference to it and re-use it.
*
* @param context The context for the database. This is usually the Application context.
* @param klass The abstract class which is annotated with {@link Database} and extends
* {@link RoomDatabase}.
* @param <T> The type of the database class.
* @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
*/
@NonNull
public static <T extends RoomDatabase> RoomDatabase.Builder<T> inMemoryDatabaseBuilder(
@NonNull Context context, @NonNull Class<T> klass) {
return new RoomDatabase.Builder<>(context, klass, null);
}
@SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static <T, C> T getGeneratedImplementation(@NonNull Class<C> klass,
@NonNull String suffix) {
final String fullPackage = klass.getPackage().getName();
String name = klass.getCanonicalName();
final String postPackageName = fullPackage.isEmpty()
? name
: name.substring(fullPackage.length() + 1);
final String implName = postPackageName.replace('.', '_') + suffix;
//noinspection TryWithIdenticalCatches
try {
final String fullClassName = fullPackage.isEmpty()
? implName
: fullPackage + "." + implName;
@SuppressWarnings("unchecked")
final Class<T> aClass = (Class<T>) Class.forName(
fullClassName, true, klass.getClassLoader());
return aClass.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("cannot find implementation for "
+ klass.getCanonicalName() + ". " + implName + " does not exist");
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access the constructor"
+ klass.getCanonicalName());
} catch (InstantiationException e) {
throw new RuntimeException("Failed to create an instance of "
+ klass.getCanonicalName());
}
}
/** @deprecated This type should not be instantiated as it contains only static methods. */
@Deprecated
@SuppressWarnings("PrivateConstructorForUtilityClass")
public Room() {
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.content.Context
import androidx.annotation.RestrictTo
/**
* Utility functions for Room.
*/
object Room {
internal const val LOG_TAG = "ROOM"
/**
* The master table where room keeps its metadata information.
*/
const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME
private const val CURSOR_CONV_SUFFIX = "_CursorConverter"
@Suppress("UNCHECKED_CAST")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun <T, C> getGeneratedImplementation(
klass: Class<C>,
suffix: String
): T {
val fullPackage = klass.getPackage()!!.name
val name: String = klass.canonicalName!!
val postPackageName =
if (fullPackage.isEmpty()) name else name.substring(fullPackage.length + 1)
val implName = postPackageName.replace('.', '_') + suffix
return try {
val fullClassName = if (fullPackage.isEmpty()) {
implName
} else {
"$fullPackage.$implName"
}
val aClass = Class.forName(
fullClassName, true, klass.classLoader
) as Class<T>
aClass.getDeclaredConstructor().newInstance()
} catch (e: ClassNotFoundException) {
throw RuntimeException(
"Cannot find implementation for ${klass.canonicalName}. $implName does not " +
"exist"
)
} catch (e: IllegalAccessException) {
throw RuntimeException(
"Cannot access the constructor ${klass.canonicalName}"
)
} catch (e: InstantiationException) {
throw RuntimeException(
"Failed to create an instance of ${klass.canonicalName}"
)
}
}
/**
* Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
* database disappears when the process is killed.
* Once a database is built, you should keep a reference to it and re-use it.
*
* @param context The context for the database. This is usually the Application context.
* @param klass The abstract class which is annotated with [Database] and extends
* [RoomDatabase].
* @param T The type of the database class.
* @return A `RoomDatabaseBuilder<T>` which you can use to create the database.
*/
@JvmStatic
fun <T : RoomDatabase> inMemoryDatabaseBuilder(
context: Context,
klass: Class<T>
): RoomDatabase.Builder<T> {
return RoomDatabase.Builder(context, klass, null)
}
/**
* Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
* should keep a reference to it and re-use it.
*
* @param context The context for the database. This is usually the Application context.
* @param klass The abstract class which is annotated with [Database] and extends
* [RoomDatabase].
* @param name The name of the database file.
* @param T The type of the database class.
* @return A `RoomDatabaseBuilder<T>` which you can use to create the database.
*/
@JvmStatic
fun <T : RoomDatabase> databaseBuilder(
context: Context,
klass: Class<T>,
name: String?
): RoomDatabase.Builder<T> {
require(!name.isNullOrBlank()) {
"Cannot build a database with null or empty name." +
" If you are trying to create an in memory database, use Room" +
".inMemoryDatabaseBuilder"
}
return RoomDatabase.Builder(context, klass, name)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,277 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.room.migration.Migration;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.util.List;
/**
* An open helper that holds a reference to the configuration until the database is opened.
*
* @hide
*/
@SuppressWarnings("unused")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
@Nullable
private DatabaseConfiguration mConfiguration;
@NonNull
private final Delegate mDelegate;
@NonNull
private final String mIdentityHash;
/**
* Room v1 had a bug where the hash was not consistent if fields are reordered.
* The new has fixes it but we still need to accept the legacy hash.
*/
@NonNull // b/64290754
private final String mLegacyHash;
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
@NonNull String identityHash, @NonNull String legacyHash) {
super(delegate.version);
mConfiguration = configuration;
mDelegate = delegate;
mIdentityHash = identityHash;
mLegacyHash = legacyHash;
}
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
@NonNull String legacyHash) {
this(configuration, delegate, "", legacyHash);
}
@Override
public void onConfigure(SupportSQLiteDatabase db) {
super.onConfigure(db);
}
@Override
public void onCreate(SupportSQLiteDatabase db) {
boolean isEmptyDatabase = hasEmptySchema(db);
mDelegate.createAllTables(db);
if (!isEmptyDatabase) {
// A 0 version pre-populated database goes through the create path because the
// framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
// find the database not to be empty, then it is a pre-populated, we must validate it to
// see if its suitable for usage.
ValidationResult result = mDelegate.onValidateSchema(db);
if (!result.isValid) {
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
+ result.expectedFoundMsg);
}
}
updateIdentity(db);
mDelegate.onCreate(db);
}
@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
boolean migrated = false;
if (mConfiguration != null) {
List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
oldVersion, newVersion);
if (migrations != null) {
mDelegate.onPreMigrate(db);
for (Migration migration : migrations) {
migration.migrate(db);
}
ValidationResult result = mDelegate.onValidateSchema(db);
if (!result.isValid) {
throw new IllegalStateException("Migration didn't properly handle: "
+ result.expectedFoundMsg);
}
mDelegate.onPostMigrate(db);
updateIdentity(db);
migrated = true;
}
}
if (!migrated) {
if (mConfiguration != null
&& !mConfiguration.isMigrationRequired(oldVersion, newVersion)) {
mDelegate.dropAllTables(db);
mDelegate.createAllTables(db);
} else {
throw new IllegalStateException("A migration from " + oldVersion + " to "
+ newVersion + " was required but not found. Please provide the "
+ "necessary Migration path via "
+ "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
+ "destructive migrations via one of the "
+ "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
}
}
}
@Override
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
@Override
public void onOpen(SupportSQLiteDatabase db) {
super.onOpen(db);
checkIdentity(db);
mDelegate.onOpen(db);
// there might be too many configurations etc, just clear it.
mConfiguration = null;
}
private void checkIdentity(SupportSQLiteDatabase db) {
if (hasRoomMasterTable(db)) {
String identityHash = null;
Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
//noinspection TryFinallyCanBeTryWithResources
try {
if (cursor.moveToFirst()) {
identityHash = cursor.getString(0);
}
} finally {
cursor.close();
}
if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ " you've changed schema but forgot to update the version number. You can"
+ " simply fix this by increasing the version number.");
}
} else {
// No room_master_table, this might an a pre-populated DB, we must validate to see if
// its suitable for usage.
ValidationResult result = mDelegate.onValidateSchema(db);
if (!result.isValid) {
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
+ result.expectedFoundMsg);
}
mDelegate.onPostMigrate(db);
updateIdentity(db);
}
}
private void updateIdentity(SupportSQLiteDatabase db) {
createMasterTableIfNotExists(db);
db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
}
private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
db.execSQL(RoomMasterTable.CREATE_QUERY);
}
private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) {
Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='"
+ RoomMasterTable.TABLE_NAME + "'");
//noinspection TryFinallyCanBeTryWithResources
try {
return cursor.moveToFirst() && cursor.getInt(0) != 0;
} finally {
cursor.close();
}
}
private static boolean hasEmptySchema(SupportSQLiteDatabase db) {
Cursor cursor = db.query(
"SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'");
//noinspection TryFinallyCanBeTryWithResources
try {
return cursor.moveToFirst() && cursor.getInt(0) == 0;
} finally {
cursor.close();
}
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public abstract static class Delegate {
public final int version;
public Delegate(int version) {
this.version = version;
}
protected abstract void dropAllTables(SupportSQLiteDatabase database);
protected abstract void createAllTables(SupportSQLiteDatabase database);
protected abstract void onOpen(SupportSQLiteDatabase database);
protected abstract void onCreate(SupportSQLiteDatabase database);
/**
* Called after a migration run to validate database integrity.
*
* @param db The SQLite database.
*
* @deprecated Use {@link #onValidateSchema(SupportSQLiteDatabase)}
*/
@Deprecated
protected void validateMigration(SupportSQLiteDatabase db) {
throw new UnsupportedOperationException("validateMigration is deprecated");
}
/**
* Called after a migration run or pre-package database copy to validate database integrity.
*
* @param db The SQLite database.
*/
@SuppressWarnings("deprecation")
@NonNull
protected ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) {
validateMigration(db);
return new ValidationResult(true, null);
}
/**
* Called before migrations execute to perform preliminary work.
* @param database The SQLite database.
*/
protected void onPreMigrate(SupportSQLiteDatabase database) {
}
/**
* Called after migrations execute to perform additional work.
* @param database The SQLite database.
*/
protected void onPostMigrate(SupportSQLiteDatabase database) {
}
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static class ValidationResult {
public final boolean isValid;
@Nullable
public final String expectedFoundMsg;
public ValidationResult(boolean isValid, @Nullable String expectedFoundMsg) {
this.isValid = isValid;
this.expectedFoundMsg = expectedFoundMsg;
}
}
}

View File

@ -0,0 +1,244 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.annotation.RestrictTo
import androidx.room.util.useCursor
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
/**
* An open helper that holds a reference to the configuration until the database is opened.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
open class RoomOpenHelper(
configuration: DatabaseConfiguration,
delegate: Delegate,
identityHash: String,
legacyHash: String
) : SupportSQLiteOpenHelper.Callback(delegate.version) {
private var configuration: DatabaseConfiguration?
private val delegate: Delegate
private val identityHash: String
/**
* Room v1 had a bug where the hash was not consistent if fields are reordered.
* The new has fixes it but we still need to accept the legacy hash.
*/
// b/64290754
private val legacyHash: String
init {
this.configuration = configuration
this.delegate = delegate
this.identityHash = identityHash
this.legacyHash = legacyHash
}
constructor(
configuration: DatabaseConfiguration,
delegate: Delegate,
legacyHash: String
) : this(configuration, delegate, "", legacyHash)
override fun onConfigure(db: SupportSQLiteDatabase) {
super.onConfigure(db)
}
override fun onCreate(db: SupportSQLiteDatabase) {
val isEmptyDatabase = hasEmptySchema(db)
delegate.createAllTables(db)
if (!isEmptyDatabase) {
// A 0 version pre-populated database goes through the create path because the
// framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
// find the database not to be empty, then it is a pre-populated, we must validate it to
// see if its suitable for usage.
val result = delegate.onValidateSchema(db)
if (!result.isValid) {
throw IllegalStateException(
"Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}"
)
}
}
updateIdentity(db)
delegate.onCreate(db)
}
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
var migrated = false
configuration?.let { config ->
val migrations = config.migrationContainer.findMigrationPath(
oldVersion, newVersion
)
if (migrations != null) {
delegate.onPreMigrate(db)
migrations.forEach { it.migrate(db) }
val result = delegate.onValidateSchema(db)
if (!result.isValid) {
throw IllegalStateException(
("Migration didn't properly handle: " +
result.expectedFoundMsg)
)
}
delegate.onPostMigrate(db)
updateIdentity(db)
migrated = true
}
}
if (!migrated) {
val config = this.configuration
if (config != null && !config.isMigrationRequired(oldVersion, newVersion)) {
delegate.dropAllTables(db)
delegate.createAllTables(db)
} else {
throw IllegalStateException(
"A migration from $oldVersion to $newVersion was required but not found. " +
"Please provide the " +
"necessary Migration path via " +
"RoomDatabase.Builder.addMigration(Migration ...) or allow for " +
"destructive migrations via one of the " +
"RoomDatabase.Builder.fallbackToDestructiveMigration* methods."
)
}
}
}
override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
onUpgrade(db, oldVersion, newVersion)
}
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
checkIdentity(db)
delegate.onOpen(db)
// there might be too many configurations etc, just clear it.
configuration = null
}
private fun checkIdentity(db: SupportSQLiteDatabase) {
if (hasRoomMasterTable(db)) {
val identityHash: String? = db.query(
SimpleSQLiteQuery(RoomMasterTable.READ_QUERY)
).useCursor { cursor ->
if (cursor.moveToFirst()) {
cursor.getString(0)
} else {
null
}
}
if (this.identityHash != identityHash && this.legacyHash != identityHash) {
throw IllegalStateException(
"Room cannot verify the data integrity. Looks like" +
" you've changed schema but forgot to update the version number. You can" +
" simply fix this by increasing the version number. Expected identity" +
" hash: ${ this.identityHash }, found: $identityHash"
)
}
} else {
// No room_master_table, this might an a pre-populated DB, we must validate to see if
// its suitable for usage.
val result = delegate.onValidateSchema(db)
if (!result.isValid) {
throw IllegalStateException(
"Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}"
)
}
delegate.onPostMigrate(db)
updateIdentity(db)
}
}
private fun updateIdentity(db: SupportSQLiteDatabase) {
createMasterTableIfNotExists(db)
db.execSQL(RoomMasterTable.createInsertQuery(identityHash))
}
private fun createMasterTableIfNotExists(db: SupportSQLiteDatabase) {
db.execSQL(RoomMasterTable.CREATE_QUERY)
}
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
abstract class Delegate(@JvmField val version: Int) {
abstract fun dropAllTables(db: SupportSQLiteDatabase)
abstract fun createAllTables(db: SupportSQLiteDatabase)
abstract fun onOpen(db: SupportSQLiteDatabase)
abstract fun onCreate(db: SupportSQLiteDatabase)
/**
* Called after a migration run to validate database integrity.
*
* @param db The SQLite database.
*/
@Deprecated("Use [onValidateSchema(SupportSQLiteDatabase)]")
protected open fun validateMigration(db: SupportSQLiteDatabase) {
throw UnsupportedOperationException("validateMigration is deprecated")
}
/**
* Called after a migration run or pre-package database copy to validate database integrity.
*
* @param db The SQLite database.
*/
@Suppress("DEPRECATION")
open fun onValidateSchema(db: SupportSQLiteDatabase): ValidationResult {
validateMigration(db)
return ValidationResult(true, null)
}
/**
* Called before migrations execute to perform preliminary work.
* @param database The SQLite database.
*/
open fun onPreMigrate(db: SupportSQLiteDatabase) {}
/**
* Called after migrations execute to perform additional work.
* @param database The SQLite database.
*/
open fun onPostMigrate(db: SupportSQLiteDatabase) {}
}
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
open class ValidationResult(
@JvmField val isValid: Boolean,
@JvmField val expectedFoundMsg: String?
)
companion object {
internal fun hasRoomMasterTable(db: SupportSQLiteDatabase): Boolean {
db.query(
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND " +
"name='${ RoomMasterTable.TABLE_NAME }'"
).useCursor { cursor ->
return cursor.moveToFirst() && cursor.getInt(0) != 0
}
}
internal fun hasEmptySchema(db: SupportSQLiteDatabase): Boolean {
db.query(
"SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'"
).useCursor { cursor ->
return cursor.moveToFirst() && cursor.getInt(0) == 0
}
}
}
}

View File

@ -1,299 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.IntDef;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.sqlite.db.SupportSQLiteProgram;
import androidx.sqlite.db.SupportSQLiteQuery;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
/**
* This class is used as an intermediate place to keep binding arguments so that we can run
* Cursor queries with correct types rather than passing everything as a string.
* <p>
* Because it is relatively a big object, they are pooled and must be released after each use.
*
* @hide
*/
@SuppressWarnings("unused")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
// Maximum number of queries we'll keep cached.
static final int POOL_LIMIT = 15;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
// Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
// clear the bigger queries (# of arguments).
static final int DESIRED_POOL_SIZE = 10;
private volatile String mQuery;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final long[] mLongBindings;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final double[] mDoubleBindings;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final String[] mStringBindings;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final byte[][] mBlobBindings;
@Binding
private final int[] mBindingTypes;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
final int mCapacity;
// number of arguments in the query
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
int mArgCount;
@SuppressWarnings("WeakerAccess")
@VisibleForTesting
static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();
/**
* Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
*
* @param supportSQLiteQuery The query to copy from
* @return A new query copied from the provided one.
*/
public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
supportSQLiteQuery.getSql(),
supportSQLiteQuery.getArgCount());
supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
@Override
public void bindNull(int index) {
query.bindNull(index);
}
@Override
public void bindLong(int index, long value) {
query.bindLong(index, value);
}
@Override
public void bindDouble(int index, double value) {
query.bindDouble(index, value);
}
@Override
public void bindString(int index, String value) {
query.bindString(index, value);
}
@Override
public void bindBlob(int index, byte[] value) {
query.bindBlob(index, value);
}
@Override
public void clearBindings() {
query.clearBindings();
}
@Override
public void close() {
// ignored.
}
});
return query;
}
/**
* Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
* given query.
*
* @param query The query to prepare
* @param argumentCount The number of query arguments
* @return A RoomSQLiteQuery that holds the given query and has space for the given number of
* arguments.
*/
@SuppressWarnings("WeakerAccess")
public static RoomSQLiteQuery acquire(String query, int argumentCount) {
synchronized (sQueryPool) {
final Map.Entry<Integer, RoomSQLiteQuery> entry =
sQueryPool.ceilingEntry(argumentCount);
if (entry != null) {
sQueryPool.remove(entry.getKey());
final RoomSQLiteQuery sqliteQuery = entry.getValue();
sqliteQuery.init(query, argumentCount);
return sqliteQuery;
}
}
RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
sqLiteQuery.init(query, argumentCount);
return sqLiteQuery;
}
private RoomSQLiteQuery(int capacity) {
mCapacity = capacity;
// because, 1 based indices... we don't want to offsets everything with 1 all the time.
int limit = capacity + 1;
//noinspection WrongConstant
mBindingTypes = new int[limit];
mLongBindings = new long[limit];
mDoubleBindings = new double[limit];
mStringBindings = new String[limit];
mBlobBindings = new byte[limit][];
}
@SuppressWarnings("WeakerAccess")
void init(String query, int argCount) {
mQuery = query;
mArgCount = argCount;
}
/**
* Releases the query back to the pool.
* <p>
* After released, the statement might be returned when {@link #acquire(String, int)} is called
* so you should never re-use it after releasing.
*/
@SuppressWarnings("WeakerAccess")
public void release() {
synchronized (sQueryPool) {
sQueryPool.put(mCapacity, this);
prunePoolLocked();
}
}
private static void prunePoolLocked() {
if (sQueryPool.size() > POOL_LIMIT) {
int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator();
while (toBeRemoved-- > 0) {
iterator.next();
iterator.remove();
}
}
}
@Override
public String getSql() {
return mQuery;
}
@Override
public int getArgCount() {
return mArgCount;
}
@Override
public void bindTo(SupportSQLiteProgram program) {
for (int index = 1; index <= mArgCount; index++) {
switch (mBindingTypes[index]) {
case NULL:
program.bindNull(index);
break;
case LONG:
program.bindLong(index, mLongBindings[index]);
break;
case DOUBLE:
program.bindDouble(index, mDoubleBindings[index]);
break;
case STRING:
program.bindString(index, mStringBindings[index]);
break;
case BLOB:
program.bindBlob(index, mBlobBindings[index]);
break;
}
}
}
@Override
public void bindNull(int index) {
mBindingTypes[index] = NULL;
}
@Override
public void bindLong(int index, long value) {
mBindingTypes[index] = LONG;
mLongBindings[index] = value;
}
@Override
public void bindDouble(int index, double value) {
mBindingTypes[index] = DOUBLE;
mDoubleBindings[index] = value;
}
@Override
public void bindString(int index, String value) {
mBindingTypes[index] = STRING;
mStringBindings[index] = value;
}
@Override
public void bindBlob(int index, byte[] value) {
mBindingTypes[index] = BLOB;
mBlobBindings[index] = value;
}
@Override
public void close() {
// no-op. not calling release because it is internal API.
}
/**
* Copies arguments from another RoomSQLiteQuery into this query.
*
* @param other The other query, which holds the arguments to be copied.
*/
public void copyArgumentsFrom(RoomSQLiteQuery other) {
int argCount = other.getArgCount() + 1; // +1 for the binding offsets
System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
}
@Override
public void clearBindings() {
Arrays.fill(mBindingTypes, NULL);
Arrays.fill(mStringBindings, null);
Arrays.fill(mBlobBindings, null);
mQuery = null;
// no need to clear others
}
private static final int NULL = 1;
private static final int LONG = 2;
private static final int DOUBLE = 3;
private static final int STRING = 4;
private static final int BLOB = 5;
@Retention(RetentionPolicy.SOURCE)
@IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
@interface Binding {
}
}

View File

@ -0,0 +1,233 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.sqlite.db.SupportSQLiteProgram
import androidx.sqlite.db.SupportSQLiteQuery
import java.util.Arrays
import java.util.TreeMap
/**
* This class is used as an intermediate place to keep binding arguments so that we can run
* Cursor queries with correct types rather than passing everything as a string.
*
* Because it is relatively a big object, they are pooled and must be released after each use.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class RoomSQLiteQuery private constructor(
@field:VisibleForTesting val capacity: Int
) : SupportSQLiteQuery, SupportSQLiteProgram {
@Volatile
private var query: String? = null
@JvmField
@VisibleForTesting
val longBindings: LongArray
@JvmField
@VisibleForTesting
val doubleBindings: DoubleArray
@JvmField
@VisibleForTesting
val stringBindings: Array<String?>
@JvmField
@VisibleForTesting
val blobBindings: Array<ByteArray?>
@Binding
private val bindingTypes: IntArray
// number of arguments in the query
override var argCount = 0
private set
fun init(query: String, initArgCount: Int) {
this.query = query
argCount = initArgCount
}
init {
// because, 1 based indices... we don't want to offsets everything with 1 all the time.
val limit = capacity + 1
bindingTypes = IntArray(limit)
longBindings = LongArray(limit)
doubleBindings = DoubleArray(limit)
stringBindings = arrayOfNulls(limit)
blobBindings = arrayOfNulls(limit)
}
/**
* Releases the query back to the pool.
*
* After released, the statement might be returned when [.acquire] is called
* so you should never re-use it after releasing.
*/
fun release() {
synchronized(queryPool) {
queryPool[capacity] = this
prunePoolLocked()
}
}
override val sql: String
get() = checkNotNull(this.query)
override fun bindTo(statement: SupportSQLiteProgram) {
for (index in 1..argCount) {
when (bindingTypes[index]) {
NULL -> statement.bindNull(index)
LONG -> statement.bindLong(index, longBindings[index])
DOUBLE -> statement.bindDouble(index, doubleBindings[index])
STRING -> statement.bindString(index, requireNotNull(stringBindings[index]))
BLOB -> statement.bindBlob(index, requireNotNull(blobBindings[index]))
}
}
}
override fun bindNull(index: Int) {
bindingTypes[index] = NULL
}
override fun bindLong(index: Int, value: Long) {
bindingTypes[index] = LONG
longBindings[index] = value
}
override fun bindDouble(index: Int, value: Double) {
bindingTypes[index] = DOUBLE
doubleBindings[index] = value
}
override fun bindString(index: Int, value: String) {
bindingTypes[index] = STRING
stringBindings[index] = value
}
override fun bindBlob(index: Int, value: ByteArray) {
bindingTypes[index] = BLOB
blobBindings[index] = value
}
override fun close() {
// no-op. not calling release because it is internal API.
}
/**
* Copies arguments from another RoomSQLiteQuery into this query.
*
* @param other The other query, which holds the arguments to be copied.
*/
fun copyArgumentsFrom(other: RoomSQLiteQuery) {
val argCount = other.argCount + 1 // +1 for the binding offsets
System.arraycopy(other.bindingTypes, 0, bindingTypes, 0, argCount)
System.arraycopy(other.longBindings, 0, longBindings, 0, argCount)
System.arraycopy(other.stringBindings, 0, stringBindings, 0, argCount)
System.arraycopy(other.blobBindings, 0, blobBindings, 0, argCount)
System.arraycopy(other.doubleBindings, 0, doubleBindings, 0, argCount)
}
override fun clearBindings() {
Arrays.fill(bindingTypes, NULL)
Arrays.fill(stringBindings, null)
Arrays.fill(blobBindings, null)
query = null
// no need to clear others
}
@Retention(AnnotationRetention.SOURCE)
@IntDef(NULL, LONG, DOUBLE, STRING, BLOB)
internal annotation class Binding
companion object {
// Maximum number of queries we'll keep cached.
@VisibleForTesting
const val POOL_LIMIT = 15
// Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
// clear the bigger queries (# of arguments).
@VisibleForTesting
const val DESIRED_POOL_SIZE = 10
@JvmField
@VisibleForTesting
val queryPool = TreeMap<Int, RoomSQLiteQuery>()
/**
* Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
*
* @param supportSQLiteQuery The query to copy from
* @return A new query copied from the provided one.
*/
@JvmStatic
fun copyFrom(supportSQLiteQuery: SupportSQLiteQuery): RoomSQLiteQuery {
val query = acquire(
supportSQLiteQuery.sql,
supportSQLiteQuery.argCount
)
supportSQLiteQuery.bindTo(object : SupportSQLiteProgram by query {})
return query
}
/**
* Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
* given query.
*
* @param query The query to prepare
* @param argumentCount The number of query arguments
* @return A RoomSQLiteQuery that holds the given query and has space for the given number
* of arguments.
*/
@JvmStatic
fun acquire(query: String, argumentCount: Int): RoomSQLiteQuery {
synchronized(queryPool) {
val entry = queryPool.ceilingEntry(argumentCount)
if (entry != null) {
queryPool.remove(entry.key)
val sqliteQuery = entry.value
sqliteQuery.init(query, argumentCount)
return sqliteQuery
}
}
val sqLiteQuery = RoomSQLiteQuery(argumentCount)
sqLiteQuery.init(query, argumentCount)
return sqLiteQuery
}
internal fun prunePoolLocked() {
if (queryPool.size > POOL_LIMIT) {
var toBeRemoved = queryPool.size - DESIRED_POOL_SIZE
val iterator = queryPool.descendingKeySet().iterator()
while (toBeRemoved-- > 0) {
iterator.next()
iterator.remove()
}
}
}
private const val NULL = 1
private const val LONG = 2
private const val DOUBLE = 3
private const val STRING = 4
private const val BLOB = 5
}
}

View File

@ -1,173 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.annotation.SuppressLint;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.lifecycle.LiveData;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A LiveData implementation that closely works with {@link InvalidationTracker} to implement
* database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long
* as they are active.
* <p>
* We need this extra handling for {@link androidx.lifecycle.LiveData} because when they are
* observed forever, there is no {@link androidx.lifecycle.Lifecycle} that will keep them in
* memory but they should stay. We cannot add-remove observer in {@link LiveData#onActive()},
* {@link LiveData#onInactive()} because that would mean missing changes in between or doing an
* extra query on every UI rotation.
* <p>
* This {@link LiveData} keeps a weak observer to the {@link InvalidationTracker} but it is hold
* strongly by the {@link InvalidationTracker} as long as it is active.
*/
class RoomTrackingLiveData<T> extends LiveData<T> {
@SuppressWarnings("WeakerAccess")
final RoomDatabase mDatabase;
@SuppressWarnings("WeakerAccess")
final boolean mInTransaction;
@SuppressWarnings("WeakerAccess")
final Callable<T> mComputeFunction;
private final InvalidationLiveDataContainer mContainer;
@SuppressWarnings("WeakerAccess")
final InvalidationTracker.Observer mObserver;
private int queued = 0;
private final Object lock = new Object();
@SuppressWarnings("WeakerAccess")
final AtomicBoolean mRegisteredObserver = new AtomicBoolean(false);
@SuppressWarnings("WeakerAccess")
final Runnable mRefreshRunnable = new Runnable() {
@WorkerThread
@Override
public void run() {
synchronized (lock) {
queued--;
if (queued < 0) {
eu.faircode.email.Log.e(mComputeFunction + " queued=" + queued);
queued = 0;
}
}
if (mRegisteredObserver.compareAndSet(false, true)) {
mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
}
T value = null;
boolean computed = false;
synchronized (mComputeFunction) {
int retry = 0;
while (!computed) {
try {
value = mComputeFunction.call();
computed = true;
} catch (Throwable e) {
if (++retry > 5) {
eu.faircode.email.Log.e(e);
break;
}
eu.faircode.email.Log.w(e);
try {
Thread.sleep(2000L);
} catch (InterruptedException ignored) {
}
}
}
}
if (computed) {
postValue(value);
}
}
};
@SuppressWarnings("WeakerAccess")
final Runnable mInvalidationRunnable = new Runnable() {
@MainThread
@Override
public void run() {
boolean isActive = hasActiveObservers();
if (isActive)
synchronized (lock) {
if (queued > 0)
eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug,
mComputeFunction + " queued=" + queued);
else {
queued++;
getQueryExecutor().execute(mRefreshRunnable);
}
}
}
};
@SuppressLint("RestrictedApi")
RoomTrackingLiveData(
RoomDatabase database,
InvalidationLiveDataContainer container,
boolean inTransaction,
Callable<T> computeFunction,
String[] tableNames) {
mDatabase = database;
mInTransaction = inTransaction;
mComputeFunction = computeFunction;
mContainer = container;
mObserver = new InvalidationTracker.Observer(tableNames) {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}
};
}
@Override
protected void onActive() {
super.onActive();
mContainer.onActive(this);
synchronized (lock) {
queued++;
getQueryExecutor().execute(mRefreshRunnable);
}
}
@Override
protected void onInactive() {
super.onInactive();
mContainer.onInactive(this);
}
Executor getQueryExecutor() {
if (mInTransaction) {
return mDatabase.getTransactionExecutor();
} else {
return mDatabase.getQueryExecutor();
}
}
}

View File

@ -0,0 +1,126 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.annotation.SuppressLint
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.lifecycle.LiveData
import java.lang.Exception
import java.lang.RuntimeException
import java.util.concurrent.Callable
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean
/**
* A LiveData implementation that closely works with [InvalidationTracker] to implement
* database drive [androidx.lifecycle.LiveData] queries that are strongly hold as long
* as they are active.
*
* We need this extra handling for [androidx.lifecycle.LiveData] because when they are
* observed forever, there is no [androidx.lifecycle.Lifecycle] that will keep them in
* memory but they should stay. We cannot add-remove observer in [LiveData.onActive],
* [LiveData.onInactive] because that would mean missing changes in between or doing an
* extra query on every UI rotation.
*
* This [LiveData] keeps a weak observer to the [InvalidationTracker] but it is hold
* strongly by the [InvalidationTracker] as long as it is active.
*/
@SuppressLint("RestrictedApi")
internal class RoomTrackingLiveData<T> (
val database: RoomDatabase,
private val container: InvalidationLiveDataContainer,
val inTransaction: Boolean,
val computeFunction: Callable<T?>,
tableNames: Array<out String>
) : LiveData<T>() {
val observer: InvalidationTracker.Observer = object : InvalidationTracker.Observer(tableNames) {
override fun onInvalidated(tables: Set<String>) {
ArchTaskExecutor.getInstance().executeOnMainThread(invalidationRunnable)
}
}
val invalid = AtomicBoolean(true)
val computing = AtomicBoolean(false)
val registeredObserver = AtomicBoolean(false)
val refreshRunnable = Runnable {
if (registeredObserver.compareAndSet(false, true)) {
database.invalidationTracker.addWeakObserver(observer)
}
var computed: Boolean
do {
computed = false
// compute can happen only in 1 thread but no reason to lock others.
if (computing.compareAndSet(false, true)) {
// as long as it is invalid, keep computing.
try {
var value: T? = null
while (invalid.compareAndSet(true, false)) {
computed = true
try {
value = computeFunction.call()
} catch (e: Exception) {
throw RuntimeException(
"Exception while computing database live data.",
e
)
}
}
if (computed) {
postValue(value)
}
} finally {
// release compute lock
computing.set(false)
}
}
// check invalid after releasing compute lock to avoid the following scenario.
// Thread A runs compute()
// Thread A checks invalid, it is false
// Main thread sets invalid to true
// Thread B runs, fails to acquire compute lock and skips
// Thread A releases compute lock
// We've left invalid in set state. The check below recovers.
} while (computed && invalid.get())
}
val invalidationRunnable = Runnable {
val isActive = hasActiveObservers()
if (invalid.compareAndSet(false, true)) {
if (isActive) {
queryExecutor.execute(refreshRunnable)
}
}
}
@Suppress("UNCHECKED_CAST")
override fun onActive() {
super.onActive()
container.onActive(this as LiveData<Any>)
queryExecutor.execute(refreshRunnable)
}
@Suppress("UNCHECKED_CAST")
override fun onInactive() {
super.onInactive()
container.onInactive(this as LiveData<Any>)
}
val queryExecutor: Executor
get() = if (inTransaction) {
database.transactionExecutor
} else {
database.queryExecutor
}
}

View File

@ -1,289 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.room.util.CopyLock;
import androidx.room.util.DBUtil;
import androidx.room.util.FileUtil;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.concurrent.Callable;
/**
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
* storage.
*/
class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
@NonNull
private final Context mContext;
@Nullable
private final String mCopyFromAssetPath;
@Nullable
private final File mCopyFromFile;
@Nullable
private final Callable<InputStream> mCopyFromInputStream;
private final int mDatabaseVersion;
@NonNull
private final SupportSQLiteOpenHelper mDelegate;
@Nullable
private DatabaseConfiguration mDatabaseConfiguration;
private boolean mVerified;
SQLiteCopyOpenHelper(
@NonNull Context context,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
int databaseVersion,
@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) {
mContext = context;
mCopyFromAssetPath = copyFromAssetPath;
mCopyFromFile = copyFromFile;
mCopyFromInputStream = copyFromInputStream;
mDatabaseVersion = databaseVersion;
mDelegate = supportSQLiteOpenHelper;
}
@Override
public String getDatabaseName() {
return mDelegate.getDatabaseName();
}
@Override
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void setWriteAheadLoggingEnabled(boolean enabled) {
mDelegate.setWriteAheadLoggingEnabled(enabled);
}
@Override
public synchronized SupportSQLiteDatabase getWritableDatabase() {
if (!mVerified) {
verifyDatabaseFile(true);
mVerified = true;
}
return mDelegate.getWritableDatabase();
}
@Override
public synchronized SupportSQLiteDatabase getReadableDatabase() {
if (!mVerified) {
verifyDatabaseFile(false);
mVerified = true;
}
return mDelegate.getReadableDatabase();
}
@Override
public synchronized void close() {
mDelegate.close();
mVerified = false;
}
@Override
@NonNull
public SupportSQLiteOpenHelper getDelegate() {
return mDelegate;
}
// Can't be constructor param because the factory is needed by the database builder which in
// turn is the one that actually builds the configuration.
void setDatabaseConfiguration(@Nullable DatabaseConfiguration databaseConfiguration) {
mDatabaseConfiguration = databaseConfiguration;
}
private void verifyDatabaseFile(boolean writable) {
String databaseName = getDatabaseName();
File databaseFile = mContext.getDatabasePath(databaseName);
boolean processLevelLock = mDatabaseConfiguration == null
|| mDatabaseConfiguration.multiInstanceInvalidation;
CopyLock copyLock = new CopyLock(databaseName, mContext.getFilesDir(), processLevelLock);
try {
// Acquire a copy lock, this lock works across threads and processes, preventing
// concurrent copy attempts from occurring.
copyLock.lock();
if (!databaseFile.exists()) {
try {
// No database file found, copy and be done.
copyDatabaseFile(databaseFile, writable);
return;
} catch (IOException e) {
throw new RuntimeException("Unable to copy database file.", e);
}
}
if (mDatabaseConfiguration == null) {
return;
}
// A database file is present, check if we need to re-copy it.
int currentVersion;
try {
currentVersion = DBUtil.readVersion(databaseFile);
} catch (IOException e) {
Log.w(Room.LOG_TAG, "Unable to read database version.", e);
return;
}
if (currentVersion == mDatabaseVersion) {
return;
}
if (mDatabaseConfiguration.isMigrationRequired(currentVersion, mDatabaseVersion)) {
// From the current version to the desired version a migration is required, i.e.
// we won't be performing a copy destructive migration.
return;
}
if (mContext.deleteDatabase(databaseName)) {
try {
copyDatabaseFile(databaseFile, writable);
} catch (IOException e) {
// We are more forgiving copying a database on a destructive migration since
// there is already a database file that can be opened.
Log.w(Room.LOG_TAG, "Unable to copy database file.", e);
}
} else {
Log.w(Room.LOG_TAG, "Failed to delete database file ("
+ databaseName + ") for a copy destructive migration.");
}
} finally {
copyLock.unlock();
}
}
private void copyDatabaseFile(File destinationFile, boolean writable) throws IOException {
ReadableByteChannel input;
if (mCopyFromAssetPath != null) {
input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath));
} else if (mCopyFromFile != null) {
input = new FileInputStream(mCopyFromFile).getChannel();
} else if (mCopyFromInputStream != null) {
final InputStream inputStream;
try {
inputStream = mCopyFromInputStream.call();
} catch (Exception e) {
throw new IOException("inputStreamCallable exception on call", e);
}
input = Channels.newChannel(inputStream);
} else {
throw new IllegalStateException("copyFromAssetPath, copyFromFile and "
+ "copyFromInputStream are all null!");
}
// An intermediate file is used so that we never end up with a half-copied database file
// in the internal directory.
//File intermediateFile = File.createTempFile(
// "room-copy-helper", ".tmp", mContext.getCacheDir());
File dir = new File(mContext.getFilesDir(), "room");
if (!dir.exists() && !dir.mkdirs())
throw new IOException("Failed to create=" + dir);
File intermediateFile = new File(dir, "room-copy-helper.tmp");
intermediateFile.deleteOnExit();
FileChannel output = new FileOutputStream(intermediateFile).getChannel();
FileUtil.copy(input, output);
File parent = destinationFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException("Failed to create directories for "
+ destinationFile.getAbsolutePath());
}
// Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch
// the open pre-packaged callback. If it fails then intermediate file won't be copied making
// invoking pre-packaged callback a transactional operation.
dispatchOnOpenPrepackagedDatabase(intermediateFile, writable);
if (!intermediateFile.renameTo(destinationFile)) {
throw new IOException("Failed to move intermediate file ("
+ intermediateFile.getAbsolutePath() + ") to destination ("
+ destinationFile.getAbsolutePath() + ").");
}
}
private void dispatchOnOpenPrepackagedDatabase(File databaseFile, boolean writable) {
if (mDatabaseConfiguration == null
|| mDatabaseConfiguration.prepackagedDatabaseCallback == null) {
return;
}
SupportSQLiteOpenHelper helper = createFrameworkOpenHelper(databaseFile);
try {
SupportSQLiteDatabase db = writable ? helper.getWritableDatabase() :
helper.getReadableDatabase();
mDatabaseConfiguration.prepackagedDatabaseCallback.onOpenPrepackagedDatabase(db);
} finally {
// Close the db and let Room re-open it through a normal path
helper.close();
}
}
private SupportSQLiteOpenHelper createFrameworkOpenHelper(File databaseFile) {
final int version;
try {
version = DBUtil.readVersion(databaseFile);
} catch (IOException e) {
throw new RuntimeException("Malformed database file, unable to read version.", e);
}
FrameworkSQLiteOpenHelperFactory factory = new FrameworkSQLiteOpenHelperFactory();
Configuration configuration = Configuration.builder(mContext)
.name(databaseFile.getAbsolutePath())
.callback(new Callback(Math.max(version, 1)) {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
}
@Override
public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion,
int newVersion) {
}
@Override
public void onOpen(@NonNull SupportSQLiteDatabase db) {
// If pre-packaged database has a version < 1 we will open it as if it was
// version 1 because the framework open helper does not allow version < 1.
// The database will be considered as newly created and onCreate() will be
// invoked, but we do nothing and reset the version back so Room later runs
// migrations as usual.
if (version < 1) {
db.setVersion(version);
}
}
})
.build();
return factory.create(configuration);
}
}

View File

@ -0,0 +1,242 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.room.Room.LOG_TAG
import androidx.room.util.copy
import androidx.room.util.readVersion
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.sqlite.util.ProcessLock
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.lang.Exception
import java.lang.IllegalStateException
import java.lang.RuntimeException
import java.nio.channels.Channels
import java.nio.channels.ReadableByteChannel
import java.util.concurrent.Callable
/**
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
* storage.
*/
@Suppress("BanSynchronizedMethods")
internal class SQLiteCopyOpenHelper(
private val context: Context,
private val copyFromAssetPath: String?,
private val copyFromFile: File?,
private val copyFromInputStream: Callable<InputStream>?,
private val databaseVersion: Int,
override val delegate: SupportSQLiteOpenHelper
) : SupportSQLiteOpenHelper, DelegatingOpenHelper {
private lateinit var databaseConfiguration: DatabaseConfiguration
private var verified = false
override val databaseName: String?
get() = delegate.databaseName
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
delegate.setWriteAheadLoggingEnabled(enabled)
}
override val writableDatabase: SupportSQLiteDatabase
get() {
if (!verified) {
verifyDatabaseFile(true)
verified = true
}
return delegate.writableDatabase
}
override val readableDatabase: SupportSQLiteDatabase
get() {
if (!verified) {
verifyDatabaseFile(false)
verified = true
}
return delegate.readableDatabase
}
@Synchronized
override fun close() {
delegate.close()
verified = false
}
// Can't be constructor param because the factory is needed by the database builder which in
// turn is the one that actually builds the configuration.
fun setDatabaseConfiguration(databaseConfiguration: DatabaseConfiguration) {
this.databaseConfiguration = databaseConfiguration
}
private fun verifyDatabaseFile(writable: Boolean) {
val name = checkNotNull(databaseName)
val databaseFile = context.getDatabasePath(name)
val processLevelLock = (databaseConfiguration.multiInstanceInvalidation)
val copyLock = ProcessLock(
name,
context.filesDir,
processLevelLock
)
try {
// Acquire a copy lock, this lock works across threads and processes, preventing
// concurrent copy attempts from occurring.
copyLock.lock()
if (!databaseFile.exists()) {
try {
// No database file found, copy and be done.
copyDatabaseFile(databaseFile, writable)
return
} catch (e: IOException) {
throw RuntimeException("Unable to copy database file.", e)
}
}
// A database file is present, check if we need to re-copy it.
val currentVersion = try {
readVersion(databaseFile)
} catch (e: IOException) {
Log.w(LOG_TAG, "Unable to read database version.", e)
return
}
if (currentVersion == databaseVersion) {
return
}
if (databaseConfiguration.isMigrationRequired(currentVersion, databaseVersion)) {
// From the current version to the desired version a migration is required, i.e.
// we won't be performing a copy destructive migration.
return
}
if (context.deleteDatabase(name)) {
try {
copyDatabaseFile(databaseFile, writable)
} catch (e: IOException) {
// We are more forgiving copying a database on a destructive migration since
// there is already a database file that can be opened.
Log.w(LOG_TAG, "Unable to copy database file.", e)
}
} else {
Log.w(
LOG_TAG, "Failed to delete database file ($name) for " +
"a copy destructive migration."
)
}
} finally {
copyLock.unlock()
}
}
@Throws(IOException::class)
private fun copyDatabaseFile(destinationFile: File, writable: Boolean) {
val input: ReadableByteChannel
if (copyFromAssetPath != null) {
input = Channels.newChannel(context.assets.open(copyFromAssetPath))
} else if (copyFromFile != null) {
input = FileInputStream(copyFromFile).channel
} else if (copyFromInputStream != null) {
val inputStream = try {
copyFromInputStream.call()
} catch (e: Exception) {
throw IOException("inputStreamCallable exception on call", e)
}
input = Channels.newChannel(inputStream)
} else {
throw IllegalStateException(
"copyFromAssetPath, copyFromFile and copyFromInputStream are all null!"
)
}
// An intermediate file is used so that we never end up with a half-copied database file
// in the internal directory.
val intermediateFile = File.createTempFile(
"room-copy-helper", ".tmp", context.cacheDir
)
intermediateFile.deleteOnExit()
val output = FileOutputStream(intermediateFile).channel
copy(input, output)
val parent = destinationFile.parentFile
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw IOException(
"Failed to create directories for ${destinationFile.absolutePath}"
)
}
// Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch
// the open pre-packaged callback. If it fails then intermediate file won't be copied making
// invoking pre-packaged callback a transactional operation.
dispatchOnOpenPrepackagedDatabase(intermediateFile, writable)
if (!intermediateFile.renameTo(destinationFile)) {
throw IOException(
"Failed to move intermediate file (${intermediateFile.absolutePath}) to " +
"destination (${destinationFile.absolutePath})."
)
}
}
private fun dispatchOnOpenPrepackagedDatabase(databaseFile: File, writable: Boolean) {
if (databaseConfiguration.prepackagedDatabaseCallback == null
) {
return
}
createFrameworkOpenHelper(databaseFile).use { helper ->
val db = if (writable) helper.writableDatabase else helper.readableDatabase
databaseConfiguration.prepackagedDatabaseCallback!!.onOpenPrepackagedDatabase(db)
}
}
private fun createFrameworkOpenHelper(databaseFile: File): SupportSQLiteOpenHelper {
val version = try {
readVersion(databaseFile)
} catch (e: IOException) {
throw RuntimeException("Malformed database file, unable to read version.", e)
}
val factory = FrameworkSQLiteOpenHelperFactory()
val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(databaseFile.absolutePath)
.callback(object : SupportSQLiteOpenHelper.Callback(version.coerceAtLeast(1)) {
override fun onCreate(db: SupportSQLiteDatabase) {}
override fun onUpgrade(
db: SupportSQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
}
override fun onOpen(db: SupportSQLiteDatabase) {
// If pre-packaged database has a version < 1 we will open it as if it was
// version 1 because the framework open helper does not allow version < 1.
// The database will be considered as newly created and onCreate() will be
// invoked, but we do nothing and reset the version back so Room later runs
// migrations as usual.
if (version < 1) {
db.version = version
}
}
})
.build()
return factory.create(configuration)
}
}

View File

@ -1,64 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.File;
import java.io.InputStream;
import java.util.concurrent.Callable;
/**
* Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
* {@link SQLiteCopyOpenHelper}.
*/
class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
@Nullable
private final String mCopyFromAssetPath;
@Nullable
private final File mCopyFromFile;
@Nullable
private final Callable<InputStream> mCopyFromInputStream;
@NonNull
private final SupportSQLiteOpenHelper.Factory mDelegate;
SQLiteCopyOpenHelperFactory(
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> 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));
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.sqlite.db.SupportSQLiteOpenHelper
import java.io.File
import java.io.InputStream
import java.util.concurrent.Callable
/**
* Implementation of [SupportSQLiteOpenHelper.Factory] that creates
* [SQLiteCopyOpenHelper].
*/
internal class SQLiteCopyOpenHelperFactory(
private val mCopyFromAssetPath: String?,
private val mCopyFromFile: File?,
private val mCopyFromInputStream: Callable<InputStream>?,
private val mDelegate: SupportSQLiteOpenHelper.Factory
) : SupportSQLiteOpenHelper.Factory {
override fun create(
configuration: SupportSQLiteOpenHelper.Configuration
): SupportSQLiteOpenHelper {
return SQLiteCopyOpenHelper(
configuration.context,
mCopyFromAssetPath,
mCopyFromFile,
mCopyFromInputStream,
configuration.callback.version,
mDelegate.create(configuration)
)
}
}

View File

@ -1,100 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Represents a prepared SQLite state that can be re-used multiple times.
* <p>
* This class is used by generated code. After it is used, {@code release} must be called so that
* it can be used by other threads.
* <p>
* To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
* statement until it is released.
*
* @hide
*/
@SuppressWarnings({"WeakerAccess", "unused"})
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public abstract class SharedSQLiteStatement {
private final AtomicBoolean mLock = new AtomicBoolean(false);
private final RoomDatabase mDatabase;
private volatile SupportSQLiteStatement mStmt;
/**
* Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
* it automatically creates a new one.
*
* @param database The database to create the statement in.
*/
public SharedSQLiteStatement(RoomDatabase database) {
mDatabase = database;
}
/**
* Create the query.
*
* @return The SQL query to prepare.
*/
protected abstract String createQuery();
protected void assertNotMainThread() {
mDatabase.assertNotMainThread();
}
private SupportSQLiteStatement createNewStatement() {
String query = createQuery();
return mDatabase.compileStatement(query);
}
private SupportSQLiteStatement getStmt(boolean canUseCached) {
final SupportSQLiteStatement stmt;
if (canUseCached) {
if (mStmt == null) {
mStmt = createNewStatement();
}
stmt = mStmt;
} else {
// it is in use, create a one off statement
stmt = createNewStatement();
}
return stmt;
}
/**
* Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done.
*/
public SupportSQLiteStatement acquire() {
assertNotMainThread();
return getStmt(mLock.compareAndSet(false, true));
}
/**
* Must call this when statement will not be used anymore.
*
* @param statement The statement that was returned from acquire.
*/
public void release(SupportSQLiteStatement statement) {
if (statement == mStmt) {
mLock.set(false);
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
import androidx.annotation.RestrictTo
import androidx.sqlite.db.SupportSQLiteStatement
import java.util.concurrent.atomic.AtomicBoolean
/**
* Represents a prepared SQLite state that can be re-used multiple times.
*
* This class is used by generated code. After it is used, `release` must be called so that
* it can be used by other threads.
*
* To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
* statement until it is released.
*
* @constructor Creates an SQLite prepared statement that can be re-used across threads. If it is
* in use, it automatically creates a new one.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
abstract class SharedSQLiteStatement(private val database: RoomDatabase) {
private val lock = AtomicBoolean(false)
private val stmt: SupportSQLiteStatement by lazy {
createNewStatement()
}
/**
* Create the query.
*
* @return The SQL query to prepare.
*/
protected abstract fun createQuery(): String
protected open fun assertNotMainThread() {
database.assertNotMainThread()
}
private fun createNewStatement(): SupportSQLiteStatement {
val query = createQuery()
return database.compileStatement(query)
}
private fun getStmt(canUseCached: Boolean): SupportSQLiteStatement {
val stmt = if (canUseCached) {
stmt
} else {
// it is in use, create a one off statement
createNewStatement()
}
return stmt
}
/**
* Call this to get the statement. Must call [.release] once done.
*/
open fun acquire(): SupportSQLiteStatement {
assertNotMainThread()
return getStmt(lock.compareAndSet(false, true))
}
/**
* Must call this when statement will not be used anymore.
*
* @param statement The statement that was returned from acquire.
*/
open fun release(statement: SupportSQLiteStatement) {
if (statement === stmt) {
lock.set(false)
}
}
}

View File

@ -1,47 +0,0 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
/**
* Java 8 Sneaky Throw technique.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class SneakyThrow {
/**
* Re-throws a checked exception as if it was a runtime exception without wrapping it.
*
* @param e the exception to re-throw.
*/
public static void reThrow(@NonNull Exception e) {
sneakyThrow(e);
}
@SuppressWarnings("unchecked")
private static <E extends Throwable> void sneakyThrow(@NonNull Throwable e) throws E {
throw (E) e;
}
private SneakyThrow() {
}
}

View File

@ -1,118 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
/**
* @hide
*
* String utilities for Room
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class StringUtil {
@SuppressWarnings("unused")
public static final String[] EMPTY_STRING_ARRAY = new String[0];
/**
* Returns a new StringBuilder to be used while producing SQL queries.
*
* @return A new or recycled StringBuilder
*/
public static StringBuilder newStringBuilder() {
// TODO pool:
return new StringBuilder();
}
/**
* Adds bind variable placeholders (?) to the given string. Each placeholder is separated
* by a comma.
*
* @param builder The StringBuilder for the query
* @param count Number of placeholders
*/
public static void appendPlaceholders(StringBuilder builder, int count) {
for (int i = 0; i < count; i++) {
builder.append("?");
if (i < count - 1) {
builder.append(",");
}
}
}
/**
* Splits a comma separated list of integers to integer list.
* <p>
* If an input is malformed, it is omitted from the result.
*
* @param input Comma separated list of integers.
* @return A List containing the integers or null if the input is null.
*/
@Nullable
public static List<Integer> splitToIntList(@Nullable String input) {
if (input == null) {
return null;
}
List<Integer> result = new ArrayList<>();
StringTokenizer tokenizer = new StringTokenizer(input, ",");
while (tokenizer.hasMoreElements()) {
final String item = tokenizer.nextToken();
try {
result.add(Integer.parseInt(item));
} catch (NumberFormatException ex) {
Log.e("ROOM", "Malformed integer list", ex);
}
}
return result;
}
/**
* Joins the given list of integers into a comma separated list.
*
* @param input The list of integers.
* @return Comma separated string composed of integers in the list. If the list is null, return
* value is null.
*/
@Nullable
public static String joinIntoString(@Nullable List<Integer> input) {
if (input == null) {
return null;
}
final int size = input.size();
if (size == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
sb.append(Integer.toString(input.get(i)));
if (i < size - 1) {
sb.append(",");
}
}
return sb.toString();
}
private StringUtil() {
}
}

View File

@ -1,743 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.Cursor;
import android.os.Build;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.room.ColumnInfo;
import androidx.sqlite.db.SupportSQLiteDatabase;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* A data class that holds the information about a table.
* <p>
* It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the
* <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a>
* documentation for more details.
* <p>
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
"SimplifiableIfStatement"})
// if you change this class, you must change TableInfoValidationWriter.kt
public final class TableInfo {
/**
* Identifies from where the info object was created.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE})
@interface CreatedFrom {
}
/**
* Identifier for when the info is created from an unknown source.
*/
public static final int CREATED_FROM_UNKNOWN = 0;
/**
* Identifier for when the info is created from an entity definition, such as generated code
* by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
*/
public static final int CREATED_FROM_ENTITY = 1;
/**
* Identifier for when the info is created from the database itself, reading information from a
* PRAGMA, such as table_info.
*/
public static final int CREATED_FROM_DATABASE = 2;
/**
* The table name.
*/
public final String name;
/**
* Unmodifiable map of columns keyed by column name.
*/
public final Map<String, Column> columns;
public final Set<ForeignKey> foreignKeys;
/**
* Sometimes, Index information is not available (older versions). If so, we skip their
* verification.
*/
@Nullable
public final Set<Index> indices;
@SuppressWarnings("unused")
public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys,
Set<Index> indices) {
this.name = name;
this.columns = Collections.unmodifiableMap(columns);
this.foreignKeys = Collections.unmodifiableSet(foreignKeys);
this.indices = indices == null ? null : Collections.unmodifiableSet(indices);
}
/**
* For backward compatibility with dbs created with older versions.
*/
@SuppressWarnings("unused")
public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
this(name, columns, foreignKeys, Collections.<Index>emptySet());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TableInfo)) return false;
TableInfo tableInfo = (TableInfo) o;
if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) {
return false;
}
if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys)
: tableInfo.foreignKeys != null) {
return false;
}
if (indices == null || tableInfo.indices == null) {
// if one us is missing index information, seems like we couldn't acquire the
// information so we better skip.
return true;
}
return indices.equals(tableInfo.indices);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (columns != null ? columns.hashCode() : 0);
result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0);
// skip index, it is not reliable for comparison.
return result;
}
@Override
public String toString() {
return "TableInfo{"
+ "name='" + name + '\''
+ ", columns=" + columns
+ ", foreignKeys=" + foreignKeys
+ ", indices=" + indices
+ '}';
}
/**
* Reads the table information from the given database.
*
* @param database The database to read the information from.
* @param tableName The table name.
* @return A TableInfo containing the schema information for the provided table name.
*/
@SuppressWarnings("SameParameterValue")
public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
Map<String, Column> columns = readColumns(database, tableName);
Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName);
Set<Index> indices = readIndices(database, tableName);
return new TableInfo(tableName, columns, foreignKeys, indices);
}
private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database,
String tableName) {
Set<ForeignKey> foreignKeys = new HashSet<>();
// this seems to return everything in order but it is not documented so better be safe
Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)");
try {
final int idColumnIndex = cursor.getColumnIndex("id");
final int seqColumnIndex = cursor.getColumnIndex("seq");
final int tableColumnIndex = cursor.getColumnIndex("table");
final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete");
final int onUpdateColumnIndex = cursor.getColumnIndex("on_update");
final List<ForeignKeyWithSequence> ordered = readForeignKeyFieldMappings(cursor);
final int count = cursor.getCount();
for (int position = 0; position < count; position++) {
cursor.moveToPosition(position);
final int seq = cursor.getInt(seqColumnIndex);
if (seq != 0) {
continue;
}
final int id = cursor.getInt(idColumnIndex);
List<String> myColumns = new ArrayList<>();
List<String> refColumns = new ArrayList<>();
for (ForeignKeyWithSequence key : ordered) {
if (key.mId == id) {
myColumns.add(key.mFrom);
refColumns.add(key.mTo);
}
}
foreignKeys.add(new ForeignKey(
cursor.getString(tableColumnIndex),
cursor.getString(onDeleteColumnIndex),
cursor.getString(onUpdateColumnIndex),
myColumns,
refColumns
));
}
} finally {
cursor.close();
}
return foreignKeys;
}
private static List<ForeignKeyWithSequence> readForeignKeyFieldMappings(Cursor cursor) {
final int idColumnIndex = cursor.getColumnIndex("id");
final int seqColumnIndex = cursor.getColumnIndex("seq");
final int fromColumnIndex = cursor.getColumnIndex("from");
final int toColumnIndex = cursor.getColumnIndex("to");
final int count = cursor.getCount();
List<ForeignKeyWithSequence> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
cursor.moveToPosition(i);
result.add(new ForeignKeyWithSequence(
cursor.getInt(idColumnIndex),
cursor.getInt(seqColumnIndex),
cursor.getString(fromColumnIndex),
cursor.getString(toColumnIndex)
));
}
Collections.sort(result);
return result;
}
private static Map<String, Column> readColumns(SupportSQLiteDatabase database,
String tableName) {
Cursor cursor = database
.query("PRAGMA table_info(`" + tableName + "`)");
//noinspection TryFinallyCanBeTryWithResources
Map<String, Column> columns = new HashMap<>();
try {
if (cursor.getColumnCount() > 0) {
int nameIndex = cursor.getColumnIndex("name");
int typeIndex = cursor.getColumnIndex("type");
int notNullIndex = cursor.getColumnIndex("notnull");
int pkIndex = cursor.getColumnIndex("pk");
int defaultValueIndex = cursor.getColumnIndex("dflt_value");
while (cursor.moveToNext()) {
final String name = cursor.getString(nameIndex);
final String type = cursor.getString(typeIndex);
final boolean notNull = 0 != cursor.getInt(notNullIndex);
final int primaryKeyPosition = cursor.getInt(pkIndex);
final String defaultValue = cursor.getString(defaultValueIndex);
columns.put(name,
new Column(name, type, notNull, primaryKeyPosition, defaultValue,
CREATED_FROM_DATABASE));
}
}
} finally {
cursor.close();
}
return columns;
}
/**
* @return null if we cannot read the indices due to older sqlite implementations.
*/
@Nullable
private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) {
Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)");
try {
final int nameColumnIndex = cursor.getColumnIndex("name");
final int originColumnIndex = cursor.getColumnIndex("origin");
final int uniqueIndex = cursor.getColumnIndex("unique");
if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
// we cannot read them so better not validate any index.
return null;
}
HashSet<Index> indices = new HashSet<>();
while (cursor.moveToNext()) {
String origin = cursor.getString(originColumnIndex);
if (!"c".equals(origin)) {
// Ignore auto-created indices
continue;
}
String name = cursor.getString(nameColumnIndex);
boolean unique = cursor.getInt(uniqueIndex) == 1;
Index index = readIndex(database, name, unique);
if (index == null) {
// we cannot read it properly so better not read it
return null;
}
indices.add(index);
}
return indices;
} finally {
cursor.close();
}
}
/**
* @return null if we cannot read the index due to older sqlite implementations.
*/
@Nullable
private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) {
Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)");
try {
final int seqnoColumnIndex = cursor.getColumnIndex("seqno");
final int cidColumnIndex = cursor.getColumnIndex("cid");
final int nameColumnIndex = cursor.getColumnIndex("name");
final int descColumnIndex = cursor.getColumnIndex("desc");
if (seqnoColumnIndex == -1 || cidColumnIndex == -1
|| nameColumnIndex == -1 || descColumnIndex == -1) {
// we cannot read them so better not validate any index.
return null;
}
final TreeMap<Integer, String> columnsMap = new TreeMap<>();
final TreeMap<Integer, String> ordersMap = new TreeMap<>();
while (cursor.moveToNext()) {
int cid = cursor.getInt(cidColumnIndex);
if (cid < 0) {
// Ignore SQLite row ID
continue;
}
int seq = cursor.getInt(seqnoColumnIndex);
String columnName = cursor.getString(nameColumnIndex);
String order = cursor.getInt(descColumnIndex) > 0 ? "DESC" : "ASC";
columnsMap.put(seq, columnName);
ordersMap.put(seq, order);
}
final List<String> columns = new ArrayList<>(columnsMap.size());
columns.addAll(columnsMap.values());
final List<String> orders = new ArrayList<>(ordersMap.size());
orders.addAll(ordersMap.values());
return new Index(name, unique, columns, orders);
} finally {
cursor.close();
}
}
/**
* Holds the information about a database column.
*/
@SuppressWarnings("WeakerAccess")
public static final class Column {
/**
* The column name.
*/
public final String name;
/**
* The column type affinity.
*/
public final String type;
/**
* The column type after it is normalized to one of the basic types according to
* https://www.sqlite.org/datatype3.html Section 3.1.
* <p>
* This is the value Room uses for equality check.
*/
@ColumnInfo.SQLiteTypeAffinity
public final int affinity;
/**
* Whether or not the column can be NULL.
*/
public final boolean notNull;
/**
* The position of the column in the list of primary keys, 0 if the column is not part
* of the primary key.
* <p>
* This information is only available in API 20+.
* <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a>
* On older platforms, it will be 1 if the column is part of the primary key and 0
* otherwise.
* <p>
* The {@link #equals(Object)} implementation handles this inconsistency based on
* API levels os if you are using a custom SQLite deployment, it may return false
* positives.
*/
public final int primaryKeyPosition;
/**
* The default value of this column.
*/
public final String defaultValue;
@CreatedFrom
private final int mCreatedFrom;
/**
* @deprecated Use {@link Column#Column(String, String, boolean, int, String, int)} instead.
*/
@Deprecated
public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
this(name, type, notNull, primaryKeyPosition, null, CREATED_FROM_UNKNOWN);
}
// if you change this constructor, you must change TableInfoWriter.kt
public Column(String name, String type, boolean notNull, int primaryKeyPosition,
String defaultValue, @CreatedFrom int createdFrom) {
this.name = name;
this.type = type;
this.notNull = notNull;
this.primaryKeyPosition = primaryKeyPosition;
this.affinity = findAffinity(type);
this.defaultValue = defaultValue;
this.mCreatedFrom = createdFrom;
}
/**
* Implements https://www.sqlite.org/datatype3.html section 3.1
*
* @param type The type that was given to the sqlite
* @return The normalized type which is one of the 5 known affinities
*/
@ColumnInfo.SQLiteTypeAffinity
private static int findAffinity(@Nullable String type) {
if (type == null) {
return ColumnInfo.BLOB;
}
String uppercaseType = type.toUpperCase(Locale.US);
if (uppercaseType.contains("INT")) {
return ColumnInfo.INTEGER;
}
if (uppercaseType.contains("CHAR")
|| uppercaseType.contains("CLOB")
|| uppercaseType.contains("TEXT")) {
return ColumnInfo.TEXT;
}
if (uppercaseType.contains("BLOB")) {
return ColumnInfo.BLOB;
}
if (uppercaseType.contains("REAL")
|| uppercaseType.contains("FLOA")
|| uppercaseType.contains("DOUB")) {
return ColumnInfo.REAL;
}
// sqlite returns NUMERIC here but it is like a catch all. We already
// have UNDEFINED so it is better to use UNDEFINED for consistency.
return ColumnInfo.UNDEFINED;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Column)) return false;
Column column = (Column) o;
if (Build.VERSION.SDK_INT >= 20) {
if (primaryKeyPosition != column.primaryKeyPosition) return false;
} else {
if (isPrimaryKey() != column.isPrimaryKey()) return false;
}
if (!name.equals(column.name)) return false;
//noinspection SimplifiableIfStatement
if (notNull != column.notNull) return false;
// Only validate default value if it was defined in an entity, i.e. if the info
// from the compiler itself has it. b/136019383
if (mCreatedFrom == CREATED_FROM_ENTITY
&& column.mCreatedFrom == CREATED_FROM_DATABASE
&& (defaultValue != null && !defaultValueEquals(defaultValue,
column.defaultValue))) {
return false;
} else if (mCreatedFrom == CREATED_FROM_DATABASE
&& column.mCreatedFrom == CREATED_FROM_ENTITY
&& (column.defaultValue != null && !defaultValueEquals(
column.defaultValue, defaultValue))) {
return false;
} else if (mCreatedFrom != CREATED_FROM_UNKNOWN
&& mCreatedFrom == column.mCreatedFrom
&& (defaultValue != null ? !defaultValueEquals(defaultValue,
column.defaultValue)
: column.defaultValue != null)) {
return false;
}
return affinity == column.affinity;
}
/**
* Checks if the default values provided match. Handles the special case in which the
* default value is surrounded by parenthesis (e.g. encountered in b/182284899).
*
* Surrounding parenthesis are removed by SQLite when reading from the database, hence
* this function will check if they are present in the actual value, if so, it will
* compare the two values by ignoring the surrounding parenthesis.
*
*/
public static boolean defaultValueEquals(@NonNull String actual, @Nullable String other) {
if (other == null) {
return false;
}
if (actual.equals(other)) {
return true;
} else if (containsSurroundingParenthesis(actual)) {
return actual.substring(1, actual.length() - 1).trim().equals(other);
}
return false;
}
/**
* Checks for potential surrounding parenthesis, if found, removes them and checks if
* remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant,
* and returns true.
*/
private static boolean containsSurroundingParenthesis(@NonNull String actual) {
if (actual.length() == 0) {
return false;
}
int surroundingParenthesis = 0;
for (int i = 0; i < actual.length(); i++) {
char c = actual.charAt(i);
if (i == 0 && c != '(') {
return false;
}
if (c == '(') {
surroundingParenthesis++;
} else if (c == ')') {
surroundingParenthesis--;
if (surroundingParenthesis == 0 && i != actual.length() - 1) {
return false;
}
}
}
return surroundingParenthesis == 0;
}
/**
* Returns whether this column is part of the primary key or not.
*
* @return True if this column is part of the primary key, false otherwise.
*/
public boolean isPrimaryKey() {
return primaryKeyPosition > 0;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + affinity;
result = 31 * result + (notNull ? 1231 : 1237);
result = 31 * result + primaryKeyPosition;
// Default value is not part of the hashcode since we conditionally check it for
// equality which would break the equals + hashcode contract.
// result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Column{"
+ "name='" + name + '\''
+ ", type='" + type + '\''
+ ", affinity='" + affinity + '\''
+ ", notNull=" + notNull
+ ", primaryKeyPosition=" + primaryKeyPosition
+ ", defaultValue='" + defaultValue + '\''
+ '}';
}
}
/**
* Holds the information about an SQLite foreign key
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static final class ForeignKey {
@NonNull
public final String referenceTable;
@NonNull
public final String onDelete;
@NonNull
public final String onUpdate;
@NonNull
public final List<String> columnNames;
@NonNull
public final List<String> referenceColumnNames;
public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete,
@NonNull String onUpdate,
@NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames) {
this.referenceTable = referenceTable;
this.onDelete = onDelete;
this.onUpdate = onUpdate;
this.columnNames = Collections.unmodifiableList(columnNames);
this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ForeignKey)) return false;
ForeignKey that = (ForeignKey) o;
if (!referenceTable.equals(that.referenceTable)) return false;
if (!onDelete.equals(that.onDelete)) return false;
if (!onUpdate.equals(that.onUpdate)) return false;
//noinspection SimplifiableIfStatement
if (!columnNames.equals(that.columnNames)) return false;
return referenceColumnNames.equals(that.referenceColumnNames);
}
@Override
public int hashCode() {
int result = referenceTable.hashCode();
result = 31 * result + onDelete.hashCode();
result = 31 * result + onUpdate.hashCode();
result = 31 * result + columnNames.hashCode();
result = 31 * result + referenceColumnNames.hashCode();
return result;
}
@Override
public String toString() {
return "ForeignKey{"
+ "referenceTable='" + referenceTable + '\''
+ ", onDelete='" + onDelete + '\''
+ ", onUpdate='" + onUpdate + '\''
+ ", columnNames=" + columnNames
+ ", referenceColumnNames=" + referenceColumnNames
+ '}';
}
}
/**
* Temporary data holder for a foreign key row in the pragma result. We need this to ensure
* sorting in the generated foreign key object.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
static class ForeignKeyWithSequence implements Comparable<ForeignKeyWithSequence> {
final int mId;
final int mSequence;
final String mFrom;
final String mTo;
ForeignKeyWithSequence(int id, int sequence, String from, String to) {
mId = id;
mSequence = sequence;
mFrom = from;
mTo = to;
}
@Override
public int compareTo(@NonNull ForeignKeyWithSequence o) {
final int idCmp = mId - o.mId;
if (idCmp == 0) {
return mSequence - o.mSequence;
} else {
return idCmp;
}
}
}
/**
* Holds the information about an SQLite index
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static final class Index {
// should match the value in Index.kt
public static final String DEFAULT_PREFIX = "index_";
public final String name;
public final boolean unique;
public final List<String> columns;
public final List<String> orders;
/**
* @deprecated Use {@link #Index(String, boolean, List, List)}
*/
public Index(String name, boolean unique, List<String> columns) {
this(name, unique, columns, null);
}
public Index(String name, boolean unique, List<String> columns, List<String> orders) {
this.name = name;
this.unique = unique;
this.columns = columns;
this.orders = orders == null || orders.size() == 0
? Collections.nCopies(columns.size(), androidx.room.Index.Order.ASC.name())
: orders;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Index)) return false;
Index index = (Index) o;
if (unique != index.unique) {
return false;
}
if (!columns.equals(index.columns)) {
return false;
}
if (!orders.equals(index.orders)) {
return false;
}
if (name.startsWith(Index.DEFAULT_PREFIX)) {
return index.name.startsWith(Index.DEFAULT_PREFIX);
} else {
return name.equals(index.name);
}
}
@Override
public int hashCode() {
int result;
if (name.startsWith(DEFAULT_PREFIX)) {
result = DEFAULT_PREFIX.hashCode();
} else {
result = name.hashCode();
}
result = 31 * result + (unique ? 1 : 0);
result = 31 * result + columns.hashCode();
result = 31 * result + orders.hashCode();
return result;
}
@Override
public String toString() {
return "Index{"
+ "name='" + name + '\''
+ ", unique=" + unique
+ ", columns=" + columns
+ ", orders=" + orders
+ '}';
}
}
}

View File

@ -13,52 +13,42 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room
package androidx.room;
import androidx.annotation.NonNull;
import java.util.ArrayDeque;
import java.util.concurrent.Executor;
import java.util.ArrayDeque
import java.util.concurrent.Executor
/**
* Executor wrapper for performing database transactions serially.
* <p>
*
* Since database transactions are exclusive, this executor ensures that transactions are performed
* in-order and one at a time, preventing threads from blocking each other when multiple concurrent
* transactions are attempted.
*/
class TransactionExecutor implements Executor {
private final Executor mExecutor;
private final ArrayDeque<Runnable> mTasks = new ArrayDeque<>();
private Runnable mActive;
TransactionExecutor(@NonNull Executor executor) {
mExecutor = executor;
}
@Override
public synchronized void execute(final Runnable command) {
mTasks.offer(new Runnable() {
@Override
public void run() {
internal class TransactionExecutor(private val executor: Executor) : Executor {
private val tasks = ArrayDeque<Runnable>()
private var active: Runnable? = null
private val syncLock = Any()
override fun execute(command: Runnable) {
synchronized(syncLock) {
tasks.offer(Runnable {
try {
command.run();
command.run()
} finally {
scheduleNext();
scheduleNext()
}
})
if (active == null) {
scheduleNext()
}
});
if (mActive == null) {
scheduleNext();
}
}
@SuppressWarnings("WeakerAccess")
synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
mExecutor.execute(mActive);
fun scheduleNext() {
synchronized(syncLock) {
if (tasks.poll().also { active = it } != null) {
executor.execute(active)
}
}
}
}

View File

@ -1,64 +0,0 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* UUID / byte[] two-way conversion utility for Room
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public final class UUIDUtil {
// private constructor to prevent instantiation
private UUIDUtil() {}
/**
* Converts a 16-bytes array BLOB into a UUID pojo
*
* @param bytes byte array stored in database as BLOB
* @return a UUID object created based on the provided byte array
*/
@NonNull
public static UUID convertByteToUUID(@NonNull byte[] bytes) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
long firstLong = buffer.getLong();
long secondLong = buffer.getLong();
return new UUID(firstLong, secondLong);
}
/**
* Converts a UUID pojo into a 16-bytes array to store into database as BLOB
*
* @param uuid the UUID pojo
* @return a byte array to store into database
*/
@NonNull
public static byte[] convertUUIDToByte(@NonNull UUID uuid) {
byte[] bytes = new byte[16];
ByteBuffer buffer = ByteBuffer.wrap(bytes);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
return buffer.array();
}
}

View File

@ -1,97 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util;
import android.database.Cursor;
import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteDatabase;
/**
* A data class that holds the information about a view.
* <p>
* This derives information from sqlite_master.
* <p>
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public final class ViewInfo {
/**
* The view name
*/
public final String name;
/**
* The SQL of CREATE VIEW.
*/
public final String sql;
public ViewInfo(String name, String sql) {
this.name = name;
this.sql = sql;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ViewInfo)) return false;
ViewInfo viewInfo = (ViewInfo) o;
return (name != null ? name.equals(viewInfo.name) : viewInfo.name == null)
&& (sql != null ? sql.equals(viewInfo.sql) : viewInfo.sql == null);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (sql != null ? sql.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "ViewInfo{"
+ "name='" + name + '\''
+ ", sql='" + sql + '\''
+ '}';
}
/**
* Reads the view information from the given database.
*
* @param database The database to read the information from.
* @param viewName The view name.
* @return A ViewInfo containing the schema information for the provided view name.
*/
@SuppressWarnings("SameParameterValue")
public static ViewInfo read(SupportSQLiteDatabase database, String viewName) {
Cursor cursor = database.query("SELECT name, sql FROM sqlite_master "
+ "WHERE type = 'view' AND name = '" + viewName + "'");
//noinspection TryFinallyCanBeTryWithResources
try {
if (cursor.moveToFirst()) {
return new ViewInfo(cursor.getString(0), cursor.getString(1));
} else {
return new ViewInfo(viewName, null);
}
} finally {
cursor.close();
}
}
}

View File

@ -13,26 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.migration
package androidx.room.migration;
import androidx.annotation.NonNull;
import androidx.room.AutoMigration;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase
/**
* Interface for defining an automatic migration specification for Room databases.
* <p>
*
* The methods defined in this interface will be called on a background thread from the executor
* set in Room's builder. It is important to note that the methods are all in a transaction when
* it is called.
*
* @see AutoMigration
* For details, see [androidx.room.AutoMigration]
*/
public interface AutoMigrationSpec {
interface AutoMigrationSpec {
/**
* Invoked after the migration is completed.
* @param db The SQLite database.
*/
default void onPostMigrate(@NonNull SupportSQLiteDatabase db) {}
fun onPostMigrate(db: SupportSQLiteDatabase) {}
}

View File

@ -13,51 +13,42 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.migration
package androidx.room.migration;
import androidx.annotation.NonNull;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteDatabase
/**
* Base class for a database migration.
* <p>
* Each migration can move between 2 versions that are defined by {@link #startVersion} and
* {@link #endVersion}.
* <p>
*
* Creates a new migration between [startVersion] and [endVersion].
*
* Each migration can move between 2 versions that are defined by [startVersion] and
* [endVersion].
*
* A migration can handle more than 1 version (e.g. if you have a faster path to choose when
* going version 3 to 5 without going to version 4). If Room opens a database at version
* 3 and latest version is &gt;= 5, Room will use the migration object that can migrate from
* 3 and latest version is 5, Room will use the migration object that can migrate from
* 3 to 5 instead of 3 to 4 and 4 to 5.
* <p>
*
* If there are not enough migrations provided to move from the current version to the latest
* version, Room will clear the database and recreate so even if you have no changes between 2
* versions, you should still provide a Migration object to the builder.
*/
public abstract class Migration {
public final int startVersion;
public final int endVersion;
/**
* Creates a new migration between {@code startVersion} and {@code endVersion}.
*
* @param startVersion The start version of the database.
* @param endVersion The end version of the database after this migration is applied.
*/
public Migration(int startVersion, int endVersion) {
this.startVersion = startVersion;
this.endVersion = endVersion;
}
abstract class Migration(
@JvmField
val startVersion: Int,
@JvmField
val endVersion: Int
) {
/**
* Should run the necessary migrations.
* <p>
* This class cannot access any generated Dao in this method.
* <p>
* This method is already called inside a transaction and that transaction might actually be a
* composite transaction of all necessary {@code Migration}s.
*
* @param database The database instance
* The Migration class cannot access any generated Dao in this method.
*
* This method is already called inside a transaction and that transaction might actually be a
* composite transaction of all necessary `Migration`s.
*
* @param db The database instance
*/
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
abstract fun migrate(db: SupportSQLiteDatabase)
}

View File

@ -46,7 +46,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
*
* @param <T> Data type returned by the data source.
*
* @hide
*/
@SuppressWarnings("deprecation")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@ -117,8 +116,8 @@ public abstract class LimitOffsetDataSource<T> extends androidx.paging.Positiona
/**
* Count number of rows query can return
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressWarnings("WeakerAccess")
public int countItems() {
registerObserverIfNecessary();
@ -154,7 +153,7 @@ public abstract class LimitOffsetDataSource<T> extends androidx.paging.Positiona
@NonNull LoadInitialCallback<T> callback) {
registerObserverIfNecessary();
List<T> list = Collections.emptyList();
int totalCount = 0;
int totalCount;
int firstLoadPosition = 0;
RoomSQLiteQuery sqLiteQuery = null;
Cursor cursor = null;
@ -172,8 +171,6 @@ public abstract class LimitOffsetDataSource<T> extends androidx.paging.Positiona
mDb.setTransactionSuccessful();
list = rows;
}
} catch (Throwable ex) {
eu.faircode.email.Log.w(ex);
} finally {
if (cursor != null) {
cursor.close();
@ -196,8 +193,8 @@ public abstract class LimitOffsetDataSource<T> extends androidx.paging.Positiona
/**
* Return the rows from startPos to startPos + loadCount
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressWarnings("deprecation")
@NonNull
public List<T> loadRange(int startPosition, int loadCount) {

View File

@ -0,0 +1,183 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("CursorUtil")
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
package androidx.room.util
import android.database.Cursor
import android.database.CursorWrapper
import android.database.MatrixCursor
import android.os.Build
import android.util.Log
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
/**
* Copies the given cursor into a in-memory cursor and then closes it.
*
*
* This is useful for iterating over a cursor multiple times without the cost of JNI while
* reading or IO while filling the window at the expense of memory consumption.
*
* @param c the cursor to copy.
* @return a new cursor containing the same data as the given cursor.
*/
fun copyAndClose(c: Cursor): Cursor = c.useCursor { cursor ->
val matrixCursor = MatrixCursor(cursor.columnNames, cursor.count)
while (cursor.moveToNext()) {
val row = arrayOfNulls<Any>(cursor.columnCount)
for (i in 0 until c.columnCount) {
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_NULL -> row[i] = null
Cursor.FIELD_TYPE_INTEGER -> row[i] = cursor.getLong(i)
Cursor.FIELD_TYPE_FLOAT -> row[i] = cursor.getDouble(i)
Cursor.FIELD_TYPE_STRING -> row[i] = cursor.getString(i)
Cursor.FIELD_TYPE_BLOB -> row[i] = cursor.getBlob(i)
else -> throw IllegalStateException()
}
}
matrixCursor.addRow(row)
}
matrixCursor
}
/**
* Patches [Cursor.getColumnIndex] to work around issues on older devices.
* If the column is not found, it retries with the specified name surrounded by backticks.
*
* @param c The cursor.
* @param name The name of the target column.
* @return The index of the column, or -1 if not found.
*/
fun getColumnIndex(c: Cursor, name: String): Int {
var index = c.getColumnIndex(name)
if (index >= 0) {
return index
}
index = c.getColumnIndex("`$name`")
return if (index >= 0) {
index
} else {
findColumnIndexBySuffix(c, name)
}
}
/**
* Patches [Cursor.getColumnIndexOrThrow] to work around issues on older devices.
* If the column is not found, it retries with the specified name surrounded by backticks.
*
* @param c The cursor.
* @param name The name of the target column.
* @return The index of the column.
* @throws IllegalArgumentException if the column does not exist.
*/
fun getColumnIndexOrThrow(c: Cursor, name: String): Int {
val index: Int = getColumnIndex(c, name)
if (index >= 0) {
return index
}
val availableColumns = try {
c.columnNames.joinToString()
} catch (e: Exception) {
Log.d("RoomCursorUtil", "Cannot collect column names for debug purposes", e)
"unknown"
}
throw IllegalArgumentException(
"column '$name' does not exist. Available columns: $availableColumns"
)
}
/**
* Finds a column by name by appending `.` in front of it and checking by suffix match.
* Also checks for the version wrapped with `` (backticks).
* workaround for b/157261134 for API levels 25 and below
*
* e.g. "foo" will match "any.foo" and "`any.foo`"
*/
private fun findColumnIndexBySuffix(cursor: Cursor, name: String): Int {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
// we need this workaround only on APIs < 26. So just return not found on newer APIs
return -1
}
if (name.isEmpty()) {
return -1
}
val columnNames = cursor.columnNames
return findColumnIndexBySuffix(columnNames, name)
}
@VisibleForTesting
fun findColumnIndexBySuffix(columnNames: Array<String>, name: String): Int {
val dotSuffix = ".$name"
val backtickSuffix = ".$name`"
columnNames.forEachIndexed { index, columnName ->
// do not check if column name is not long enough. 1 char for table name, 1 char for '.'
if (columnName.length >= name.length + 2) {
if (columnName.endsWith(dotSuffix)) {
return index
} else if (columnName[0] == '`' && columnName.endsWith(backtickSuffix)) {
return index
}
}
}
return -1
}
/**
* Backwards compatible function that executes the given block function on this Cursor and then
* closes the Cursor.
*/
inline fun <R> Cursor.useCursor(block: (Cursor) -> R): R {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return this.use(block)
} else {
try {
return block(this)
} finally {
this.close()
}
}
}
/**
* Wraps the given cursor such that `getColumnIndex()` will utilize the provided
* `mapping` when getting the index of a column in `columnNames`.
*
* This is useful when the original cursor contains duplicate columns. Instead of letting the
* cursor return the first matching column with a name, we can resolve the ambiguous column
* indices and wrap the cursor such that for a set of desired column indices, the returned
* value will be that from the pre-computation.
*
* @param cursor the cursor to wrap.
* @param columnNames the column names whose index are known. The result column index of the
* column name at i will be at `mapping[i]`.
* @param mapping the cursor column indices of the columns at `columnNames`.
* @return the wrapped Cursor.
*/
fun wrapMappedColumns(cursor: Cursor, columnNames: Array<String>, mapping: IntArray): Cursor {
check(columnNames.size == mapping.size) { "Expected columnNames.length == mapping.length" }
return object : CursorWrapper(cursor) {
override fun getColumnIndex(columnName: String): Int {
columnNames.forEachIndexed { i, mappedColumnName ->
if (mappedColumnName.equals(columnName, ignoreCase = true)) {
return mapping[i]
}
}
return super.getColumnIndex(columnName)
}
}
}

View File

@ -0,0 +1,213 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("DBUtil")
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
package androidx.room.util
import android.database.AbstractWindowedCursor
import android.database.Cursor
import android.database.sqlite.SQLiteConstraintException
import android.os.Build
import android.os.CancellationSignal
import androidx.annotation.RestrictTo
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteCompat
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQuery
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.nio.ByteBuffer
/**
* Performs the SQLiteQuery on the given database.
*
* This util method encapsulates copying the cursor if the `maybeCopy` parameter is
* `true` and either the api level is below a certain threshold or the full result of the
* query does not fit in a single window.
*
* @param db The database to perform the query on.
* @param sqLiteQuery The query to perform.
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
* @return Result of the query.
*
*/
@Deprecated(
"This is only used in the generated code and shouldn't be called directly."
)
fun query(db: RoomDatabase, sqLiteQuery: SupportSQLiteQuery, maybeCopy: Boolean): Cursor {
return query(db, sqLiteQuery, maybeCopy, null)
}
/**
* Performs the SQLiteQuery on the given database.
*
* This util method encapsulates copying the cursor if the `maybeCopy` parameter is
* `true` and either the api level is below a certain threshold or the full result of the
* query does not fit in a single window.
*
* @param db The database to perform the query on.
* @param sqLiteQuery The query to perform.
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
* @param signal The cancellation signal to be attached to the query.
* @return Result of the query.
*/
fun query(
db: RoomDatabase,
sqLiteQuery: SupportSQLiteQuery,
maybeCopy: Boolean,
signal: CancellationSignal?
): Cursor {
val cursor = db.query(sqLiteQuery, signal)
if (maybeCopy && cursor is AbstractWindowedCursor) {
val rowsInCursor = cursor.count // Should fill the window.
val rowsInWindow = if (cursor.hasWindow()) {
cursor.window.numRows
} else {
rowsInCursor
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
return copyAndClose(cursor)
}
}
return cursor
}
/**
* Drops all FTS content sync triggers created by Room.
*
* FTS content sync triggers created by Room are those that are found in the sqlite_master table
* who's names start with 'room_fts_content_sync_'.
*
* @param db The database.
*/
fun dropFtsSyncTriggers(db: SupportSQLiteDatabase) {
val existingTriggers = buildList {
db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'").useCursor { cursor ->
while (cursor.moveToNext()) {
add(cursor.getString(0))
}
}
}
existingTriggers.forEach { triggerName ->
if (triggerName.startsWith("room_fts_content_sync_")) {
db.execSQL("DROP TRIGGER IF EXISTS $triggerName")
}
}
}
/**
* Checks for foreign key violations by executing a PRAGMA foreign_key_check.
*/
fun foreignKeyCheck(
db: SupportSQLiteDatabase,
tableName: String
) {
db.query("PRAGMA foreign_key_check(`$tableName`)").useCursor { cursor ->
if (cursor.count > 0) {
val errorMsg = processForeignKeyCheckFailure(cursor)
throw SQLiteConstraintException(errorMsg)
}
}
}
/**
* Reads the user version number out of the database header from the given file.
*
* @param databaseFile the database file.
* @return the database version
* @throws IOException if something goes wrong reading the file, such as bad database header or
* missing permissions.
*
* @see [User Version
* Number](https://www.sqlite.org/fileformat.html.user_version_number).
*/
@Throws(IOException::class)
fun readVersion(databaseFile: File): Int {
FileInputStream(databaseFile).channel.use { input ->
val buffer = ByteBuffer.allocate(4)
input.tryLock(60, 4, true)
input.position(60)
val read = input.read(buffer)
if (read != 4) {
throw IOException("Bad database header, unable to read 4 bytes at offset 60")
}
buffer.rewind()
return buffer.int // ByteBuffer is big-endian by default
}
}
/**
* CancellationSignal is only available from API 16 on. This function will create a new
* instance of the Cancellation signal only if the current API > 16.
*
* @return A new instance of CancellationSignal or null.
*/
fun createCancellationSignal(): CancellationSignal? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
SupportSQLiteCompat.Api16Impl.createCancellationSignal()
} else {
null
}
}
/**
* Converts the [Cursor] returned in case of a foreign key violation into a detailed
* error message for debugging.
*
* The foreign_key_check pragma returns one row output for each foreign key violation.
*
* The cursor received has four columns for each row output. The first column is the name of
* the child table. The second column is the rowId of the row that contains the foreign key
* violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the
* name of the parent table. The fourth column is the index of the specific foreign key
* constraint that failed.
*
* @param cursor Cursor containing information regarding the FK violation
* @return Error message generated containing debugging information
*/
private fun processForeignKeyCheckFailure(cursor: Cursor): String {
return buildString {
val rowCount = cursor.count
val fkParentTables = mutableMapOf<String, String>()
while (cursor.moveToNext()) {
if (cursor.isFirst) {
append("Foreign key violation(s) detected in '")
append(cursor.getString(0)).append("'.\n")
}
val constraintIndex = cursor.getString(3)
if (!fkParentTables.containsKey(constraintIndex)) {
fkParentTables[constraintIndex] = cursor.getString(2)
}
}
append("Number of different violations discovered: ")
append(fkParentTables.keys.size).append("\n")
append("Number of rows in violation: ")
append(rowCount).append("\n")
append("Violation(s) detected in the following constraint(s):\n")
for ((key, value) in fkParentTables) {
append("\tParent Table = ")
append(value)
append(", Foreign Key Constraint Index = ")
append(key).append("\n")
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("FileUtil")
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
package androidx.room.util
import android.annotation.SuppressLint
import android.os.Build
import androidx.annotation.RestrictTo
import java.io.IOException
import java.nio.channels.Channels
import java.nio.channels.FileChannel
import java.nio.channels.ReadableByteChannel
/**
* Copies data from the input channel to the output file channel.
*
* @param input the input channel to copy.
* @param output the output channel to copy.
* @throws IOException if there is an I/O error.
*/
@SuppressLint("LambdaLast")
@Throws(IOException::class)
fun copy(input: ReadableByteChannel, output: FileChannel) {
try {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
output.transferFrom(input, 0, Long.MAX_VALUE)
} else {
val inputStream = Channels.newInputStream(input)
val outputStream = Channels.newOutputStream(output)
var length: Int
val buffer = ByteArray(1024 * 4)
// TODO: Use Kotlin stdlib IO APIs
while (inputStream.read(buffer).also { length = it } > 0) {
outputStream.write(buffer, 0, length)
}
}
output.force(false)
} finally {
input.close()
output.close()
}
}

View File

@ -0,0 +1,181 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.sqlite.db.SupportSQLiteDatabase
import java.util.ArrayDeque
/**
* A data class that holds the information about an FTS table.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class FtsTableInfo(
/**
* The table name
*/
@JvmField
val name: String,
/**
* The column names
*/
@JvmField
val columns: Set<String>,
/**
* The set of options. Each value in the set contains the option in the following format:
* <key, value>.
*/
@JvmField
val options: Set<String>
) {
constructor(name: String, columns: Set<String>, createSql: String) :
this(name, columns, parseOptions(createSql))
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is FtsTableInfo) return false
val that = other
if (name != that.name) return false
if (columns != that.columns) return false
return options == that.options
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + (columns.hashCode())
result = 31 * result + (options.hashCode())
return result
}
override fun toString(): String {
return ("FtsTableInfo{name='$name', columns=$columns, options=$options'}")
}
companion object {
// A set of valid FTS Options
private val FTS_OPTIONS = arrayOf(
"tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
"order=", "prefix=", "uncompress="
)
/**
* Reads the table information from the given database.
*
* @param database The database to read the information from.
* @param tableName The table name.
* @return A FtsTableInfo containing the columns and options for the provided table name.
*/
@JvmStatic
fun read(database: SupportSQLiteDatabase, tableName: String): FtsTableInfo {
val columns = readColumns(database, tableName)
val options = readOptions(database, tableName)
return FtsTableInfo(tableName, columns, options)
}
private fun readColumns(database: SupportSQLiteDatabase, tableName: String): Set<String> {
return buildSet {
database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor ->
if (cursor.columnCount > 0) {
val nameIndex = cursor.getColumnIndex("name")
while (cursor.moveToNext()) {
add(cursor.getString(nameIndex))
}
}
}
}
}
private fun readOptions(database: SupportSQLiteDatabase, tableName: String): Set<String> {
val sql = database.query(
"SELECT * FROM sqlite_master WHERE `name` = '$tableName'"
).useCursor { cursor ->
if (cursor.moveToFirst()) {
cursor.getString(cursor.getColumnIndexOrThrow("sql"))
} else {
""
}
}
return parseOptions(sql)
}
/**
* Parses FTS options from the create statement of an FTS table.
*
* This method assumes the given create statement is a valid well-formed SQLite statement as
* defined in the [CREATE VIRTUAL TABLE
* syntax diagram](https://www.sqlite.org/lang_createvtab.html).
*
* @param createStatement the "CREATE VIRTUAL TABLE" statement.
* @return the set of FTS option key and values in the create statement.
*/
@VisibleForTesting
@JvmStatic
fun parseOptions(createStatement: String): Set<String> {
if (createStatement.isEmpty()) {
return emptySet()
}
// Module arguments are within the parenthesis followed by the module name.
val argsString = createStatement.substring(
createStatement.indexOf('(') + 1,
createStatement.lastIndexOf(')')
)
// Split the module argument string by the comma delimiter, keeping track of quotation
// so that if the delimiter is found within a string literal we don't substring at the
// wrong index. SQLite supports four ways of quoting keywords, see:
// https://www.sqlite.org/lang_keywords.html
val args = mutableListOf<String>()
val quoteStack = ArrayDeque<Char>()
var lastDelimiterIndex = -1
argsString.forEachIndexed { i, value ->
when (value) {
'\'', '"', '`' ->
if (quoteStack.isEmpty()) {
quoteStack.push(value)
} else if (quoteStack.peek() == value) {
quoteStack.pop()
}
'[' -> if (quoteStack.isEmpty()) {
quoteStack.push(value)
}
']' -> if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
quoteStack.pop()
}
',' -> if (quoteStack.isEmpty()) {
args.add(argsString.substring(lastDelimiterIndex + 1, i).trim { it <= ' ' })
lastDelimiterIndex = i
}
}
}
// Add final argument.
args.add(argsString.substring(lastDelimiterIndex + 1).trim())
// Match args against valid options, otherwise they are column definitions.
val options = args.filter { arg ->
FTS_OPTIONS.any { validOption ->
arg.startsWith(validOption)
}
}.toSet()
return options
}
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("RelationUtil")
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
package androidx.room.util
import androidx.annotation.RestrictTo
import androidx.collection.ArrayMap
import androidx.collection.LongSparseArray
import androidx.room.RoomDatabase
/**
* Utility function used in generated code to recursively fetch relationships when the amount of
* keys exceed [RoomDatabase.MAX_BIND_PARAMETER_CNT].
*
* @param map - The map containing the relationship keys to fill-in.
* @param isRelationCollection - True if [V] is a [Collection] which means it is non null.
* @param fetchBlock - A lambda for calling the generated _fetchRelationship function.
*/
fun <K : Any, V> recursiveFetchHashMap(
map: HashMap<K, V>,
isRelationCollection: Boolean,
fetchBlock: (HashMap<K, V>) -> Unit
) {
val tmpMap = HashMap<K, V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
var count = 0
for (key in map.keys) {
// Safe because `V` is a nullable type arg when isRelationCollection == false and vice versa
@Suppress("UNCHECKED_CAST")
if (isRelationCollection) {
tmpMap[key] = map[key] as V
} else {
tmpMap[key] = null as V
}
count++
if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
// recursively load that batch
fetchBlock(tmpMap)
// for non collection relation, put the loaded batch in the original map,
// not needed when dealing with collections since references are passed
if (!isRelationCollection) {
map.putAll(tmpMap)
}
tmpMap.clear()
count = 0
}
}
if (count > 0) {
// load the last batch
fetchBlock(tmpMap)
// for non collection relation, put the last batch in the original map
if (!isRelationCollection) {
map.putAll(tmpMap)
}
}
}
/**
* Same as [recursiveFetchHashMap] but for [LongSparseArray].
*/
fun <V> recursiveFetchLongSparseArray(
map: LongSparseArray<V>,
isRelationCollection: Boolean,
fetchBlock: (LongSparseArray<V>) -> Unit
) {
val tmpMap = LongSparseArray<V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
var count = 0
var mapIndex = 0
val limit = map.size()
while (mapIndex < limit) {
if (isRelationCollection) {
tmpMap.put(map.keyAt(mapIndex), map.valueAt(mapIndex))
} else {
// Safe because `V` is a nullable type arg when isRelationCollection == false
@Suppress("UNCHECKED_CAST")
tmpMap.put(map.keyAt(mapIndex), null as V)
}
mapIndex++
count++
if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
fetchBlock(tmpMap)
if (!isRelationCollection) {
map.putAll(tmpMap)
}
tmpMap.clear()
count = 0
}
}
if (count > 0) {
fetchBlock(tmpMap)
if (!isRelationCollection) {
map.putAll(tmpMap)
}
}
}
/**
* Same as [recursiveFetchHashMap] but for [ArrayMap].
*/
fun <K : Any, V> recursiveFetchArrayMap(
map: ArrayMap<K, V>,
isRelationCollection: Boolean,
fetchBlock: (ArrayMap<K, V>) -> Unit
) {
val tmpMap = ArrayMap<K, V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
var count = 0
var mapIndex = 0
val limit = map.size
while (mapIndex < limit) {
if (isRelationCollection) {
tmpMap[map.keyAt(mapIndex)] = map.valueAt(mapIndex)
} else {
tmpMap[map.keyAt(mapIndex)] = null
}
mapIndex++
count++
if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
fetchBlock(tmpMap)
if (!isRelationCollection) {
// Cast needed to disambiguate from putAll(SimpleArrayMap)
map.putAll(tmpMap as Map<K, V>)
}
tmpMap.clear()
count = 0
}
}
if (count > 0) {
fetchBlock(tmpMap)
if (!isRelationCollection) {
// Cast needed to disambiguate from putAll(SimpleArrayMap)
map.putAll(tmpMap as Map<K, V>)
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("StringUtil")
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
package androidx.room.util
import android.util.Log
import androidx.annotation.RestrictTo
import java.lang.NumberFormatException
import java.lang.StringBuilder
@Suppress("unused")
@JvmField
val EMPTY_STRING_ARRAY = arrayOfNulls<String>(0)
/**
* Returns a new StringBuilder to be used while producing SQL queries.
*
* @return A new or recycled StringBuilder
*/
fun newStringBuilder(): StringBuilder {
// TODO pool:
return StringBuilder()
}
/**
* Adds bind variable placeholders (?) to the given string. Each placeholder is separated
* by a comma.
*
* @param builder The StringBuilder for the query
* @param count Number of placeholders
*/
fun appendPlaceholders(builder: StringBuilder, count: Int) {
for (i in 0 until count) {
builder.append("?")
if (i < count - 1) {
builder.append(",")
}
}
}
/**
* Splits a comma separated list of integers to integer list.
*
*
* If an input is malformed, it is omitted from the result.
*
* @param input Comma separated list of integers.
* @return A List containing the integers or null if the input is null.
*/
fun splitToIntList(input: String?): List<Int>? {
return input?.split(',')?.mapNotNull { item ->
try {
item.toInt()
} catch (ex: NumberFormatException) {
Log.e("ROOM", "Malformed integer list", ex)
null
}
}
}
/**
* Joins the given list of integers into a comma separated list.
*
* @param input The list of integers.
* @return Comma separated string composed of integers in the list. If the list is null, return
* value is null.
*/
fun joinIntoString(input: List<Int>?): String? {
return input?.joinToString(",")
}

View File

@ -0,0 +1,645 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util
import android.database.Cursor
import android.os.Build
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.room.ColumnInfo
import androidx.room.ColumnInfo.SQLiteTypeAffinity
import androidx.sqlite.db.SupportSQLiteDatabase
import java.util.Locale
import java.util.TreeMap
/**
* A data class that holds the information about a table.
*
* It directly maps to the result of `PRAGMA table_info(<table_name>)`. Check the
* [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
* documentation for more details.
*
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
// if you change this class, you must change TableInfoValidationWriter.kt
class TableInfo(
/**
* The table name.
*/
@JvmField
val name: String,
@JvmField
val columns: Map<String, Column>,
@JvmField
val foreignKeys: Set<ForeignKey>,
@JvmField
val indices: Set<Index>? = null
) {
/**
* Identifies from where the info object was created.
*/
@Retention(AnnotationRetention.SOURCE)
@IntDef(value = [CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE])
internal annotation class CreatedFrom()
/**
* For backward compatibility with dbs created with older versions.
*/
@SuppressWarnings("unused")
constructor(
name: String,
columns: Map<String, Column>,
foreignKeys: Set<ForeignKey>
) : this(name, columns, foreignKeys, emptySet<Index>())
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TableInfo) return false
if (name != other.name) return false
if (columns != other.columns) {
return false
}
if (foreignKeys != other.foreignKeys) {
return false
}
return if (indices == null || other.indices == null) {
// if one us is missing index information, seems like we couldn't acquire the
// information so we better skip.
true
} else indices == other.indices
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + columns.hashCode()
result = 31 * result + foreignKeys.hashCode()
// skip index, it is not reliable for comparison.
return result
}
override fun toString(): String {
return ("TableInfo{name='$name', columns=$columns, foreignKeys=$foreignKeys, " +
"indices=$indices}")
}
companion object {
/**
* Identifier for when the info is created from an unknown source.
*/
const val CREATED_FROM_UNKNOWN = 0
/**
* Identifier for when the info is created from an entity definition, such as generated code
* by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
*/
const val CREATED_FROM_ENTITY = 1
/**
* Identifier for when the info is created from the database itself, reading information
* from a PRAGMA, such as table_info.
*/
const val CREATED_FROM_DATABASE = 2
/**
* Reads the table information from the given database.
*
* @param database The database to read the information from.
* @param tableName The table name.
* @return A TableInfo containing the schema information for the provided table name.
*/
@JvmStatic
fun read(database: SupportSQLiteDatabase, tableName: String): TableInfo {
return readTableInfo(
database = database,
tableName = tableName
)
}
}
/**
* Holds the information about a database column.
*/
class Column(
/**
* The column name.
*/
@JvmField
val name: String,
/**
* The column type affinity.
*/
@JvmField
val type: String,
/**
* Whether or not the column can be NULL.
*/
@JvmField
val notNull: Boolean,
@JvmField
val primaryKeyPosition: Int,
@JvmField
val defaultValue: String?,
@CreatedFrom
@JvmField
val createdFrom: Int
) {
/**
* The column type after it is normalized to one of the basic types according to
* https://www.sqlite.org/datatype3.html Section 3.1.
*
*
* This is the value Room uses for equality check.
*/
@SQLiteTypeAffinity
@JvmField
val affinity: Int = findAffinity(type)
@Deprecated("Use {@link Column#Column(String, String, boolean, int, String, int)} instead.")
constructor(name: String, type: String, notNull: Boolean, primaryKeyPosition: Int) : this(
name,
type,
notNull,
primaryKeyPosition,
null,
CREATED_FROM_UNKNOWN
)
/**
* Implements https://www.sqlite.org/datatype3.html section 3.1
*
* @param type The type that was given to the sqlite
* @return The normalized type which is one of the 5 known affinities
*/
@SQLiteTypeAffinity
private fun findAffinity(type: String?): Int {
if (type == null) {
return ColumnInfo.BLOB
}
val uppercaseType = type.uppercase(Locale.US)
if (uppercaseType.contains("INT")) {
return ColumnInfo.INTEGER
}
if (uppercaseType.contains("CHAR") ||
uppercaseType.contains("CLOB") ||
uppercaseType.contains("TEXT")
) {
return ColumnInfo.TEXT
}
if (uppercaseType.contains("BLOB")) {
return ColumnInfo.BLOB
}
if (uppercaseType.contains("REAL") ||
uppercaseType.contains("FLOA") ||
uppercaseType.contains("DOUB")
) {
return ColumnInfo.REAL
}
// sqlite returns NUMERIC here but it is like a catch all. We already
// have UNDEFINED so it is better to use UNDEFINED for consistency.
return ColumnInfo.UNDEFINED
}
companion object {
/**
* Checks if the default values provided match. Handles the special case in which the
* default value is surrounded by parenthesis (e.g. encountered in b/182284899).
*
* Surrounding parenthesis are removed by SQLite when reading from the database, hence
* this function will check if they are present in the actual value, if so, it will
* compare the two values by ignoring the surrounding parenthesis.
*
*/
@VisibleForTesting
@JvmStatic
fun defaultValueEquals(current: String, other: String?): Boolean {
if (current == other) {
return true
} else if (containsSurroundingParenthesis(current)) {
return current.substring(1, current.length - 1).trim() == other
}
return false
}
/**
* Checks for potential surrounding parenthesis, if found, removes them and checks if
* remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant,
* and returns true.
*/
private fun containsSurroundingParenthesis(current: String): Boolean {
if (current.isEmpty()) {
return false
}
var surroundingParenthesis = 0
current.forEachIndexed { i, c ->
if (i == 0 && c != '(') {
return false
}
if (c == '(') {
surroundingParenthesis++
} else if (c == ')') {
surroundingParenthesis--
if (surroundingParenthesis == 0 && i != current.length - 1) {
return false
}
}
}
return surroundingParenthesis == 0
}
}
// TODO: problem probably here
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Column) return false
if (Build.VERSION.SDK_INT >= 20) {
if (primaryKeyPosition != other.primaryKeyPosition) return false
} else {
if (isPrimaryKey != other.isPrimaryKey) return false
}
if (name != other.name) return false
if (notNull != other.notNull) return false
// Only validate default value if it was defined in an entity, i.e. if the info
// from the compiler itself has it. b/136019383
if (
createdFrom == CREATED_FROM_ENTITY &&
other.createdFrom == CREATED_FROM_DATABASE &&
defaultValue != null &&
!defaultValueEquals(defaultValue, other.defaultValue)
) {
return false
} else if (
createdFrom == CREATED_FROM_DATABASE &&
other.createdFrom == CREATED_FROM_ENTITY &&
other.defaultValue != null &&
!defaultValueEquals(other.defaultValue, defaultValue)
) {
return false
} else if (
createdFrom != CREATED_FROM_UNKNOWN &&
createdFrom == other.createdFrom &&
(if (defaultValue != null)
!defaultValueEquals(defaultValue, other.defaultValue)
else other.defaultValue != null)
) {
return false
}
return affinity == other.affinity
}
/**
* Returns whether this column is part of the primary key or not.
*
* @return True if this column is part of the primary key, false otherwise.
*/
val isPrimaryKey: Boolean
get() = primaryKeyPosition > 0
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + affinity
result = 31 * result + if (notNull) 1231 else 1237
result = 31 * result + primaryKeyPosition
// Default value is not part of the hashcode since we conditionally check it for
// equality which would break the equals + hashcode contract.
// result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
return result
}
override fun toString(): String {
return ("Column{name='$name', type='$type', affinity='$affinity', " +
"notNull=$notNull, primaryKeyPosition=$primaryKeyPosition, " +
"defaultValue='${defaultValue ?: "undefined"}'}")
}
}
/**
* Holds the information about an SQLite foreign key
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class ForeignKey(
@JvmField
val referenceTable: String,
@JvmField
val onDelete: String,
@JvmField
val onUpdate: String,
@JvmField
val columnNames: List<String>,
@JvmField
val referenceColumnNames: List<String>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ForeignKey) return false
if (referenceTable != other.referenceTable) return false
if (onDelete != other.onDelete) return false
if (onUpdate != other.onUpdate) return false
return if (columnNames != other.columnNames) false else referenceColumnNames ==
other.referenceColumnNames
}
override fun hashCode(): Int {
var result = referenceTable.hashCode()
result = 31 * result + onDelete.hashCode()
result = 31 * result + onUpdate.hashCode()
result = 31 * result + columnNames.hashCode()
result = 31 * result + referenceColumnNames.hashCode()
return result
}
override fun toString(): String {
return ("ForeignKey{referenceTable='$referenceTable', onDelete='$onDelete +', " +
"onUpdate='$onUpdate', columnNames=$columnNames, " +
"referenceColumnNames=$referenceColumnNames}")
}
}
/**
* Temporary data holder for a foreign key row in the pragma result. We need this to ensure
* sorting in the generated foreign key object.
*/
internal class ForeignKeyWithSequence(
val id: Int,
val sequence: Int,
val from: String,
val to: String
) : Comparable<ForeignKeyWithSequence> {
override fun compareTo(other: ForeignKeyWithSequence): Int {
val idCmp = id - other.id
return if (idCmp == 0) {
sequence - other.sequence
} else {
idCmp
}
}
}
/**
* Holds the information about an SQLite index
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class Index(
@JvmField
val name: String,
@JvmField
val unique: Boolean,
@JvmField
val columns: List<String>,
@JvmField
var orders: List<String>
) {
init {
orders = orders.ifEmpty {
List(columns.size) { androidx.room.Index.Order.ASC.name }
}
}
companion object {
// should match the value in Index.kt
const val DEFAULT_PREFIX = "index_"
}
@Deprecated("Use {@link #Index(String, boolean, List, List)}")
constructor(name: String, unique: Boolean, columns: List<String>) : this(
name,
unique,
columns,
List<String>(columns.size) { androidx.room.Index.Order.ASC.name }
)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Index) return false
if (unique != other.unique) {
return false
}
if (columns != other.columns) {
return false
}
if (orders != other.orders) {
return false
}
return if (name.startsWith(DEFAULT_PREFIX)) {
other.name.startsWith(DEFAULT_PREFIX)
} else {
name == other.name
}
}
override fun hashCode(): Int {
var result = if (name.startsWith(DEFAULT_PREFIX)) {
DEFAULT_PREFIX.hashCode()
} else {
name.hashCode()
}
result = 31 * result + if (unique) 1 else 0
result = 31 * result + columns.hashCode()
result = 31 * result + orders.hashCode()
return result
}
override fun toString(): String {
return ("Index{name='$name', unique=$unique, columns=$columns, orders=$orders'}")
}
}
}
internal fun readTableInfo(database: SupportSQLiteDatabase, tableName: String): TableInfo {
val columns = readColumns(database, tableName)
val foreignKeys = readForeignKeys(database, tableName)
val indices = readIndices(database, tableName)
return TableInfo(tableName, columns, foreignKeys, indices)
}
private fun readForeignKeys(
database: SupportSQLiteDatabase,
tableName: String
): Set<TableInfo.ForeignKey> {
// this seems to return everything in order but it is not documented so better be safe
database.query("PRAGMA foreign_key_list(`$tableName`)").useCursor { cursor ->
val idColumnIndex = cursor.getColumnIndex("id")
val seqColumnIndex = cursor.getColumnIndex("seq")
val tableColumnIndex = cursor.getColumnIndex("table")
val onDeleteColumnIndex = cursor.getColumnIndex("on_delete")
val onUpdateColumnIndex = cursor.getColumnIndex("on_update")
val ordered = readForeignKeyFieldMappings(cursor)
// Reset cursor as readForeignKeyFieldMappings has moved it
cursor.moveToPosition(-1)
return buildSet {
while (cursor.moveToNext()) {
val seq = cursor.getInt(seqColumnIndex)
if (seq != 0) {
continue
}
val id = cursor.getInt(idColumnIndex)
val myColumns = mutableListOf<String>()
val refColumns = mutableListOf<String>()
ordered.filter {
it.id == id
}.forEach { key ->
myColumns.add(key.from)
refColumns.add(key.to)
}
add(
TableInfo.ForeignKey(
referenceTable = cursor.getString(tableColumnIndex),
onDelete = cursor.getString(onDeleteColumnIndex),
onUpdate = cursor.getString(onUpdateColumnIndex),
columnNames = myColumns,
referenceColumnNames = refColumns
)
)
}
}
}
}
private fun readForeignKeyFieldMappings(cursor: Cursor): List<TableInfo.ForeignKeyWithSequence> {
val idColumnIndex = cursor.getColumnIndex("id")
val seqColumnIndex = cursor.getColumnIndex("seq")
val fromColumnIndex = cursor.getColumnIndex("from")
val toColumnIndex = cursor.getColumnIndex("to")
return buildList {
while (cursor.moveToNext()) {
add(
TableInfo.ForeignKeyWithSequence(
id = cursor.getInt(idColumnIndex),
sequence = cursor.getInt(seqColumnIndex),
from = cursor.getString(fromColumnIndex),
to = cursor.getString(toColumnIndex)
)
)
}
}.sorted()
}
private fun readColumns(
database: SupportSQLiteDatabase,
tableName: String
): Map<String, TableInfo.Column> {
database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor ->
if (cursor.columnCount <= 0) {
return emptyMap()
}
val nameIndex = cursor.getColumnIndex("name")
val typeIndex = cursor.getColumnIndex("type")
val notNullIndex = cursor.getColumnIndex("notnull")
val pkIndex = cursor.getColumnIndex("pk")
val defaultValueIndex = cursor.getColumnIndex("dflt_value")
return buildMap {
while (cursor.moveToNext()) {
val name = cursor.getString(nameIndex)
val type = cursor.getString(typeIndex)
val notNull = 0 != cursor.getInt(notNullIndex)
val primaryKeyPosition = cursor.getInt(pkIndex)
val defaultValue = cursor.getString(defaultValueIndex)
put(
key = name,
value = TableInfo.Column(
name = name,
type = type,
notNull = notNull,
primaryKeyPosition = primaryKeyPosition,
defaultValue = defaultValue,
createdFrom = TableInfo.CREATED_FROM_DATABASE
)
)
}
}
}
}
/**
* @return null if we cannot read the indices due to older sqlite implementations.
*/
private fun readIndices(database: SupportSQLiteDatabase, tableName: String): Set<TableInfo.Index>? {
database.query("PRAGMA index_list(`$tableName`)").useCursor { cursor ->
val nameColumnIndex = cursor.getColumnIndex("name")
val originColumnIndex = cursor.getColumnIndex("origin")
val uniqueIndex = cursor.getColumnIndex("unique")
if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
// we cannot read them so better not validate any index.
return null
}
return buildSet {
while (cursor.moveToNext()) {
val origin = cursor.getString(originColumnIndex)
if ("c" != origin) {
// Ignore auto-created indices
continue
}
val name = cursor.getString(nameColumnIndex)
val unique = cursor.getInt(uniqueIndex) == 1
// Read index but if we cannot read it properly so better not read it
val index = readIndex(database, name, unique) ?: return null
add(index)
}
}
}
}
/**
* @return null if we cannot read the index due to older sqlite implementations.
*/
private fun readIndex(
database: SupportSQLiteDatabase,
name: String,
unique: Boolean
): TableInfo.Index? {
return database.query("PRAGMA index_xinfo(`$name`)").useCursor { cursor ->
val seqnoColumnIndex = cursor.getColumnIndex("seqno")
val cidColumnIndex = cursor.getColumnIndex("cid")
val nameColumnIndex = cursor.getColumnIndex("name")
val descColumnIndex = cursor.getColumnIndex("desc")
if (
seqnoColumnIndex == -1 ||
cidColumnIndex == -1 ||
nameColumnIndex == -1 ||
descColumnIndex == -1
) {
// we cannot read them so better not validate any index.
return null
}
val columnsMap = TreeMap<Int, String>()
val ordersMap = TreeMap<Int, String>()
while (cursor.moveToNext()) {
val cid = cursor.getInt(cidColumnIndex)
if (cid < 0) {
// Ignore SQLite row ID
continue
}
val seq = cursor.getInt(seqnoColumnIndex)
val columnName = cursor.getString(nameColumnIndex)
val order = if (cursor.getInt(descColumnIndex) > 0) "DESC" else "ASC"
columnsMap[seq] = columnName
ordersMap[seq] = order
}
val columns = columnsMap.values.toList()
val orders = ordersMap.values.toList()
TableInfo.Index(name, unique, columns, orders)
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:JvmName("UUIDUtil")
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
package androidx.room.util
import androidx.annotation.RestrictTo
import java.nio.ByteBuffer
import java.util.UUID
/**
* UUID / byte[] two-way conversion utility for Room
*
*/
/**
* Converts a 16-bytes array BLOB into a UUID pojo
*
* @param bytes byte array stored in database as BLOB
* @return a UUID object created based on the provided byte array
*/
fun convertByteToUUID(bytes: ByteArray): UUID {
val buffer = ByteBuffer.wrap(bytes)
val firstLong = buffer.long
val secondLong = buffer.long
return UUID(firstLong, secondLong)
}
/**
* Converts a UUID pojo into a 16-bytes array to store into database as BLOB
*
* @param uuid the UUID pojo
* @return a byte array to store into database
*/
fun convertUUIDToByte(uuid: UUID): ByteArray {
val bytes = ByteArray(16)
val buffer = ByteBuffer.wrap(bytes)
buffer.putLong(uuid.mostSignificantBits)
buffer.putLong(uuid.leastSignificantBits)
return buffer.array()
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.room.util
import androidx.annotation.RestrictTo
import androidx.sqlite.db.SupportSQLiteDatabase
/**
* A data class that holds the information about a view.
*
*
* This derives information from sqlite_master.
*
*
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class ViewInfo(
/**
* The view name
*/
@JvmField
val name: String,
/**
* The SQL of CREATE VIEW.
*/
@JvmField
val sql: String?
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ViewInfo) return false
return ((name == other.name) && if (sql != null) sql == other.sql else other.sql == null)
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + (sql?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return ("ViewInfo{" + "name='" + name + '\'' + ", sql='" + sql + '\'' + '}')
}
companion object {
/**
* Reads the view information from the given database.
*
* @param database The database to read the information from.
* @param viewName The view name.
* @return A ViewInfo containing the schema information for the provided view name.
*/
@JvmStatic
fun read(database: SupportSQLiteDatabase, viewName: String): ViewInfo {
return database.query(
"SELECT name, sql FROM sqlite_master " +
"WHERE type = 'view' AND name = '$viewName'"
).useCursor { cursor ->
if (cursor.moveToFirst()) {
ViewInfo(cursor.getString(0), cursor.getString(1))
} else {
ViewInfo(viewName, null)
}
}
}
}
}

View File

@ -563,7 +563,7 @@ public abstract class DB extends RoomDatabase {
if (BuildConfig.DEBUG && false)
builder.setQueryCallback(new QueryCallback() {
@Override
public void onQuery(@NonNull String sqlQuery, @NonNull List<Object> bindArgs) {
public void onQuery(@NonNull String sqlQuery, @NonNull List<?> bindArgs) {
Log.i("query=" + sqlQuery);
}
}, Helper.getParallelExecutor());