mirror of https://github.com/M66B/FairEmail.git
parent
8f2bab4c48
commit
03a331175f
@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.arch.core.util.Function;
|
||||||
|
import androidx.room.util.SneakyThrow;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AutoCloser is responsible for automatically opening (using
|
||||||
|
* delegateOpenHelper) and closing (on a timer started when there are no remaining references) a
|
||||||
|
* SupportSqliteDatabase.
|
||||||
|
*
|
||||||
|
* It is important to ensure that the ref count is incremented when using a returned database.
|
||||||
|
*/
|
||||||
|
final class AutoCloser {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private SupportSQLiteOpenHelper mDelegateOpenHelper = null;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
// Package private for access from mAutoCloser
|
||||||
|
@Nullable
|
||||||
|
Runnable mOnAutoCloseCallback = null;
|
||||||
|
|
||||||
|
// Package private for access from mAutoCloser
|
||||||
|
@NonNull
|
||||||
|
final Object mLock = new Object();
|
||||||
|
|
||||||
|
// Package private for access from mAutoCloser
|
||||||
|
final long mAutoCloseTimeoutInMs;
|
||||||
|
|
||||||
|
// Package private for access from mExecuteAutoCloser
|
||||||
|
@NonNull
|
||||||
|
final Executor mExecutor;
|
||||||
|
|
||||||
|
// Package private for access from mAutoCloser
|
||||||
|
@GuardedBy("mLock")
|
||||||
|
int mRefCount = 0;
|
||||||
|
|
||||||
|
// Package private for access from mAutoCloser
|
||||||
|
@GuardedBy("mLock")
|
||||||
|
long mLastDecrementRefCountTimeStamp = SystemClock.uptimeMillis();
|
||||||
|
|
||||||
|
// The unwrapped SupportSqliteDatabase
|
||||||
|
// Package private for access from mAutoCloser
|
||||||
|
@GuardedBy("mLock")
|
||||||
|
@Nullable
|
||||||
|
SupportSQLiteDatabase mDelegateDatabase;
|
||||||
|
|
||||||
|
private boolean mManuallyClosed = false;
|
||||||
|
|
||||||
|
private final Runnable mExecuteAutoCloser = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mExecutor.execute(mAutoCloser);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Package private for access from mExecuteAutoCloser
|
||||||
|
@NonNull
|
||||||
|
final Runnable mAutoCloser = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
if (SystemClock.uptimeMillis() - mLastDecrementRefCountTimeStamp
|
||||||
|
< mAutoCloseTimeoutInMs) {
|
||||||
|
// An increment + decrement beat us to closing the db. We
|
||||||
|
// will not close the database, and there should be at least
|
||||||
|
// one more auto-close scheduled.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mRefCount != 0) {
|
||||||
|
// An increment beat us to closing the db. We don't close the
|
||||||
|
// db, and another closer will be scheduled once the ref
|
||||||
|
// count is decremented.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOnAutoCloseCallback != null) {
|
||||||
|
mOnAutoCloseCallback.run();
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("mOnAutoCloseCallback is null but it should"
|
||||||
|
+ " have been set before use. Please file a bug "
|
||||||
|
+ "against Room at: https://issuetracker.google"
|
||||||
|
+ ".com/issues/new?component=413107&template=1096568");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) {
|
||||||
|
try {
|
||||||
|
mDelegateDatabase.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
SneakyThrow.reThrow(e);
|
||||||
|
}
|
||||||
|
mDelegateDatabase = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an AutoCloser.
|
||||||
|
*
|
||||||
|
* @param autoCloseTimeoutAmount time for auto close timer
|
||||||
|
* @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount
|
||||||
|
* @param autoCloseExecutor the executor on which the auto close operation will happen
|
||||||
|
*/
|
||||||
|
AutoCloser(long autoCloseTimeoutAmount,
|
||||||
|
@NonNull TimeUnit autoCloseTimeUnit,
|
||||||
|
@NonNull Executor autoCloseExecutor) {
|
||||||
|
mAutoCloseTimeoutInMs = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount);
|
||||||
|
mExecutor = autoCloseExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the
|
||||||
|
* delegateOpenHelper after construction.
|
||||||
|
*
|
||||||
|
* @param delegateOpenHelper the open helper that is used to create
|
||||||
|
* new SupportSqliteDatabases
|
||||||
|
*/
|
||||||
|
public void init(@NonNull SupportSQLiteOpenHelper delegateOpenHelper) {
|
||||||
|
if (mDelegateOpenHelper != null) {
|
||||||
|
Log.e(Room.LOG_TAG, "AutoCloser initialized multiple times. Please file a bug against"
|
||||||
|
+ " room at: https://issuetracker.google"
|
||||||
|
+ ".com/issues/new?component=413107&template=1096568");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mDelegateOpenHelper = delegateOpenHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a ref counting function. The function will receive an unwrapped open database and
|
||||||
|
* this database will stay open until at least after function returns. If there are no more
|
||||||
|
* references in use for the db once function completes, an auto close operation will be
|
||||||
|
* scheduled.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public <V> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,227 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.SystemClock
|
|
||||||
import androidx.annotation.GuardedBy
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AutoCloser is responsible for automatically opening (using
|
|
||||||
* delegateOpenHelper) and closing (on a timer started when there are no remaining references) a
|
|
||||||
* SupportSqliteDatabase.
|
|
||||||
*
|
|
||||||
* It is important to ensure that the ref count is incremented when using a returned database.
|
|
||||||
*
|
|
||||||
* @param autoCloseTimeoutAmount time for auto close timer
|
|
||||||
* @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount
|
|
||||||
* @param autoCloseExecutor the executor on which the auto close operation will happen
|
|
||||||
*/
|
|
||||||
internal class AutoCloser(
|
|
||||||
autoCloseTimeoutAmount: Long,
|
|
||||||
autoCloseTimeUnit: TimeUnit,
|
|
||||||
autoCloseExecutor: Executor
|
|
||||||
) {
|
|
||||||
lateinit var delegateOpenHelper: SupportSQLiteOpenHelper
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
|
||||||
|
|
||||||
internal var onAutoCloseCallback: Runnable? = null
|
|
||||||
|
|
||||||
private val lock = Any()
|
|
||||||
|
|
||||||
private var autoCloseTimeoutInMs: Long = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount)
|
|
||||||
|
|
||||||
private val executor: Executor = autoCloseExecutor
|
|
||||||
|
|
||||||
@GuardedBy("lock")
|
|
||||||
internal var refCount = 0
|
|
||||||
|
|
||||||
@GuardedBy("lock")
|
|
||||||
internal var lastDecrementRefCountTimeStamp = SystemClock.uptimeMillis()
|
|
||||||
|
|
||||||
// The unwrapped SupportSqliteDatabase
|
|
||||||
@GuardedBy("lock")
|
|
||||||
internal var delegateDatabase: SupportSQLiteDatabase? = null
|
|
||||||
|
|
||||||
private var manuallyClosed = false
|
|
||||||
|
|
||||||
private val executeAutoCloser = Runnable { executor.execute(autoCloser) }
|
|
||||||
|
|
||||||
private val autoCloser = Runnable {
|
|
||||||
synchronized(lock) {
|
|
||||||
if (SystemClock.uptimeMillis() - lastDecrementRefCountTimeStamp
|
|
||||||
< autoCloseTimeoutInMs
|
|
||||||
) {
|
|
||||||
// An increment + decrement beat us to closing the db. We
|
|
||||||
// will not close the database, and there should be at least
|
|
||||||
// one more auto-close scheduled.
|
|
||||||
return@Runnable
|
|
||||||
}
|
|
||||||
if (refCount != 0) {
|
|
||||||
// An increment beat us to closing the db. We don't close the
|
|
||||||
// db, and another closer will be scheduled once the ref
|
|
||||||
// count is decremented.
|
|
||||||
return@Runnable
|
|
||||||
}
|
|
||||||
onAutoCloseCallback?.run() ?: error(
|
|
||||||
"onAutoCloseCallback is null but it should" +
|
|
||||||
" have been set before use. Please file a bug " +
|
|
||||||
"against Room at: $autoCloseBug"
|
|
||||||
)
|
|
||||||
|
|
||||||
delegateDatabase?.let {
|
|
||||||
if (it.isOpen) {
|
|
||||||
it.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delegateDatabase = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the
|
|
||||||
* delegateOpenHelper after construction.
|
|
||||||
*
|
|
||||||
* @param delegateOpenHelper the open helper that is used to create
|
|
||||||
* new SupportSqliteDatabases
|
|
||||||
*/
|
|
||||||
fun init(delegateOpenHelper: SupportSQLiteOpenHelper) {
|
|
||||||
this.delegateOpenHelper = delegateOpenHelper
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a ref counting function. The function will receive an unwrapped open database and
|
|
||||||
* this database will stay open until at least after function returns. If there are no more
|
|
||||||
* references in use for the db once function completes, an auto close operation will be
|
|
||||||
* scheduled.
|
|
||||||
*/
|
|
||||||
fun <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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,878 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.CharArrayBuffer;
|
||||||
|
import android.database.ContentObserver;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.DataSetObserver;
|
||||||
|
import android.database.SQLException;
|
||||||
|
import android.database.sqlite.SQLiteTransactionListener;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.CancellationSignal;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.arch.core.util.Function;
|
||||||
|
import androidx.room.util.SneakyThrow;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteCompat;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SupportSQLiteOpenHelper that has autoclose enabled for database connections.
|
||||||
|
*/
|
||||||
|
final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
|
||||||
|
@NonNull
|
||||||
|
private final SupportSQLiteOpenHelper mDelegateOpenHelper;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final AutoClosingSupportSQLiteDatabase mAutoClosingDb;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final AutoCloser mAutoCloser;
|
||||||
|
|
||||||
|
AutoClosingRoomOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
|
||||||
|
@NonNull AutoCloser autoCloser) {
|
||||||
|
mDelegateOpenHelper = supportSQLiteOpenHelper;
|
||||||
|
mAutoCloser = autoCloser;
|
||||||
|
autoCloser.init(mDelegateOpenHelper);
|
||||||
|
mAutoClosingDb = new AutoClosingSupportSQLiteDatabase(mAutoCloser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getDatabaseName() {
|
||||||
|
return mDelegateOpenHelper.getDatabaseName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
public void setWriteAheadLoggingEnabled(boolean enabled) {
|
||||||
|
mDelegateOpenHelper.setWriteAheadLoggingEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
|
@Override
|
||||||
|
public SupportSQLiteDatabase getWritableDatabase() {
|
||||||
|
// Note we don't differentiate between writable db and readable db
|
||||||
|
// We try to open the db so the open callbacks run
|
||||||
|
mAutoClosingDb.pokeOpen();
|
||||||
|
return mAutoClosingDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
|
@Override
|
||||||
|
public SupportSQLiteDatabase getReadableDatabase() {
|
||||||
|
// Note we don't differentiate between writable db and readable db
|
||||||
|
// We try to open the db so the open callbacks run
|
||||||
|
mAutoClosingDb.pokeOpen();
|
||||||
|
return mAutoClosingDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
mAutoClosingDb.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
SneakyThrow.reThrow(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* package protected to pass it to invalidation tracker...
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
AutoCloser getAutoCloser() {
|
||||||
|
return this.mAutoCloser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
SupportSQLiteDatabase getAutoClosingDb() {
|
||||||
|
return this.mAutoClosingDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public SupportSQLiteOpenHelper getDelegate() {
|
||||||
|
return mDelegateOpenHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SupportSQLiteDatabase that also keeps refcounts and autocloses the database
|
||||||
|
*/
|
||||||
|
static final class AutoClosingSupportSQLiteDatabase implements SupportSQLiteDatabase {
|
||||||
|
@NonNull
|
||||||
|
private final AutoCloser mAutoCloser;
|
||||||
|
|
||||||
|
AutoClosingSupportSQLiteDatabase(@NonNull AutoCloser autoCloser) {
|
||||||
|
mAutoCloser = autoCloser;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pokeOpen() {
|
||||||
|
mAutoCloser.executeRefCountingFunction(db -> null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SupportSQLiteStatement compileStatement(String sql) {
|
||||||
|
return new AutoClosingSupportSqliteStatement(sql, mAutoCloser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginTransaction() {
|
||||||
|
// We assume that after every successful beginTransaction() call there *must* be a
|
||||||
|
// endTransaction() call.
|
||||||
|
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||||
|
try {
|
||||||
|
db.beginTransaction();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||||
|
// fails since there won't be a corresponding endTransaction call.
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginTransactionNonExclusive() {
|
||||||
|
// We assume that after every successful beginTransaction() call there *must* be a
|
||||||
|
// endTransaction() call.
|
||||||
|
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||||
|
try {
|
||||||
|
db.beginTransactionNonExclusive();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||||
|
// fails since there won't be a corresponding endTransaction call.
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
|
||||||
|
// We assume that after every successful beginTransaction() call there *must* be a
|
||||||
|
// endTransaction() call.
|
||||||
|
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||||
|
try {
|
||||||
|
db.beginTransactionWithListener(transactionListener);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||||
|
// fails since there won't be a corresponding endTransaction call.
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginTransactionWithListenerNonExclusive(
|
||||||
|
SQLiteTransactionListener transactionListener) {
|
||||||
|
// We assume that after every successful beginTransaction() call there *will* always
|
||||||
|
// be a corresponding endTransaction() call. Without a corresponding
|
||||||
|
// endTransactionCall we will never close the db.
|
||||||
|
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||||
|
try {
|
||||||
|
db.beginTransactionWithListenerNonExclusive(transactionListener);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||||
|
// fails since there won't be a corresponding endTransaction call.
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTransaction() {
|
||||||
|
if (mAutoCloser.getDelegateDatabase() == null) {
|
||||||
|
// This should never happen.
|
||||||
|
throw new IllegalStateException("End transaction called but delegateDb is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mAutoCloser.getDelegateDatabase().endTransaction();
|
||||||
|
} finally {
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTransactionSuccessful() {
|
||||||
|
SupportSQLiteDatabase delegate = mAutoCloser.getDelegateDatabase();
|
||||||
|
|
||||||
|
if (delegate == null) {
|
||||||
|
// This should never happen.
|
||||||
|
throw new IllegalStateException("setTransactionSuccessful called but delegateDb "
|
||||||
|
+ "is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate.setTransactionSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inTransaction() {
|
||||||
|
if (mAutoCloser.getDelegateDatabase() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDbLockedByCurrentThread() {
|
||||||
|
if (mAutoCloser.getDelegateDatabase() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mAutoCloser.executeRefCountingFunction(
|
||||||
|
SupportSQLiteDatabase::isDbLockedByCurrentThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean yieldIfContendedSafely() {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(
|
||||||
|
SupportSQLiteDatabase::yieldIfContendedSafely);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(
|
||||||
|
SupportSQLiteDatabase::yieldIfContendedSafely);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setVersion(int version) {
|
||||||
|
mAutoCloser.executeRefCountingFunction(db -> {
|
||||||
|
db.setVersion(version);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getMaximumSize() {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getMaximumSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long setMaximumSize(long numBytes) {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(db -> db.setMaximumSize(numBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPageSize() {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPageSize(long numBytes) {
|
||||||
|
mAutoCloser.executeRefCountingFunction(db -> {
|
||||||
|
db.setPageSize(numBytes);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(String query) {
|
||||||
|
Cursor result;
|
||||||
|
try {
|
||||||
|
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||||
|
result = db.query(query);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
throw throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeepAliveCursor(result, mAutoCloser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(String query, Object[] bindArgs) {
|
||||||
|
Cursor result;
|
||||||
|
try {
|
||||||
|
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||||
|
result = db.query(query, bindArgs);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
throw throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeepAliveCursor(result, mAutoCloser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cursor query(SupportSQLiteQuery query) {
|
||||||
|
|
||||||
|
Cursor result;
|
||||||
|
try {
|
||||||
|
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||||
|
result = db.query(query);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
throw throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeepAliveCursor(result, mAutoCloser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
|
@Override
|
||||||
|
public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) {
|
||||||
|
Cursor result;
|
||||||
|
try {
|
||||||
|
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||||
|
result = db.query(query, cancellationSignal);
|
||||||
|
} catch (Throwable throwable) {
|
||||||
|
mAutoCloser.decrementCountAndScheduleClose();
|
||||||
|
throw throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KeepAliveCursor(result, mAutoCloser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long insert(String table, int conflictAlgorithm, ContentValues values)
|
||||||
|
throws SQLException {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(db -> db.insert(table, conflictAlgorithm,
|
||||||
|
values));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int delete(String table, String whereClause, Object[] whereArgs) {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(
|
||||||
|
db -> db.delete(table, whereClause, whereArgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int update(String table, int conflictAlgorithm, ContentValues values,
|
||||||
|
String whereClause, Object[] whereArgs) {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(db -> db.update(table, conflictAlgorithm,
|
||||||
|
values, whereClause, whereArgs));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execSQL(String sql) throws SQLException {
|
||||||
|
mAutoCloser.executeRefCountingFunction(db -> {
|
||||||
|
db.execSQL(sql);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execSQL(String sql, Object[] bindArgs) throws SQLException {
|
||||||
|
mAutoCloser.executeRefCountingFunction(db -> {
|
||||||
|
db.execSQL(sql, bindArgs);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::isReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOpen() {
|
||||||
|
// Get the db without incrementing the reference cause we don't want to open
|
||||||
|
// the db for an isOpen call.
|
||||||
|
SupportSQLiteDatabase localDelegate = mAutoCloser.getDelegateDatabase();
|
||||||
|
|
||||||
|
if (localDelegate == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return localDelegate.isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean needUpgrade(int newVersion) {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(db -> db.needUpgrade(newVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPath() {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLocale(Locale locale) {
|
||||||
|
mAutoCloser.executeRefCountingFunction(db -> {
|
||||||
|
db.setLocale(locale);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMaxSqlCacheSize(int cacheSize) {
|
||||||
|
mAutoCloser.executeRefCountingFunction(db -> {
|
||||||
|
db.setMaxSqlCacheSize(cacheSize);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
@Override
|
||||||
|
public void setForeignKeyConstraintsEnabled(boolean enable) {
|
||||||
|
mAutoCloser.executeRefCountingFunction(db -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
db.setForeignKeyConstraintsEnabled(enable);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean enableWriteAheadLogging() {
|
||||||
|
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
|
||||||
|
+ "OpenHelper instead of on the database directly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableWriteAheadLogging() {
|
||||||
|
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
|
||||||
|
+ "OpenHelper instead of on the database directly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
@Override
|
||||||
|
public boolean isWriteAheadLoggingEnabled() {
|
||||||
|
return mAutoCloser.executeRefCountingFunction(db -> {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
return db.isWriteAheadLoggingEnabled();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,570 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.SQLException
|
|
||||||
import android.database.sqlite.SQLiteTransactionListener
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.CancellationSignal
|
|
||||||
import android.util.Pair
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.sqlite.db.SupportSQLiteCompat
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A SupportSQLiteOpenHelper that has auto close enabled for database connections.
|
|
||||||
*/
|
|
||||||
internal class AutoClosingRoomOpenHelper(
|
|
||||||
override val delegate: SupportSQLiteOpenHelper,
|
|
||||||
@JvmField
|
|
||||||
internal val autoCloser: AutoCloser
|
|
||||||
) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper {
|
|
||||||
private val autoClosingDb: AutoClosingSupportSQLiteDatabase
|
|
||||||
|
|
||||||
init {
|
|
||||||
autoCloser.init(delegate)
|
|
||||||
autoClosingDb = AutoClosingSupportSQLiteDatabase(
|
|
||||||
autoCloser
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
override val writableDatabase: SupportSQLiteDatabase
|
|
||||||
get() {
|
|
||||||
autoClosingDb.pokeOpen()
|
|
||||||
return autoClosingDb
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
override val readableDatabase: SupportSQLiteDatabase
|
|
||||||
get() {
|
|
||||||
// Note we don't differentiate between writable db and readable db
|
|
||||||
// We try to open the db so the open callbacks run
|
|
||||||
autoClosingDb.pokeOpen()
|
|
||||||
return autoClosingDb
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
autoClosingDb.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SupportSQLiteDatabase that also keeps refcounts and autocloses the database
|
|
||||||
*/
|
|
||||||
internal class AutoClosingSupportSQLiteDatabase(
|
|
||||||
private val autoCloser: AutoCloser
|
|
||||||
) : SupportSQLiteDatabase {
|
|
||||||
fun pokeOpen() {
|
|
||||||
autoCloser.executeRefCountingFunction<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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory class for AutoClosingRoomOpenHelper
|
||||||
|
*/
|
||||||
|
final class AutoClosingRoomOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
|
||||||
|
@NonNull
|
||||||
|
private final SupportSQLiteOpenHelper.Factory mDelegate;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final AutoCloser mAutoCloser;
|
||||||
|
|
||||||
|
AutoClosingRoomOpenHelperFactory(
|
||||||
|
@NonNull SupportSQLiteOpenHelper.Factory factory,
|
||||||
|
@NonNull AutoCloser autoCloser) {
|
||||||
|
mDelegate = factory;
|
||||||
|
mAutoCloser = autoCloser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return AutoClosingRoomOpenHelper instances.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public AutoClosingRoomOpenHelper create(
|
||||||
|
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
|
||||||
|
return new AutoClosingRoomOpenHelper(mDelegate.create(configuration), mAutoCloser);
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class for AutoClosingRoomOpenHelper
|
|
||||||
*/
|
|
||||||
internal class AutoClosingRoomOpenHelperFactory(
|
|
||||||
private val delegate: SupportSQLiteOpenHelper.Factory,
|
|
||||||
private val autoCloser: AutoCloser
|
|
||||||
) : SupportSQLiteOpenHelper.Factory {
|
|
||||||
/**
|
|
||||||
* @return AutoClosingRoomOpenHelper instances.
|
|
||||||
*/
|
|
||||||
override fun create(
|
|
||||||
configuration: SupportSQLiteOpenHelper.Configuration
|
|
||||||
): AutoClosingRoomOpenHelper {
|
|
||||||
return AutoClosingRoomOpenHelper(delegate.create(configuration), autoCloser)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for in-process and multi-process key-based lock mechanism for safely copying
|
||||||
|
* database files.
|
||||||
|
* <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.MatrixCursor;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cursor utilities for Room
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||||
|
public class CursorUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the given cursor into a in-memory cursor and then closes it.
|
||||||
|
* <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() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import android.database.AbstractWindowedCursor;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.CancellationSignal;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.room.RoomDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteCompat;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database utilities for Room
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||||
|
public class DBUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the SQLiteQuery on the given database.
|
||||||
|
* <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() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,629 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.room.migration.AutoMigrationSpec;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration class for a {@link RoomDatabase}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
public class DatabaseConfiguration {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The factory to use to access the database.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
|
||||||
|
/**
|
||||||
|
* The context to use while connecting to the database.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final Context context;
|
||||||
|
/**
|
||||||
|
* The name of the database file or null if it is an in-memory database.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of available migrations.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public final RoomDatabase.MigrationContainer migrationContainer;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public final List<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));
|
||||||
|
}
|
||||||
|
}
|
@ -1,680 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.room.migration.AutoMigrationSpec
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.concurrent.Callable
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration class for a [RoomDatabase].
|
|
||||||
*/
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
open class DatabaseConfiguration @SuppressLint("LambdaLast")
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
constructor(
|
|
||||||
/**
|
|
||||||
* The context to use while connecting to the database.
|
|
||||||
*/
|
|
||||||
@JvmField
|
|
||||||
val context: Context,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of the database file or null if it is an in-memory database.
|
|
||||||
*/
|
|
||||||
@JvmField
|
|
||||||
val name: String?,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The factory to use to access the database.
|
|
||||||
*/
|
|
||||||
@JvmField
|
|
||||||
val sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collection of available migrations.
|
|
||||||
*/
|
|
||||||
@JvmField
|
|
||||||
val migrationContainer: RoomDatabase.MigrationContainer,
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val callbacks: List<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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This file exists to trick AGP/lint to work around b/234865137
|
@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementations of this class knows how to insert a particular entity.
|
||||||
|
* <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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,228 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementations of this class knows how to insert a particular entity.
|
|
||||||
*
|
|
||||||
* This is an internal library class and all of its implementations are auto-generated.
|
|
||||||
*
|
|
||||||
* @constructor Creates an InsertionAdapter that can insert the entity type T into the given
|
|
||||||
* database.
|
|
||||||
*
|
|
||||||
* @param T The type parameter of the entity to be inserted
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
abstract class EntityInsertionAdapter<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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,223 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteConstraintException
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The error code defined by SQLite Library for SQLITE_CONSTRAINT_PRIMARYKEY error
|
|
||||||
* Only used by android of version newer than 19.
|
|
||||||
*/
|
|
||||||
private const val SQLITE_CONSTRAINT_PRIMARYKEY = "1555"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The error code defined by SQLite Library for SQLITE_CONSTRAINT_UNIQUE error.
|
|
||||||
*/
|
|
||||||
private const val SQLITE_CONSTRAINT_UNIQUE = "2067"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For android of version below and including 19, use error message instead of
|
|
||||||
* error code to check
|
|
||||||
*/
|
|
||||||
private const val ErrorMsg = "unique"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class knows how to insert an entity. When the insertion fails
|
|
||||||
* due to a unique constraint conflict (i.e. primary key conflict),
|
|
||||||
* it will perform an update.
|
|
||||||
*
|
|
||||||
* @constructor Creates an EntityUpsertionAdapter that can upsert entity of type T
|
|
||||||
* into the database using the given insertionAdapter to perform insertion and
|
|
||||||
* updateAdapter to perform update when the insertion fails
|
|
||||||
*
|
|
||||||
* @param T the type param of the entity to be upserted
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
class EntityUpsertionAdapter<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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File utilities for Room
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||||
|
public class FileUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies data from the input channel to the output file channel.
|
||||||
|
*
|
||||||
|
* @param input the input channel to copy.
|
||||||
|
* @param output the output channel to copy.
|
||||||
|
* @throws IOException if there is an I/O error.
|
||||||
|
*/
|
||||||
|
@SuppressLint("LambdaLast")
|
||||||
|
public static void copy(@NonNull ReadableByteChannel input, @NonNull FileChannel output)
|
||||||
|
throws IOException {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
||||||
|
output.transferFrom(input, 0, Long.MAX_VALUE);
|
||||||
|
} else {
|
||||||
|
InputStream inputStream = Channels.newInputStream(input);
|
||||||
|
OutputStream outputStream = Channels.newOutputStream(output);
|
||||||
|
int length;
|
||||||
|
byte[] buffer = new byte[1024 * 4];
|
||||||
|
while ((length = inputStream.read(buffer)) > 0) {
|
||||||
|
outputStream.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.force(false);
|
||||||
|
} finally {
|
||||||
|
input.close();
|
||||||
|
output.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileUtil() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,220 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class that holds the information about an FTS table.
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||||
|
public final class FtsTableInfo {
|
||||||
|
|
||||||
|
// A set of valid FTS Options
|
||||||
|
private static final String[] FTS_OPTIONS = new String[] {
|
||||||
|
"tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
|
||||||
|
"order=", "prefix=", "uncompress="};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The table name
|
||||||
|
*/
|
||||||
|
public final String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The column names
|
||||||
|
*/
|
||||||
|
public final Set<String> columns;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The set of options. Each value in the set contains the option in the following format:
|
||||||
|
* <key>=<value>.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class that maintains {@link RoomTrackingLiveData} instances for an
|
||||||
|
* {@link InvalidationTracker}.
|
||||||
|
* <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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import java.util.Collections
|
|
||||||
import java.util.IdentityHashMap
|
|
||||||
import java.util.concurrent.Callable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper class that maintains [RoomTrackingLiveData] instances for an
|
|
||||||
* [InvalidationTracker].
|
|
||||||
*
|
|
||||||
* We keep a strong reference to active LiveData instances to avoid garbage collection in case
|
|
||||||
* developer does not hold onto the returned LiveData.
|
|
||||||
*/
|
|
||||||
internal class InvalidationLiveDataContainer(private val database: RoomDatabase) {
|
|
||||||
internal val liveDataSet: MutableSet<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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,902 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.arch.core.internal.SafeIterableMap;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
|
||||||
|
* these tables.
|
||||||
|
*/
|
||||||
|
// Some details on how the InvalidationTracker works:
|
||||||
|
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
|
||||||
|
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
|
||||||
|
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
|
||||||
|
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
|
||||||
|
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
|
||||||
|
// tables.
|
||||||
|
// * Each update (write operation) on one of the observed tables triggers an update into the
|
||||||
|
// memory table table, flipping the invalidated flag ON.
|
||||||
|
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
|
||||||
|
// It works as an Observer, and notifies other instances of table invalidation.
|
||||||
|
public class InvalidationTracker {
|
||||||
|
|
||||||
|
private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
|
||||||
|
|
||||||
|
private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
|
||||||
|
|
||||||
|
private static final String TABLE_ID_COLUMN_NAME = "table_id";
|
||||||
|
|
||||||
|
private static final String INVALIDATED_COLUMN_NAME = "invalidated";
|
||||||
|
|
||||||
|
private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
|
||||||
|
+ "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, "
|
||||||
|
+ INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
|
||||||
|
+ " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
|
||||||
|
+ " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
final HashMap<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,839 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.database.sqlite.SQLiteException
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.GuardedBy
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.arch.core.internal.SafeIterableMap
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.room.Room.LOG_TAG
|
|
||||||
import androidx.room.util.useCursor
|
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.util.Arrays
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.concurrent.Callable
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
|
|
||||||
* these tables.
|
|
||||||
*/
|
|
||||||
// Some details on how the InvalidationTracker works:
|
|
||||||
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
|
|
||||||
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
|
|
||||||
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
|
|
||||||
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
|
|
||||||
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
|
|
||||||
// tables.
|
|
||||||
// * Each update (write operation) on one of the observed tables triggers an update into the
|
|
||||||
// memory table table, flipping the invalidated flag ON.
|
|
||||||
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
|
|
||||||
// It works as an Observer, and notifies other instances of table invalidation.
|
|
||||||
open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) constructor(
|
|
||||||
internal val database: RoomDatabase,
|
|
||||||
private val shadowTablesMap: Map<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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to
|
||||||
|
* {@link MultiInstanceInvalidationService}.
|
||||||
|
*/
|
||||||
|
class MultiInstanceInvalidationClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The application context.
|
||||||
|
*/
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final Context mAppContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the database file.
|
||||||
|
*/
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final String mName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The client ID assigned by {@link MultiInstanceInvalidationService}.
|
||||||
|
*/
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
int mClientId;
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final InvalidationTracker mInvalidationTracker;
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final InvalidationTracker.Observer mObserver;
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
@Nullable
|
||||||
|
IMultiInstanceInvalidationService mService;
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final Executor mExecutor;
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final IMultiInstanceInvalidationCallback mCallback =
|
||||||
|
new IMultiInstanceInvalidationCallback.Stub() {
|
||||||
|
@Override
|
||||||
|
public void onInvalidation(final String[] tables) {
|
||||||
|
mExecutor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mInvalidationTracker.notifyObserversByTableNames(tables);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final AtomicBoolean mStopped = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
mService = IMultiInstanceInvalidationService.Stub.asInterface(service);
|
||||||
|
mExecutor.execute(mSetUpRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
mExecutor.execute(mRemoveObserverRunnable);
|
||||||
|
mService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final Runnable mSetUpRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
final IMultiInstanceInvalidationService service = mService;
|
||||||
|
if (service != null) {
|
||||||
|
mClientId = service.registerCallback(mCallback, mName);
|
||||||
|
mInvalidationTracker.addObserver(mObserver);
|
||||||
|
}
|
||||||
|
} catch (RemoteException e) {
|
||||||
|
Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// synthetic access
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
final Runnable mRemoveObserverRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mInvalidationTracker.removeObserver(mObserver);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context The Context to be used for binding
|
||||||
|
* {@link IMultiInstanceInvalidationService}.
|
||||||
|
* @param name The name of the database file.
|
||||||
|
* @param serviceIntent The {@link Intent} used for binding
|
||||||
|
* {@link IMultiInstanceInvalidationService}.
|
||||||
|
* @param invalidationTracker The {@link InvalidationTracker}
|
||||||
|
* @param executor The background executor.
|
||||||
|
*/
|
||||||
|
MultiInstanceInvalidationClient(Context context, String name, Intent serviceIntent,
|
||||||
|
InvalidationTracker invalidationTracker, Executor executor) {
|
||||||
|
mAppContext = context.getApplicationContext();
|
||||||
|
mName = name;
|
||||||
|
mInvalidationTracker = invalidationTracker;
|
||||||
|
mExecutor = executor;
|
||||||
|
// Use all tables names for observer.
|
||||||
|
final Set<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,129 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.RemoteException
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.room.Room.LOG_TAG
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles all the communication from [RoomDatabase] and [InvalidationTracker] to
|
|
||||||
* [MultiInstanceInvalidationService].
|
|
||||||
*
|
|
||||||
* @param context The Context to be used for binding
|
|
||||||
* [IMultiInstanceInvalidationService].
|
|
||||||
* @param name The name of the database file.
|
|
||||||
* @param serviceIntent The [Intent] used for binding
|
|
||||||
* [IMultiInstanceInvalidationService].
|
|
||||||
* @param invalidationTracker The [InvalidationTracker]
|
|
||||||
* @param executor The background executor.
|
|
||||||
*/
|
|
||||||
internal class MultiInstanceInvalidationClient(
|
|
||||||
context: Context,
|
|
||||||
val name: String,
|
|
||||||
serviceIntent: Intent,
|
|
||||||
val invalidationTracker: InvalidationTracker,
|
|
||||||
val executor: Executor
|
|
||||||
) {
|
|
||||||
private val appContext = context.applicationContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The client ID assigned by [MultiInstanceInvalidationService].
|
|
||||||
*/
|
|
||||||
var clientId = 0
|
|
||||||
lateinit var observer: InvalidationTracker.Observer
|
|
||||||
var service: IMultiInstanceInvalidationService? = null
|
|
||||||
|
|
||||||
val callback: IMultiInstanceInvalidationCallback =
|
|
||||||
object : IMultiInstanceInvalidationCallback.Stub() {
|
|
||||||
override fun onInvalidation(tables: Array<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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteCallbackList;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances.
|
||||||
|
* This service runs in the main app process. All the instances of {@link InvalidationTracker}
|
||||||
|
* (potentially in other processes) has to connect to this service.
|
||||||
|
*
|
||||||
|
* <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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.RemoteCallbackList
|
|
||||||
import android.os.RemoteException
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.room.Room.LOG_TAG
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [Service] for remote invalidation among multiple [InvalidationTracker] instances.
|
|
||||||
* This service runs in the main app process. All the instances of [InvalidationTracker]
|
|
||||||
* (potentially in other processes) has to connect to this service.
|
|
||||||
*
|
|
||||||
* The intent to launch it can be specified by
|
|
||||||
* [RoomDatabase.Builder.setMultiInstanceInvalidationServiceIntent], although the service is
|
|
||||||
* defined in the manifest by default so there should be no need to override it in a normal
|
|
||||||
* situation.
|
|
||||||
*/
|
|
||||||
@ExperimentalRoomApi
|
|
||||||
class MultiInstanceInvalidationService : Service() {
|
|
||||||
internal var maxClientId = 0
|
|
||||||
internal val clientNames = mutableMapOf<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
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,312 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.SQLException;
|
||||||
|
import android.database.sqlite.SQLiteTransactionListener;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.CancellationSignal;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements {@link SupportSQLiteDatabase} for SQLite queries.
|
||||||
|
*/
|
||||||
|
final class QueryInterceptorDatabase implements SupportSQLiteDatabase {
|
||||||
|
|
||||||
|
private final SupportSQLiteDatabase mDelegate;
|
||||||
|
private final RoomDatabase.QueryCallback mQueryCallback;
|
||||||
|
private final Executor mQueryCallbackExecutor;
|
||||||
|
|
||||||
|
QueryInterceptorDatabase(@NonNull SupportSQLiteDatabase supportSQLiteDatabase,
|
||||||
|
@NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
|
||||||
|
queryCallbackExecutor) {
|
||||||
|
mDelegate = supportSQLiteDatabase;
|
||||||
|
mQueryCallback = queryCallback;
|
||||||
|
mQueryCallbackExecutor = queryCallbackExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public SupportSQLiteStatement compileStatement(@NonNull String sql) {
|
||||||
|
return new QueryInterceptorStatement(mDelegate.compileStatement(sql),
|
||||||
|
mQueryCallback, sql, mQueryCallbackExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginTransaction() {
|
||||||
|
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
|
||||||
|
Collections.emptyList()));
|
||||||
|
mDelegate.beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginTransactionNonExclusive() {
|
||||||
|
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
|
||||||
|
Collections.emptyList()));
|
||||||
|
mDelegate.beginTransactionNonExclusive();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginTransactionWithListener(@NonNull SQLiteTransactionListener
|
||||||
|
transactionListener) {
|
||||||
|
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
|
||||||
|
Collections.emptyList()));
|
||||||
|
mDelegate.beginTransactionWithListener(transactionListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beginTransactionWithListenerNonExclusive(
|
||||||
|
@NonNull SQLiteTransactionListener transactionListener) {
|
||||||
|
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
|
||||||
|
Collections.emptyList()));
|
||||||
|
mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void endTransaction() {
|
||||||
|
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("END TRANSACTION",
|
||||||
|
Collections.emptyList()));
|
||||||
|
mDelegate.endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTransactionSuccessful() {
|
||||||
|
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("TRANSACTION SUCCESSFUL",
|
||||||
|
Collections.emptyList()));
|
||||||
|
mDelegate.setTransactionSuccessful();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inTransaction() {
|
||||||
|
return mDelegate.inTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDbLockedByCurrentThread() {
|
||||||
|
return mDelegate.isDbLockedByCurrentThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean yieldIfContendedSafely() {
|
||||||
|
return mDelegate.yieldIfContendedSafely();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
|
||||||
|
return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return mDelegate.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setVersion(int version) {
|
||||||
|
mDelegate.setVersion(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getMaximumSize() {
|
||||||
|
return mDelegate.getMaximumSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long setMaximumSize(long numBytes) {
|
||||||
|
return mDelegate.setMaximumSize(numBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getPageSize() {
|
||||||
|
return mDelegate.getPageSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPageSize(long numBytes) {
|
||||||
|
mDelegate.setPageSize(numBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull String query) {
|
||||||
|
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
|
||||||
|
Collections.emptyList()));
|
||||||
|
return mDelegate.query(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Cursor query(@NonNull String query, @NonNull Object[] bindArgs) {
|
||||||
|
List<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) {
|
||||||
|
}
|
||||||
|
}
|
@ -1,145 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.sqlite.SQLiteTransactionListener
|
|
||||||
import android.os.CancellationSignal
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements [SupportSQLiteDatabase] for SQLite queries.
|
|
||||||
*/
|
|
||||||
internal class QueryInterceptorDatabase(
|
|
||||||
private val delegate: SupportSQLiteDatabase,
|
|
||||||
private val queryCallbackExecutor: Executor,
|
|
||||||
private val queryCallback: RoomDatabase.QueryCallback
|
|
||||||
) : SupportSQLiteDatabase by delegate {
|
|
||||||
|
|
||||||
override fun compileStatement(sql: String): SupportSQLiteStatement {
|
|
||||||
return QueryInterceptorStatement(
|
|
||||||
delegate.compileStatement(sql),
|
|
||||||
sql,
|
|
||||||
queryCallbackExecutor,
|
|
||||||
queryCallback,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beginTransaction() {
|
|
||||||
queryCallbackExecutor.execute {
|
|
||||||
queryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", emptyList())
|
|
||||||
}
|
|
||||||
delegate.beginTransaction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beginTransactionNonExclusive() {
|
|
||||||
queryCallbackExecutor.execute {
|
|
||||||
queryCallback.onQuery("BEGIN DEFERRED TRANSACTION", emptyList())
|
|
||||||
}
|
|
||||||
delegate.beginTransactionNonExclusive()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) {
|
|
||||||
queryCallbackExecutor.execute {
|
|
||||||
queryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", emptyList())
|
|
||||||
}
|
|
||||||
delegate.beginTransactionWithListener(transactionListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beginTransactionWithListenerNonExclusive(
|
|
||||||
transactionListener: SQLiteTransactionListener
|
|
||||||
) {
|
|
||||||
queryCallbackExecutor.execute {
|
|
||||||
queryCallback.onQuery("BEGIN DEFERRED TRANSACTION", emptyList())
|
|
||||||
}
|
|
||||||
delegate.beginTransactionWithListenerNonExclusive(transactionListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun endTransaction() {
|
|
||||||
queryCallbackExecutor.execute {
|
|
||||||
queryCallback.onQuery("END TRANSACTION", emptyList())
|
|
||||||
}
|
|
||||||
delegate.endTransaction()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setTransactionSuccessful() {
|
|
||||||
queryCallbackExecutor.execute {
|
|
||||||
queryCallback.onQuery("TRANSACTION SUCCESSFUL", emptyList())
|
|
||||||
}
|
|
||||||
delegate.setTransactionSuccessful()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun query(query: String): Cursor {
|
|
||||||
queryCallbackExecutor.execute {
|
|
||||||
queryCallback.onQuery(query, emptyList())
|
|
||||||
}
|
|
||||||
return delegate.query(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun query(query: String, bindArgs: Array<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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
|
||||||
|
|
||||||
|
private final SupportSQLiteOpenHelper mDelegate;
|
||||||
|
private final RoomDatabase.QueryCallback mQueryCallback;
|
||||||
|
private final Executor mQueryCallbackExecutor;
|
||||||
|
|
||||||
|
QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
|
||||||
|
@NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
|
||||||
|
queryCallbackExecutor) {
|
||||||
|
mDelegate = supportSQLiteOpenHelper;
|
||||||
|
mQueryCallback = queryCallback;
|
||||||
|
mQueryCallbackExecutor = queryCallbackExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getDatabaseName() {
|
||||||
|
return mDelegate.getDatabaseName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
@Override
|
||||||
|
public void setWriteAheadLoggingEnabled(boolean enabled) {
|
||||||
|
mDelegate.setWriteAheadLoggingEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SupportSQLiteDatabase getWritableDatabase() {
|
||||||
|
return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback,
|
||||||
|
mQueryCallbackExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SupportSQLiteDatabase getReadableDatabase() {
|
||||||
|
return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback,
|
||||||
|
mQueryCallbackExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
mDelegate.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@NonNull
|
||||||
|
public SupportSQLiteOpenHelper getDelegate() {
|
||||||
|
return mDelegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
internal class QueryInterceptorOpenHelper(
|
|
||||||
override val delegate: SupportSQLiteOpenHelper,
|
|
||||||
private val queryCallbackExecutor: Executor,
|
|
||||||
private val queryCallback: RoomDatabase.QueryCallback
|
|
||||||
) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper {
|
|
||||||
override val writableDatabase: SupportSQLiteDatabase
|
|
||||||
get() = QueryInterceptorDatabase(
|
|
||||||
delegate.writableDatabase,
|
|
||||||
queryCallbackExecutor,
|
|
||||||
queryCallback
|
|
||||||
)
|
|
||||||
|
|
||||||
override val readableDatabase: SupportSQLiteDatabase
|
|
||||||
get() = QueryInterceptorDatabase(
|
|
||||||
delegate.readableDatabase,
|
|
||||||
queryCallbackExecutor,
|
|
||||||
queryCallback
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("AcronymName")
|
||||||
|
final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
|
||||||
|
|
||||||
|
private final SupportSQLiteOpenHelper.Factory mDelegate;
|
||||||
|
private final RoomDatabase.QueryCallback mQueryCallback;
|
||||||
|
private final Executor mQueryCallbackExecutor;
|
||||||
|
|
||||||
|
@SuppressWarnings("LambdaLast")
|
||||||
|
QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory,
|
||||||
|
@NonNull RoomDatabase.QueryCallback queryCallback,
|
||||||
|
@NonNull Executor queryCallbackExecutor) {
|
||||||
|
mDelegate = factory;
|
||||||
|
mQueryCallback = queryCallback;
|
||||||
|
mQueryCallbackExecutor = queryCallbackExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public SupportSQLiteOpenHelper create(
|
||||||
|
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
|
||||||
|
return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback,
|
||||||
|
mQueryCallbackExecutor);
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements [SupportSQLiteOpenHelper.Factory] to wrap [QueryInterceptorOpenHelper].
|
|
||||||
*/
|
|
||||||
internal class QueryInterceptorOpenHelperFactory(
|
|
||||||
private val delegate: SupportSQLiteOpenHelper.Factory,
|
|
||||||
private val queryCallbackExecutor: Executor,
|
|
||||||
private val queryCallback: RoomDatabase.QueryCallback,
|
|
||||||
) : SupportSQLiteOpenHelper.Factory by delegate {
|
|
||||||
override fun create(
|
|
||||||
configuration: SupportSQLiteOpenHelper.Configuration
|
|
||||||
): SupportSQLiteOpenHelper {
|
|
||||||
return QueryInterceptorOpenHelper(
|
|
||||||
delegate.create(configuration),
|
|
||||||
queryCallbackExecutor,
|
|
||||||
queryCallback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.sqlite.db.SupportSQLiteProgram;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A program implementing an {@link SupportSQLiteProgram} API to record bind arguments.
|
||||||
|
*/
|
||||||
|
final class QueryInterceptorProgram implements SupportSQLiteProgram {
|
||||||
|
private List<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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteProgram
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A program implementing an [SupportSQLiteProgram] API to record bind arguments.
|
|
||||||
*/
|
|
||||||
internal class QueryInterceptorProgram : SupportSQLiteProgram {
|
|
||||||
internal val bindArgsCache = mutableListOf<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
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements an instance of {@link SupportSQLiteStatement} for SQLite queries.
|
||||||
|
*/
|
||||||
|
final class QueryInterceptorStatement implements SupportSQLiteStatement {
|
||||||
|
|
||||||
|
private final SupportSQLiteStatement mDelegate;
|
||||||
|
private final RoomDatabase.QueryCallback mQueryCallback;
|
||||||
|
private final String mSqlStatement;
|
||||||
|
private final List<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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements an instance of [SupportSQLiteStatement] for SQLite queries.
|
|
||||||
*/
|
|
||||||
internal class QueryInterceptorStatement(
|
|
||||||
private val delegate: SupportSQLiteStatement,
|
|
||||||
private val sqlStatement: String,
|
|
||||||
private val queryCallbackExecutor: Executor,
|
|
||||||
private val queryCallback: RoomDatabase.QueryCallback,
|
|
||||||
) : SupportSQLiteStatement by delegate {
|
|
||||||
|
|
||||||
private val bindArgsCache = mutableListOf<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
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for Room.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class Room {
|
||||||
|
static final String LOG_TAG = "ROOM";
|
||||||
|
/**
|
||||||
|
* The master table where room keeps its metadata information.
|
||||||
|
*/
|
||||||
|
public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
|
||||||
|
private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
|
||||||
|
* should keep a reference to it and re-use it.
|
||||||
|
*
|
||||||
|
* @param context The context for the database. This is usually the Application context.
|
||||||
|
* @param klass The abstract class which is annotated with {@link Database} and extends
|
||||||
|
* {@link RoomDatabase}.
|
||||||
|
* @param name The name of the database file.
|
||||||
|
* @param <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() {
|
||||||
|
}
|
||||||
|
}
|
@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility functions for Room.
|
|
||||||
*/
|
|
||||||
object Room {
|
|
||||||
|
|
||||||
internal const val LOG_TAG = "ROOM"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The master table where room keeps its metadata information.
|
|
||||||
*/
|
|
||||||
const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME
|
|
||||||
|
|
||||||
private const val CURSOR_CONV_SUFFIX = "_CursorConverter"
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
|
||||||
@JvmStatic
|
|
||||||
fun <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
@ -0,0 +1,277 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.room.migration.Migration;
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An open helper that holds a reference to the configuration until the database is opened.
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||||
|
public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
|
||||||
|
@Nullable
|
||||||
|
private DatabaseConfiguration mConfiguration;
|
||||||
|
@NonNull
|
||||||
|
private final Delegate mDelegate;
|
||||||
|
@NonNull
|
||||||
|
private final String mIdentityHash;
|
||||||
|
/**
|
||||||
|
* Room v1 had a bug where the hash was not consistent if fields are reordered.
|
||||||
|
* The new has fixes it but we still need to accept the legacy hash.
|
||||||
|
*/
|
||||||
|
@NonNull // b/64290754
|
||||||
|
private final String mLegacyHash;
|
||||||
|
|
||||||
|
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
|
||||||
|
@NonNull String identityHash, @NonNull String legacyHash) {
|
||||||
|
super(delegate.version);
|
||||||
|
mConfiguration = configuration;
|
||||||
|
mDelegate = delegate;
|
||||||
|
mIdentityHash = identityHash;
|
||||||
|
mLegacyHash = legacyHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
|
||||||
|
@NonNull String legacyHash) {
|
||||||
|
this(configuration, delegate, "", legacyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConfigure(SupportSQLiteDatabase db) {
|
||||||
|
super.onConfigure(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SupportSQLiteDatabase db) {
|
||||||
|
boolean isEmptyDatabase = hasEmptySchema(db);
|
||||||
|
mDelegate.createAllTables(db);
|
||||||
|
if (!isEmptyDatabase) {
|
||||||
|
// A 0 version pre-populated database goes through the create path because the
|
||||||
|
// framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
|
||||||
|
// find the database not to be empty, then it is a pre-populated, we must validate it to
|
||||||
|
// see if its suitable for usage.
|
||||||
|
ValidationResult result = mDelegate.onValidateSchema(db);
|
||||||
|
if (!result.isValid) {
|
||||||
|
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
|
||||||
|
+ result.expectedFoundMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateIdentity(db);
|
||||||
|
mDelegate.onCreate(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
boolean migrated = false;
|
||||||
|
if (mConfiguration != null) {
|
||||||
|
List<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,244 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.room.util.useCursor
|
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An open helper that holds a reference to the configuration until the database is opened.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
open class RoomOpenHelper(
|
|
||||||
configuration: DatabaseConfiguration,
|
|
||||||
delegate: Delegate,
|
|
||||||
identityHash: String,
|
|
||||||
legacyHash: String
|
|
||||||
) : SupportSQLiteOpenHelper.Callback(delegate.version) {
|
|
||||||
private var configuration: DatabaseConfiguration?
|
|
||||||
private val delegate: Delegate
|
|
||||||
private val identityHash: String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Room v1 had a bug where the hash was not consistent if fields are reordered.
|
|
||||||
* The new has fixes it but we still need to accept the legacy hash.
|
|
||||||
*/
|
|
||||||
// b/64290754
|
|
||||||
private val legacyHash: String
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.configuration = configuration
|
|
||||||
this.delegate = delegate
|
|
||||||
this.identityHash = identityHash
|
|
||||||
this.legacyHash = legacyHash
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
configuration: DatabaseConfiguration,
|
|
||||||
delegate: Delegate,
|
|
||||||
legacyHash: String
|
|
||||||
) : this(configuration, delegate, "", legacyHash)
|
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
|
||||||
super.onConfigure(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
|
||||||
val isEmptyDatabase = hasEmptySchema(db)
|
|
||||||
delegate.createAllTables(db)
|
|
||||||
if (!isEmptyDatabase) {
|
|
||||||
// A 0 version pre-populated database goes through the create path because the
|
|
||||||
// framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
|
|
||||||
// find the database not to be empty, then it is a pre-populated, we must validate it to
|
|
||||||
// see if its suitable for usage.
|
|
||||||
val result = delegate.onValidateSchema(db)
|
|
||||||
if (!result.isValid) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateIdentity(db)
|
|
||||||
delegate.onCreate(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
|
||||||
var migrated = false
|
|
||||||
configuration?.let { config ->
|
|
||||||
val migrations = config.migrationContainer.findMigrationPath(
|
|
||||||
oldVersion, newVersion
|
|
||||||
)
|
|
||||||
if (migrations != null) {
|
|
||||||
delegate.onPreMigrate(db)
|
|
||||||
migrations.forEach { it.migrate(db) }
|
|
||||||
val result = delegate.onValidateSchema(db)
|
|
||||||
if (!result.isValid) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
("Migration didn't properly handle: " +
|
|
||||||
result.expectedFoundMsg)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
delegate.onPostMigrate(db)
|
|
||||||
updateIdentity(db)
|
|
||||||
migrated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!migrated) {
|
|
||||||
val config = this.configuration
|
|
||||||
if (config != null && !config.isMigrationRequired(oldVersion, newVersion)) {
|
|
||||||
delegate.dropAllTables(db)
|
|
||||||
delegate.createAllTables(db)
|
|
||||||
} else {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"A migration from $oldVersion to $newVersion was required but not found. " +
|
|
||||||
"Please provide the " +
|
|
||||||
"necessary Migration path via " +
|
|
||||||
"RoomDatabase.Builder.addMigration(Migration ...) or allow for " +
|
|
||||||
"destructive migrations via one of the " +
|
|
||||||
"RoomDatabase.Builder.fallbackToDestructiveMigration* methods."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
|
||||||
onUpgrade(db, oldVersion, newVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
|
||||||
super.onOpen(db)
|
|
||||||
checkIdentity(db)
|
|
||||||
delegate.onOpen(db)
|
|
||||||
// there might be too many configurations etc, just clear it.
|
|
||||||
configuration = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkIdentity(db: SupportSQLiteDatabase) {
|
|
||||||
if (hasRoomMasterTable(db)) {
|
|
||||||
val identityHash: String? = db.query(
|
|
||||||
SimpleSQLiteQuery(RoomMasterTable.READ_QUERY)
|
|
||||||
).useCursor { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
cursor.getString(0)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.identityHash != identityHash && this.legacyHash != identityHash) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"Room cannot verify the data integrity. Looks like" +
|
|
||||||
" you've changed schema but forgot to update the version number. You can" +
|
|
||||||
" simply fix this by increasing the version number. Expected identity" +
|
|
||||||
" hash: ${ this.identityHash }, found: $identityHash"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No room_master_table, this might an a pre-populated DB, we must validate to see if
|
|
||||||
// its suitable for usage.
|
|
||||||
val result = delegate.onValidateSchema(db)
|
|
||||||
if (!result.isValid) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
delegate.onPostMigrate(db)
|
|
||||||
updateIdentity(db)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateIdentity(db: SupportSQLiteDatabase) {
|
|
||||||
createMasterTableIfNotExists(db)
|
|
||||||
db.execSQL(RoomMasterTable.createInsertQuery(identityHash))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMasterTableIfNotExists(db: SupportSQLiteDatabase) {
|
|
||||||
db.execSQL(RoomMasterTable.CREATE_QUERY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
abstract class Delegate(@JvmField val version: Int) {
|
|
||||||
abstract fun dropAllTables(db: SupportSQLiteDatabase)
|
|
||||||
abstract fun createAllTables(db: SupportSQLiteDatabase)
|
|
||||||
abstract fun onOpen(db: SupportSQLiteDatabase)
|
|
||||||
abstract fun onCreate(db: SupportSQLiteDatabase)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after a migration run to validate database integrity.
|
|
||||||
*
|
|
||||||
* @param db The SQLite database.
|
|
||||||
*/
|
|
||||||
@Deprecated("Use [onValidateSchema(SupportSQLiteDatabase)]")
|
|
||||||
protected open fun validateMigration(db: SupportSQLiteDatabase) {
|
|
||||||
throw UnsupportedOperationException("validateMigration is deprecated")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after a migration run or pre-package database copy to validate database integrity.
|
|
||||||
*
|
|
||||||
* @param db The SQLite database.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
open fun onValidateSchema(db: SupportSQLiteDatabase): ValidationResult {
|
|
||||||
validateMigration(db)
|
|
||||||
return ValidationResult(true, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called before migrations execute to perform preliminary work.
|
|
||||||
* @param database The SQLite database.
|
|
||||||
*/
|
|
||||||
open fun onPreMigrate(db: SupportSQLiteDatabase) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after migrations execute to perform additional work.
|
|
||||||
* @param database The SQLite database.
|
|
||||||
*/
|
|
||||||
open fun onPostMigrate(db: SupportSQLiteDatabase) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
open class ValidationResult(
|
|
||||||
@JvmField val isValid: Boolean,
|
|
||||||
@JvmField val expectedFoundMsg: String?
|
|
||||||
)
|
|
||||||
companion object {
|
|
||||||
internal fun hasRoomMasterTable(db: SupportSQLiteDatabase): Boolean {
|
|
||||||
db.query(
|
|
||||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND " +
|
|
||||||
"name='${ RoomMasterTable.TABLE_NAME }'"
|
|
||||||
).useCursor { cursor ->
|
|
||||||
return cursor.moveToFirst() && cursor.getInt(0) != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun hasEmptySchema(db: SupportSQLiteDatabase): Boolean {
|
|
||||||
db.query(
|
|
||||||
"SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'"
|
|
||||||
).useCursor { cursor ->
|
|
||||||
return cursor.moveToFirst() && cursor.getInt(0) == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,299 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteProgram;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used as an intermediate place to keep binding arguments so that we can run
|
||||||
|
* Cursor queries with correct types rather than passing everything as a string.
|
||||||
|
* <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 {
|
||||||
|
}
|
||||||
|
}
|
@ -1,233 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.annotation.IntDef
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.sqlite.db.SupportSQLiteProgram
|
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
|
||||||
import java.util.Arrays
|
|
||||||
import java.util.TreeMap
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used as an intermediate place to keep binding arguments so that we can run
|
|
||||||
* Cursor queries with correct types rather than passing everything as a string.
|
|
||||||
*
|
|
||||||
* Because it is relatively a big object, they are pooled and must be released after each use.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
class RoomSQLiteQuery private constructor(
|
|
||||||
@field:VisibleForTesting val capacity: Int
|
|
||||||
) : SupportSQLiteQuery, SupportSQLiteProgram {
|
|
||||||
@Volatile
|
|
||||||
private var query: String? = null
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
@VisibleForTesting
|
|
||||||
val longBindings: LongArray
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
@VisibleForTesting
|
|
||||||
val doubleBindings: DoubleArray
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
@VisibleForTesting
|
|
||||||
val stringBindings: Array<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
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
|
import androidx.annotation.MainThread;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.WorkerThread;
|
||||||
|
import androidx.arch.core.executor.ArchTaskExecutor;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A LiveData implementation that closely works with {@link InvalidationTracker} to implement
|
||||||
|
* database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long
|
||||||
|
* as they are active.
|
||||||
|
* <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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,126 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.arch.core.executor.ArchTaskExecutor
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import java.lang.Exception
|
|
||||||
import java.lang.RuntimeException
|
|
||||||
import java.util.concurrent.Callable
|
|
||||||
import java.util.concurrent.Executor
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A LiveData implementation that closely works with [InvalidationTracker] to implement
|
|
||||||
* database drive [androidx.lifecycle.LiveData] queries that are strongly hold as long
|
|
||||||
* as they are active.
|
|
||||||
*
|
|
||||||
* We need this extra handling for [androidx.lifecycle.LiveData] because when they are
|
|
||||||
* observed forever, there is no [androidx.lifecycle.Lifecycle] that will keep them in
|
|
||||||
* memory but they should stay. We cannot add-remove observer in [LiveData.onActive],
|
|
||||||
* [LiveData.onInactive] because that would mean missing changes in between or doing an
|
|
||||||
* extra query on every UI rotation.
|
|
||||||
*
|
|
||||||
* This [LiveData] keeps a weak observer to the [InvalidationTracker] but it is hold
|
|
||||||
* strongly by the [InvalidationTracker] as long as it is active.
|
|
||||||
*/
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
internal class RoomTrackingLiveData<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
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,289 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.room.util.CopyLock;
|
||||||
|
import androidx.room.util.DBUtil;
|
||||||
|
import androidx.room.util.FileUtil;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
|
||||||
|
* storage.
|
||||||
|
*/
|
||||||
|
class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final Context mContext;
|
||||||
|
@Nullable
|
||||||
|
private final String mCopyFromAssetPath;
|
||||||
|
@Nullable
|
||||||
|
private final File mCopyFromFile;
|
||||||
|
@Nullable
|
||||||
|
private final Callable<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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,242 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.room.Room.LOG_TAG
|
|
||||||
import androidx.room.util.copy
|
|
||||||
import androidx.room.util.readVersion
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
|
||||||
import androidx.sqlite.util.ProcessLock
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.lang.Exception
|
|
||||||
import java.lang.IllegalStateException
|
|
||||||
import java.lang.RuntimeException
|
|
||||||
import java.nio.channels.Channels
|
|
||||||
import java.nio.channels.ReadableByteChannel
|
|
||||||
import java.util.concurrent.Callable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
|
|
||||||
* storage.
|
|
||||||
*/
|
|
||||||
@Suppress("BanSynchronizedMethods")
|
|
||||||
internal class SQLiteCopyOpenHelper(
|
|
||||||
private val context: Context,
|
|
||||||
private val copyFromAssetPath: String?,
|
|
||||||
private val copyFromFile: File?,
|
|
||||||
private val copyFromInputStream: Callable<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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
|
||||||
|
* {@link SQLiteCopyOpenHelper}.
|
||||||
|
*/
|
||||||
|
class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final String mCopyFromAssetPath;
|
||||||
|
@Nullable
|
||||||
|
private final File mCopyFromFile;
|
||||||
|
@Nullable
|
||||||
|
private final Callable<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));
|
||||||
|
}
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.concurrent.Callable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of [SupportSQLiteOpenHelper.Factory] that creates
|
|
||||||
* [SQLiteCopyOpenHelper].
|
|
||||||
*/
|
|
||||||
internal class SQLiteCopyOpenHelperFactory(
|
|
||||||
private val mCopyFromAssetPath: String?,
|
|
||||||
private val mCopyFromFile: File?,
|
|
||||||
private val mCopyFromInputStream: Callable<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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package androidx.room;
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a prepared SQLite state that can be re-used multiple times.
|
||||||
|
* <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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.sqlite.db.SupportSQLiteStatement
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a prepared SQLite state that can be re-used multiple times.
|
|
||||||
*
|
|
||||||
* This class is used by generated code. After it is used, `release` must be called so that
|
|
||||||
* it can be used by other threads.
|
|
||||||
*
|
|
||||||
* To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
|
|
||||||
* statement until it is released.
|
|
||||||
*
|
|
||||||
* @constructor Creates an SQLite prepared statement that can be re-used across threads. If it is
|
|
||||||
* in use, it automatically creates a new one.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
abstract class SharedSQLiteStatement(private val database: RoomDatabase) {
|
|
||||||
private val lock = AtomicBoolean(false)
|
|
||||||
|
|
||||||
private val stmt: SupportSQLiteStatement by lazy {
|
|
||||||
createNewStatement()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the query.
|
|
||||||
*
|
|
||||||
* @return The SQL query to prepare.
|
|
||||||
*/
|
|
||||||
protected abstract fun createQuery(): String
|
|
||||||
|
|
||||||
protected open fun assertNotMainThread() {
|
|
||||||
database.assertNotMainThread()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createNewStatement(): SupportSQLiteStatement {
|
|
||||||
val query = createQuery()
|
|
||||||
return database.compileStatement(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getStmt(canUseCached: Boolean): SupportSQLiteStatement {
|
|
||||||
val stmt = if (canUseCached) {
|
|
||||||
stmt
|
|
||||||
} else {
|
|
||||||
// it is in use, create a one off statement
|
|
||||||
createNewStatement()
|
|
||||||
}
|
|
||||||
return stmt
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this to get the statement. Must call [.release] once done.
|
|
||||||
*/
|
|
||||||
open fun acquire(): SupportSQLiteStatement {
|
|
||||||
assertNotMainThread()
|
|
||||||
return getStmt(lock.compareAndSet(false, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Must call this when statement will not be used anymore.
|
|
||||||
*
|
|
||||||
* @param statement The statement that was returned from acquire.
|
|
||||||
*/
|
|
||||||
open fun release(statement: SupportSQLiteStatement) {
|
|
||||||
if (statement === stmt) {
|
|
||||||
lock.set(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java 8 Sneaky Throw technique.
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
||||||
|
public class SneakyThrow {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-throws a checked exception as if it was a runtime exception without wrapping it.
|
||||||
|
*
|
||||||
|
* @param e the exception to re-throw.
|
||||||
|
*/
|
||||||
|
public static void reThrow(@NonNull Exception e) {
|
||||||
|
sneakyThrow(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <E extends Throwable> void sneakyThrow(@NonNull Throwable e) throws E {
|
||||||
|
throw (E) e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SneakyThrow() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @hide
|
||||||
|
*
|
||||||
|
* String utilities for Room
|
||||||
|
*/
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||||
|
public class StringUtil {
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||||
|
/**
|
||||||
|
* Returns a new StringBuilder to be used while producing SQL queries.
|
||||||
|
*
|
||||||
|
* @return A new or recycled StringBuilder
|
||||||
|
*/
|
||||||
|
public static StringBuilder newStringBuilder() {
|
||||||
|
// TODO pool:
|
||||||
|
return new StringBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds bind variable placeholders (?) to the given string. Each placeholder is separated
|
||||||
|
* by a comma.
|
||||||
|
*
|
||||||
|
* @param builder The StringBuilder for the query
|
||||||
|
* @param count Number of placeholders
|
||||||
|
*/
|
||||||
|
public static void appendPlaceholders(StringBuilder builder, int count) {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
builder.append("?");
|
||||||
|
if (i < count - 1) {
|
||||||
|
builder.append(",");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Splits a comma separated list of integers to integer list.
|
||||||
|
* <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() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,743 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.room.ColumnInfo;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class that holds the information about a table.
|
||||||
|
* <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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UUID / byte[] two-way conversion utility for Room
|
||||||
|
*
|
||||||
|
* @hide
|
||||||
|
*/
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||||
|
public final class UUIDUtil {
|
||||||
|
|
||||||
|
// private constructor to prevent instantiation
|
||||||
|
private UUIDUtil() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a 16-bytes array BLOB into a UUID pojo
|
||||||
|
*
|
||||||
|
* @param bytes byte array stored in database as BLOB
|
||||||
|
* @return a UUID object created based on the provided byte array
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static UUID convertByteToUUID(@NonNull byte[] bytes) {
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||||
|
long firstLong = buffer.getLong();
|
||||||
|
long secondLong = buffer.getLong();
|
||||||
|
return new UUID(firstLong, secondLong);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a UUID pojo into a 16-bytes array to store into database as BLOB
|
||||||
|
*
|
||||||
|
* @param uuid the UUID pojo
|
||||||
|
* @return a byte array to store into database
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static byte[] convertUUIDToByte(@NonNull UUID uuid) {
|
||||||
|
byte[] bytes = new byte[16];
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||||
|
buffer.putLong(uuid.getMostSignificantBits());
|
||||||
|
buffer.putLong(uuid.getLeastSignificantBits());
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.room.util;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class that holds the information about a view.
|
||||||
|
* <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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,183 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
@file:JvmName("CursorUtil")
|
|
||||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.CursorWrapper
|
|
||||||
import android.database.MatrixCursor
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies the given cursor into a in-memory cursor and then closes it.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This is useful for iterating over a cursor multiple times without the cost of JNI while
|
|
||||||
* reading or IO while filling the window at the expense of memory consumption.
|
|
||||||
*
|
|
||||||
* @param c the cursor to copy.
|
|
||||||
* @return a new cursor containing the same data as the given cursor.
|
|
||||||
*/
|
|
||||||
fun copyAndClose(c: Cursor): Cursor = c.useCursor { cursor ->
|
|
||||||
val matrixCursor = MatrixCursor(cursor.columnNames, cursor.count)
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
val row = arrayOfNulls<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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,213 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
@file:JvmName("DBUtil")
|
|
||||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import android.database.AbstractWindowedCursor
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.database.sqlite.SQLiteConstraintException
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.CancellationSignal
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteCompat
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the SQLiteQuery on the given database.
|
|
||||||
*
|
|
||||||
* This util method encapsulates copying the cursor if the `maybeCopy` parameter is
|
|
||||||
* `true` and either the api level is below a certain threshold or the full result of the
|
|
||||||
* query does not fit in a single window.
|
|
||||||
*
|
|
||||||
* @param db The database to perform the query on.
|
|
||||||
* @param sqLiteQuery The query to perform.
|
|
||||||
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
|
|
||||||
* @return Result of the query.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Deprecated(
|
|
||||||
"This is only used in the generated code and shouldn't be called directly."
|
|
||||||
)
|
|
||||||
fun query(db: RoomDatabase, sqLiteQuery: SupportSQLiteQuery, maybeCopy: Boolean): Cursor {
|
|
||||||
return query(db, sqLiteQuery, maybeCopy, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the SQLiteQuery on the given database.
|
|
||||||
*
|
|
||||||
* This util method encapsulates copying the cursor if the `maybeCopy` parameter is
|
|
||||||
* `true` and either the api level is below a certain threshold or the full result of the
|
|
||||||
* query does not fit in a single window.
|
|
||||||
*
|
|
||||||
* @param db The database to perform the query on.
|
|
||||||
* @param sqLiteQuery The query to perform.
|
|
||||||
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
|
|
||||||
* @param signal The cancellation signal to be attached to the query.
|
|
||||||
* @return Result of the query.
|
|
||||||
*/
|
|
||||||
fun query(
|
|
||||||
db: RoomDatabase,
|
|
||||||
sqLiteQuery: SupportSQLiteQuery,
|
|
||||||
maybeCopy: Boolean,
|
|
||||||
signal: CancellationSignal?
|
|
||||||
): Cursor {
|
|
||||||
val cursor = db.query(sqLiteQuery, signal)
|
|
||||||
if (maybeCopy && cursor is AbstractWindowedCursor) {
|
|
||||||
val rowsInCursor = cursor.count // Should fill the window.
|
|
||||||
val rowsInWindow = if (cursor.hasWindow()) {
|
|
||||||
cursor.window.numRows
|
|
||||||
} else {
|
|
||||||
rowsInCursor
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
|
|
||||||
return copyAndClose(cursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drops all FTS content sync triggers created by Room.
|
|
||||||
*
|
|
||||||
* FTS content sync triggers created by Room are those that are found in the sqlite_master table
|
|
||||||
* who's names start with 'room_fts_content_sync_'.
|
|
||||||
*
|
|
||||||
* @param db The database.
|
|
||||||
*/
|
|
||||||
fun dropFtsSyncTriggers(db: SupportSQLiteDatabase) {
|
|
||||||
val existingTriggers = buildList {
|
|
||||||
db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'").useCursor { cursor ->
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
add(cursor.getString(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
existingTriggers.forEach { triggerName ->
|
|
||||||
if (triggerName.startsWith("room_fts_content_sync_")) {
|
|
||||||
db.execSQL("DROP TRIGGER IF EXISTS $triggerName")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for foreign key violations by executing a PRAGMA foreign_key_check.
|
|
||||||
*/
|
|
||||||
fun foreignKeyCheck(
|
|
||||||
db: SupportSQLiteDatabase,
|
|
||||||
tableName: String
|
|
||||||
) {
|
|
||||||
db.query("PRAGMA foreign_key_check(`$tableName`)").useCursor { cursor ->
|
|
||||||
if (cursor.count > 0) {
|
|
||||||
val errorMsg = processForeignKeyCheckFailure(cursor)
|
|
||||||
throw SQLiteConstraintException(errorMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the user version number out of the database header from the given file.
|
|
||||||
*
|
|
||||||
* @param databaseFile the database file.
|
|
||||||
* @return the database version
|
|
||||||
* @throws IOException if something goes wrong reading the file, such as bad database header or
|
|
||||||
* missing permissions.
|
|
||||||
*
|
|
||||||
* @see [User Version
|
|
||||||
* Number](https://www.sqlite.org/fileformat.html.user_version_number).
|
|
||||||
*/
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun readVersion(databaseFile: File): Int {
|
|
||||||
FileInputStream(databaseFile).channel.use { input ->
|
|
||||||
val buffer = ByteBuffer.allocate(4)
|
|
||||||
input.tryLock(60, 4, true)
|
|
||||||
input.position(60)
|
|
||||||
val read = input.read(buffer)
|
|
||||||
if (read != 4) {
|
|
||||||
throw IOException("Bad database header, unable to read 4 bytes at offset 60")
|
|
||||||
}
|
|
||||||
buffer.rewind()
|
|
||||||
return buffer.int // ByteBuffer is big-endian by default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CancellationSignal is only available from API 16 on. This function will create a new
|
|
||||||
* instance of the Cancellation signal only if the current API > 16.
|
|
||||||
*
|
|
||||||
* @return A new instance of CancellationSignal or null.
|
|
||||||
*/
|
|
||||||
fun createCancellationSignal(): CancellationSignal? {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
|
||||||
SupportSQLiteCompat.Api16Impl.createCancellationSignal()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the [Cursor] returned in case of a foreign key violation into a detailed
|
|
||||||
* error message for debugging.
|
|
||||||
*
|
|
||||||
* The foreign_key_check pragma returns one row output for each foreign key violation.
|
|
||||||
*
|
|
||||||
* The cursor received has four columns for each row output. The first column is the name of
|
|
||||||
* the child table. The second column is the rowId of the row that contains the foreign key
|
|
||||||
* violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the
|
|
||||||
* name of the parent table. The fourth column is the index of the specific foreign key
|
|
||||||
* constraint that failed.
|
|
||||||
*
|
|
||||||
* @param cursor Cursor containing information regarding the FK violation
|
|
||||||
* @return Error message generated containing debugging information
|
|
||||||
*/
|
|
||||||
private fun processForeignKeyCheckFailure(cursor: Cursor): String {
|
|
||||||
return buildString {
|
|
||||||
val rowCount = cursor.count
|
|
||||||
val fkParentTables = mutableMapOf<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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
@file:JvmName("FileUtil")
|
|
||||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.channels.Channels
|
|
||||||
import java.nio.channels.FileChannel
|
|
||||||
import java.nio.channels.ReadableByteChannel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies data from the input channel to the output file channel.
|
|
||||||
*
|
|
||||||
* @param input the input channel to copy.
|
|
||||||
* @param output the output channel to copy.
|
|
||||||
* @throws IOException if there is an I/O error.
|
|
||||||
*/
|
|
||||||
@SuppressLint("LambdaLast")
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun copy(input: ReadableByteChannel, output: FileChannel) {
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
|
||||||
output.transferFrom(input, 0, Long.MAX_VALUE)
|
|
||||||
} else {
|
|
||||||
val inputStream = Channels.newInputStream(input)
|
|
||||||
val outputStream = Channels.newOutputStream(output)
|
|
||||||
var length: Int
|
|
||||||
val buffer = ByteArray(1024 * 4)
|
|
||||||
|
|
||||||
// TODO: Use Kotlin stdlib IO APIs
|
|
||||||
while (inputStream.read(buffer).also { length = it } > 0) {
|
|
||||||
outputStream.write(buffer, 0, length)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.force(false)
|
|
||||||
} finally {
|
|
||||||
input.close()
|
|
||||||
output.close()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,181 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import java.util.ArrayDeque
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that holds the information about an FTS table.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
class FtsTableInfo(
|
|
||||||
/**
|
|
||||||
* The table name
|
|
||||||
*/
|
|
||||||
@JvmField
|
|
||||||
val name: String,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The column names
|
|
||||||
*/
|
|
||||||
@JvmField
|
|
||||||
val columns: Set<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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:JvmName("RelationUtil")
|
|
||||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.collection.ArrayMap
|
|
||||||
import androidx.collection.LongSparseArray
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function used in generated code to recursively fetch relationships when the amount of
|
|
||||||
* keys exceed [RoomDatabase.MAX_BIND_PARAMETER_CNT].
|
|
||||||
*
|
|
||||||
* @param map - The map containing the relationship keys to fill-in.
|
|
||||||
* @param isRelationCollection - True if [V] is a [Collection] which means it is non null.
|
|
||||||
* @param fetchBlock - A lambda for calling the generated _fetchRelationship function.
|
|
||||||
*/
|
|
||||||
fun <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>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
@file:JvmName("StringUtil")
|
|
||||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import java.lang.NumberFormatException
|
|
||||||
import java.lang.StringBuilder
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
@JvmField
|
|
||||||
val EMPTY_STRING_ARRAY = arrayOfNulls<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(",")
|
|
||||||
}
|
|
@ -1,645 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.IntDef
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.ColumnInfo.SQLiteTypeAffinity
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TreeMap
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that holds the information about a table.
|
|
||||||
*
|
|
||||||
* It directly maps to the result of `PRAGMA table_info(<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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2021 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
@file:JvmName("UUIDUtil")
|
|
||||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UUID / byte[] two-way conversion utility for Room
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a 16-bytes array BLOB into a UUID pojo
|
|
||||||
*
|
|
||||||
* @param bytes byte array stored in database as BLOB
|
|
||||||
* @return a UUID object created based on the provided byte array
|
|
||||||
*/
|
|
||||||
fun convertByteToUUID(bytes: ByteArray): UUID {
|
|
||||||
val buffer = ByteBuffer.wrap(bytes)
|
|
||||||
val firstLong = buffer.long
|
|
||||||
val secondLong = buffer.long
|
|
||||||
return UUID(firstLong, secondLong)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a UUID pojo into a 16-bytes array to store into database as BLOB
|
|
||||||
*
|
|
||||||
* @param uuid the UUID pojo
|
|
||||||
* @return a byte array to store into database
|
|
||||||
*/
|
|
||||||
fun convertUUIDToByte(uuid: UUID): ByteArray {
|
|
||||||
val bytes = ByteArray(16)
|
|
||||||
val buffer = ByteBuffer.wrap(bytes)
|
|
||||||
buffer.putLong(uuid.mostSignificantBits)
|
|
||||||
buffer.putLong(uuid.leastSignificantBits)
|
|
||||||
return buffer.array()
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package androidx.room.util
|
|
||||||
|
|
||||||
import androidx.annotation.RestrictTo
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A data class that holds the information about a view.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This derives information from sqlite_master.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
|
||||||
class ViewInfo(
|
|
||||||
/**
|
|
||||||
* The view name
|
|
||||||
*/
|
|
||||||
@JvmField
|
|
||||||
val name: String,
|
|
||||||
/**
|
|
||||||
* The SQL of CREATE VIEW.
|
|
||||||
*/
|
|
||||||
@JvmField
|
|
||||||
val sql: String?
|
|
||||||
) {
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (other !is ViewInfo) return false
|
|
||||||
return ((name == other.name) && if (sql != null) sql == other.sql else other.sql == null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = name.hashCode()
|
|
||||||
result = 31 * result + (sql?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return ("ViewInfo{" + "name='" + name + '\'' + ", sql='" + sql + '\'' + '}')
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Reads the view information from the given database.
|
|
||||||
*
|
|
||||||
* @param database The database to read the information from.
|
|
||||||
* @param viewName The view name.
|
|
||||||
* @return A ViewInfo containing the schema information for the provided view name.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun read(database: SupportSQLiteDatabase, viewName: String): ViewInfo {
|
|
||||||
return database.query(
|
|
||||||
"SELECT name, sql FROM sqlite_master " +
|
|
||||||
"WHERE type = 'view' AND name = '$viewName'"
|
|
||||||
).useCursor { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
ViewInfo(cursor.getString(0), cursor.getString(1))
|
|
||||||
} else {
|
|
||||||
ViewInfo(viewName, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in new issue