mirror of https://github.com/M66B/FairEmail.git
parent
03a331175f
commit
07f6fa6095
|
@ -516,7 +516,7 @@ configurations.configureEach {
|
|||
resolutionStrategy.eachDependency { details ->
|
||||
if (details.requested.group == "androidx.room") {
|
||||
//print("Pinning " + details.requested.group + ":" + details.requested.name + "\n")
|
||||
details.useVersion "2.4.3"
|
||||
details.useVersion "2.6.1"
|
||||
} else if (details.requested.group == "androidx.lifecycle" &&
|
||||
details.requested.name != "lifecycle-extensions") {
|
||||
//print("Pinning " + details.requested.group + ":" + details.requested.name + "\n")
|
||||
|
@ -558,7 +558,7 @@ dependencies {
|
|||
def documentfile_version = "1.1.0-alpha01"
|
||||
def lifecycle_version = "2.7.0" // 2.8.0-rc01
|
||||
def lifecycle_extensions_version = "2.2.0"
|
||||
def room_version = "2.4.3" // 2.5.2/2.6.1/2.7.0-alpha01
|
||||
def room_version = "2.6.1" // 2.7.0-alpha01
|
||||
def sqlite_version = "2.4.0" // 2.5.0-alpha01
|
||||
def requery_version = "3.39.2"
|
||||
def paging_version = "2.1.2" // 3.3.0-rc01
|
||||
|
|
|
@ -1,311 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.room.util.SneakyThrow;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* AutoCloser is responsible for automatically opening (using
|
||||
* delegateOpenHelper) and closing (on a timer started when there are no remaining references) a
|
||||
* SupportSqliteDatabase.
|
||||
*
|
||||
* It is important to ensure that the ref count is incremented when using a returned database.
|
||||
*/
|
||||
final class AutoCloser {
|
||||
|
||||
@Nullable
|
||||
private SupportSQLiteOpenHelper mDelegateOpenHelper = null;
|
||||
|
||||
@NonNull
|
||||
private final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// Package private for access from mAutoCloser
|
||||
@Nullable
|
||||
Runnable mOnAutoCloseCallback = null;
|
||||
|
||||
// Package private for access from mAutoCloser
|
||||
@NonNull
|
||||
final Object mLock = new Object();
|
||||
|
||||
// Package private for access from mAutoCloser
|
||||
final long mAutoCloseTimeoutInMs;
|
||||
|
||||
// Package private for access from mExecuteAutoCloser
|
||||
@NonNull
|
||||
final Executor mExecutor;
|
||||
|
||||
// Package private for access from mAutoCloser
|
||||
@GuardedBy("mLock")
|
||||
int mRefCount = 0;
|
||||
|
||||
// Package private for access from mAutoCloser
|
||||
@GuardedBy("mLock")
|
||||
long mLastDecrementRefCountTimeStamp = SystemClock.uptimeMillis();
|
||||
|
||||
// The unwrapped SupportSqliteDatabase
|
||||
// Package private for access from mAutoCloser
|
||||
@GuardedBy("mLock")
|
||||
@Nullable
|
||||
SupportSQLiteDatabase mDelegateDatabase;
|
||||
|
||||
private boolean mManuallyClosed = false;
|
||||
|
||||
private final Runnable mExecuteAutoCloser = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mExecutor.execute(mAutoCloser);
|
||||
}
|
||||
};
|
||||
|
||||
// Package private for access from mExecuteAutoCloser
|
||||
@NonNull
|
||||
final Runnable mAutoCloser = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mLock) {
|
||||
if (SystemClock.uptimeMillis() - mLastDecrementRefCountTimeStamp
|
||||
< mAutoCloseTimeoutInMs) {
|
||||
// An increment + decrement beat us to closing the db. We
|
||||
// will not close the database, and there should be at least
|
||||
// one more auto-close scheduled.
|
||||
return;
|
||||
}
|
||||
|
||||
if (mRefCount != 0) {
|
||||
// An increment beat us to closing the db. We don't close the
|
||||
// db, and another closer will be scheduled once the ref
|
||||
// count is decremented.
|
||||
return;
|
||||
}
|
||||
|
||||
if (mOnAutoCloseCallback != null) {
|
||||
mOnAutoCloseCallback.run();
|
||||
} else {
|
||||
throw new IllegalStateException("mOnAutoCloseCallback is null but it should"
|
||||
+ " have been set before use. Please file a bug "
|
||||
+ "against Room at: https://issuetracker.google"
|
||||
+ ".com/issues/new?component=413107&template=1096568");
|
||||
}
|
||||
|
||||
if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) {
|
||||
try {
|
||||
mDelegateDatabase.close();
|
||||
} catch (IOException e) {
|
||||
SneakyThrow.reThrow(e);
|
||||
}
|
||||
mDelegateDatabase = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Construct an AutoCloser.
|
||||
*
|
||||
* @param autoCloseTimeoutAmount time for auto close timer
|
||||
* @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount
|
||||
* @param autoCloseExecutor the executor on which the auto close operation will happen
|
||||
*/
|
||||
AutoCloser(long autoCloseTimeoutAmount,
|
||||
@NonNull TimeUnit autoCloseTimeUnit,
|
||||
@NonNull Executor autoCloseExecutor) {
|
||||
mAutoCloseTimeoutInMs = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount);
|
||||
mExecutor = autoCloseExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the
|
||||
* delegateOpenHelper after construction.
|
||||
*
|
||||
* @param delegateOpenHelper the open helper that is used to create
|
||||
* new SupportSqliteDatabases
|
||||
*/
|
||||
public void init(@NonNull SupportSQLiteOpenHelper delegateOpenHelper) {
|
||||
if (mDelegateOpenHelper != null) {
|
||||
Log.e(Room.LOG_TAG, "AutoCloser initialized multiple times. Please file a bug against"
|
||||
+ " room at: https://issuetracker.google"
|
||||
+ ".com/issues/new?component=413107&template=1096568");
|
||||
return;
|
||||
}
|
||||
this.mDelegateOpenHelper = delegateOpenHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a ref counting function. The function will receive an unwrapped open database and
|
||||
* this database will stay open until at least after function returns. If there are no more
|
||||
* references in use for the db once function completes, an auto close operation will be
|
||||
* scheduled.
|
||||
*/
|
||||
@Nullable
|
||||
public <V> V executeRefCountingFunction(@NonNull Function<SupportSQLiteDatabase, V> function) {
|
||||
try {
|
||||
SupportSQLiteDatabase db = incrementCountAndEnsureDbIsOpen();
|
||||
return function.apply(db);
|
||||
} finally {
|
||||
decrementCountAndScheduleClose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that autoCloser is no longer running and confirms that mDelegateDatabase is set
|
||||
* and open. mDelegateDatabase will not be auto closed until
|
||||
* decrementRefCountAndScheduleClose is called. decrementRefCountAndScheduleClose must be
|
||||
* called once for each call to incrementCountAndEnsureDbIsOpen.
|
||||
*
|
||||
* If this throws an exception, decrementCountAndScheduleClose must still be called!
|
||||
*
|
||||
* @return the *unwrapped* SupportSQLiteDatabase.
|
||||
*/
|
||||
@NonNull
|
||||
public SupportSQLiteDatabase incrementCountAndEnsureDbIsOpen() {
|
||||
//TODO(rohitsat): avoid synchronized(mLock) when possible. We should be able to avoid it
|
||||
// when refCount is not hitting zero or if there is no auto close scheduled if we use
|
||||
// Atomics.
|
||||
synchronized (mLock) {
|
||||
// If there is a scheduled autoclose operation, we should remove it from the handler.
|
||||
mHandler.removeCallbacks(mExecuteAutoCloser);
|
||||
|
||||
mRefCount++;
|
||||
|
||||
if (mManuallyClosed) {
|
||||
throw new IllegalStateException("Attempting to open already closed database.");
|
||||
}
|
||||
|
||||
if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) {
|
||||
return mDelegateDatabase;
|
||||
}
|
||||
|
||||
// Get the database while holding `mLock` so no other threads try to create it or
|
||||
// destroy it.
|
||||
if (mDelegateOpenHelper != null) {
|
||||
mDelegateDatabase = mDelegateOpenHelper.getWritableDatabase();
|
||||
} else {
|
||||
throw new IllegalStateException("AutoCloser has not been initialized. Please file "
|
||||
+ "a bug against Room at: "
|
||||
+ "https://issuetracker.google.com/issues/new?component=413107&template=1096568");
|
||||
}
|
||||
|
||||
return mDelegateDatabase;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the ref count and schedules a close if there are no other references to the db.
|
||||
* This must only be called after a corresponding incrementCountAndEnsureDbIsOpen call.
|
||||
*/
|
||||
public void decrementCountAndScheduleClose() {
|
||||
//TODO(rohitsat): avoid synchronized(mLock) when possible
|
||||
synchronized (mLock) {
|
||||
if (mRefCount <= 0) {
|
||||
throw new IllegalStateException("ref count is 0 or lower but we're supposed to "
|
||||
+ "decrement");
|
||||
}
|
||||
|
||||
// decrement refCount
|
||||
mRefCount--;
|
||||
|
||||
// if refcount is zero, schedule close operation
|
||||
if (mRefCount == 0) {
|
||||
if (mDelegateDatabase == null) {
|
||||
// No db to close, this can happen due to exceptions when creating db...
|
||||
return;
|
||||
}
|
||||
mHandler.postDelayed(mExecuteAutoCloser, mAutoCloseTimeoutInMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying database. This does not ensure that the database is open; the
|
||||
* caller is responsible for ensuring that the database is open and the ref count is non-zero.
|
||||
*
|
||||
* This is primarily meant for use cases where we don't want to open the database (isOpen) or
|
||||
* we know that the database is already open (KeepAliveCursor).
|
||||
*/
|
||||
@Nullable // Since the db might be closed
|
||||
public SupportSQLiteDatabase getDelegateDatabase() {
|
||||
synchronized (mLock) {
|
||||
return mDelegateDatabase;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database if it is still active.
|
||||
*
|
||||
* @throws IOException if an exception is encountered when closing the underlying db.
|
||||
*/
|
||||
public void closeDatabaseIfOpen() throws IOException {
|
||||
synchronized (mLock) {
|
||||
mManuallyClosed = true;
|
||||
|
||||
if (mDelegateDatabase != null) {
|
||||
mDelegateDatabase.close();
|
||||
}
|
||||
mDelegateDatabase = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The auto closer is still active if the database has not been closed. This means that
|
||||
* whether or not the underlying database is closed, when active we will re-open it on the
|
||||
* next access.
|
||||
*
|
||||
* @return a boolean indicating whether the auto closer is still active
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return !mManuallyClosed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current ref count for this auto closer. This is only visible for testing.
|
||||
*
|
||||
* @return current ref count
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public int getRefCountForTest() {
|
||||
synchronized (mLock) {
|
||||
return mRefCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback that will be run every time the database is auto-closed. This callback
|
||||
* needs to be lightweight since it is run while holding a lock.
|
||||
*
|
||||
* @param onAutoClose the callback to run
|
||||
*/
|
||||
public void setAutoCloseCallback(Runnable onAutoClose) {
|
||||
mOnAutoCloseCallback = onAutoClose;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import androidx.annotation.GuardedBy
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* AutoCloser is responsible for automatically opening (using
|
||||
* delegateOpenHelper) and closing (on a timer started when there are no remaining references) a
|
||||
* SupportSqliteDatabase.
|
||||
*
|
||||
* It is important to ensure that the ref count is incremented when using a returned database.
|
||||
*
|
||||
* @param autoCloseTimeoutAmount time for auto close timer
|
||||
* @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount
|
||||
* @param autoCloseExecutor the executor on which the auto close operation will happen
|
||||
*/
|
||||
internal class AutoCloser(
|
||||
autoCloseTimeoutAmount: Long,
|
||||
autoCloseTimeUnit: TimeUnit,
|
||||
autoCloseExecutor: Executor
|
||||
) {
|
||||
lateinit var delegateOpenHelper: SupportSQLiteOpenHelper
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
internal var onAutoCloseCallback: Runnable? = null
|
||||
|
||||
private val lock = Any()
|
||||
|
||||
private var autoCloseTimeoutInMs: Long = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount)
|
||||
|
||||
private val executor: Executor = autoCloseExecutor
|
||||
|
||||
@GuardedBy("lock")
|
||||
internal var refCount = 0
|
||||
|
||||
@GuardedBy("lock")
|
||||
internal var lastDecrementRefCountTimeStamp = SystemClock.uptimeMillis()
|
||||
|
||||
// The unwrapped SupportSqliteDatabase
|
||||
@GuardedBy("lock")
|
||||
internal var delegateDatabase: SupportSQLiteDatabase? = null
|
||||
|
||||
private var manuallyClosed = false
|
||||
|
||||
private val executeAutoCloser = Runnable { executor.execute(autoCloser) }
|
||||
|
||||
private val autoCloser = Runnable {
|
||||
synchronized(lock) {
|
||||
if (SystemClock.uptimeMillis() - lastDecrementRefCountTimeStamp
|
||||
< autoCloseTimeoutInMs
|
||||
) {
|
||||
// An increment + decrement beat us to closing the db. We
|
||||
// will not close the database, and there should be at least
|
||||
// one more auto-close scheduled.
|
||||
return@Runnable
|
||||
}
|
||||
if (refCount != 0) {
|
||||
// An increment beat us to closing the db. We don't close the
|
||||
// db, and another closer will be scheduled once the ref
|
||||
// count is decremented.
|
||||
return@Runnable
|
||||
}
|
||||
onAutoCloseCallback?.run() ?: error(
|
||||
"onAutoCloseCallback is null but it should" +
|
||||
" have been set before use. Please file a bug " +
|
||||
"against Room at: $autoCloseBug"
|
||||
)
|
||||
|
||||
delegateDatabase?.let {
|
||||
if (it.isOpen) {
|
||||
it.close()
|
||||
}
|
||||
}
|
||||
delegateDatabase = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the
|
||||
* delegateOpenHelper after construction.
|
||||
*
|
||||
* @param delegateOpenHelper the open helper that is used to create
|
||||
* new SupportSqliteDatabases
|
||||
*/
|
||||
fun init(delegateOpenHelper: SupportSQLiteOpenHelper) {
|
||||
this.delegateOpenHelper = delegateOpenHelper
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a ref counting function. The function will receive an unwrapped open database and
|
||||
* this database will stay open until at least after function returns. If there are no more
|
||||
* references in use for the db once function completes, an auto close operation will be
|
||||
* scheduled.
|
||||
*/
|
||||
fun <V> executeRefCountingFunction(block: (SupportSQLiteDatabase) -> V): V =
|
||||
try {
|
||||
block(incrementCountAndEnsureDbIsOpen())
|
||||
} finally {
|
||||
decrementCountAndScheduleClose()
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that autoCloser is no longer running and confirms that delegateDatabase is set
|
||||
* and open. delegateDatabase will not be auto closed until
|
||||
* decrementRefCountAndScheduleClose is called. decrementRefCountAndScheduleClose must be
|
||||
* called once for each call to incrementCountAndEnsureDbIsOpen.
|
||||
*
|
||||
* If this throws an exception, decrementCountAndScheduleClose must still be called!
|
||||
*
|
||||
* @return the *unwrapped* SupportSQLiteDatabase.
|
||||
*/
|
||||
fun incrementCountAndEnsureDbIsOpen(): SupportSQLiteDatabase {
|
||||
// TODO(rohitsat): avoid synchronized(lock) when possible. We should be able to avoid it
|
||||
// when refCount is not hitting zero or if there is no auto close scheduled if we use
|
||||
// Atomics.
|
||||
synchronized(lock) {
|
||||
|
||||
// If there is a scheduled autoclose operation, we should remove it from the handler.
|
||||
handler.removeCallbacks(executeAutoCloser)
|
||||
refCount++
|
||||
check(!manuallyClosed) { "Attempting to open already closed database." }
|
||||
delegateDatabase?.let {
|
||||
if (it.isOpen) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return delegateOpenHelper.writableDatabase.also { delegateDatabase = it }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the ref count and schedules a close if there are no other references to the db.
|
||||
* This must only be called after a corresponding incrementCountAndEnsureDbIsOpen call.
|
||||
*/
|
||||
fun decrementCountAndScheduleClose() {
|
||||
// TODO(rohitsat): avoid synchronized(lock) when possible
|
||||
synchronized(lock) {
|
||||
check(refCount > 0) {
|
||||
"ref count is 0 or lower but we're supposed to decrement"
|
||||
}
|
||||
// decrement refCount
|
||||
refCount--
|
||||
|
||||
// if refcount is zero, schedule close operation
|
||||
if (refCount == 0) {
|
||||
if (delegateDatabase == null) {
|
||||
// No db to close, this can happen due to exceptions when creating db...
|
||||
return
|
||||
}
|
||||
handler.postDelayed(executeAutoCloser, autoCloseTimeoutInMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database if it is still active.
|
||||
*
|
||||
* @throws IOException if an exception is encountered when closing the underlying db.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun closeDatabaseIfOpen() {
|
||||
synchronized(lock) {
|
||||
manuallyClosed = true
|
||||
delegateDatabase?.close()
|
||||
delegateDatabase = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The auto closer is still active if the database has not been closed. This means that
|
||||
* whether or not the underlying database is closed, when active we will re-open it on the
|
||||
* next access.
|
||||
*
|
||||
* @return a boolean indicating whether the auto closer is still active
|
||||
*/
|
||||
val isActive: Boolean
|
||||
get() = !manuallyClosed
|
||||
|
||||
/**
|
||||
* Returns the current ref count for this auto closer. This is only visible for testing.
|
||||
*
|
||||
* @return current ref count
|
||||
*/
|
||||
@get:VisibleForTesting
|
||||
internal val refCountForTest: Int
|
||||
get() {
|
||||
synchronized(lock) { return refCount }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback that will be run every time the database is auto-closed. This callback
|
||||
* needs to be lightweight since it is run while holding a lock.
|
||||
*
|
||||
* @param onAutoClose the callback to run
|
||||
*/
|
||||
fun setAutoCloseCallback(onAutoClose: Runnable) {
|
||||
onAutoCloseCallback = onAutoClose
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val autoCloseBug = "https://issuetracker.google.com/issues/new?component=" +
|
||||
"413107&template=1096568"
|
||||
}
|
||||
}
|
|
@ -1,878 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.database.CharArrayBuffer;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.database.SQLException;
|
||||
import android.database.sqlite.SQLiteTransactionListener;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.room.util.SneakyThrow;
|
||||
import androidx.sqlite.db.SupportSQLiteCompat;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A SupportSQLiteOpenHelper that has autoclose enabled for database connections.
|
||||
*/
|
||||
final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
|
||||
@NonNull
|
||||
private final SupportSQLiteOpenHelper mDelegateOpenHelper;
|
||||
|
||||
@NonNull
|
||||
private final AutoClosingSupportSQLiteDatabase mAutoClosingDb;
|
||||
|
||||
@NonNull
|
||||
private final AutoCloser mAutoCloser;
|
||||
|
||||
AutoClosingRoomOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
|
||||
@NonNull AutoCloser autoCloser) {
|
||||
mDelegateOpenHelper = supportSQLiteOpenHelper;
|
||||
mAutoCloser = autoCloser;
|
||||
autoCloser.init(mDelegateOpenHelper);
|
||||
mAutoClosingDb = new AutoClosingSupportSQLiteDatabase(mAutoCloser);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getDatabaseName() {
|
||||
return mDelegateOpenHelper.getDatabaseName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
public void setWriteAheadLoggingEnabled(boolean enabled) {
|
||||
mDelegateOpenHelper.setWriteAheadLoggingEnabled(enabled);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public SupportSQLiteDatabase getWritableDatabase() {
|
||||
// Note we don't differentiate between writable db and readable db
|
||||
// We try to open the db so the open callbacks run
|
||||
mAutoClosingDb.pokeOpen();
|
||||
return mAutoClosingDb;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public SupportSQLiteDatabase getReadableDatabase() {
|
||||
// Note we don't differentiate between writable db and readable db
|
||||
// We try to open the db so the open callbacks run
|
||||
mAutoClosingDb.pokeOpen();
|
||||
return mAutoClosingDb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
mAutoClosingDb.close();
|
||||
} catch (IOException e) {
|
||||
SneakyThrow.reThrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* package protected to pass it to invalidation tracker...
|
||||
*/
|
||||
@NonNull
|
||||
AutoCloser getAutoCloser() {
|
||||
return this.mAutoCloser;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
SupportSQLiteDatabase getAutoClosingDb() {
|
||||
return this.mAutoClosingDb;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public SupportSQLiteOpenHelper getDelegate() {
|
||||
return mDelegateOpenHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* SupportSQLiteDatabase that also keeps refcounts and autocloses the database
|
||||
*/
|
||||
static final class AutoClosingSupportSQLiteDatabase implements SupportSQLiteDatabase {
|
||||
@NonNull
|
||||
private final AutoCloser mAutoCloser;
|
||||
|
||||
AutoClosingSupportSQLiteDatabase(@NonNull AutoCloser autoCloser) {
|
||||
mAutoCloser = autoCloser;
|
||||
}
|
||||
|
||||
void pokeOpen() {
|
||||
mAutoCloser.executeRefCountingFunction(db -> null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SupportSQLiteStatement compileStatement(String sql) {
|
||||
return new AutoClosingSupportSqliteStatement(sql, mAutoCloser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginTransaction() {
|
||||
// We assume that after every successful beginTransaction() call there *must* be a
|
||||
// endTransaction() call.
|
||||
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
try {
|
||||
db.beginTransaction();
|
||||
} catch (Throwable t) {
|
||||
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||
// fails since there won't be a corresponding endTransaction call.
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginTransactionNonExclusive() {
|
||||
// We assume that after every successful beginTransaction() call there *must* be a
|
||||
// endTransaction() call.
|
||||
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
try {
|
||||
db.beginTransactionNonExclusive();
|
||||
} catch (Throwable t) {
|
||||
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||
// fails since there won't be a corresponding endTransaction call.
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
|
||||
// We assume that after every successful beginTransaction() call there *must* be a
|
||||
// endTransaction() call.
|
||||
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
try {
|
||||
db.beginTransactionWithListener(transactionListener);
|
||||
} catch (Throwable t) {
|
||||
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||
// fails since there won't be a corresponding endTransaction call.
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginTransactionWithListenerNonExclusive(
|
||||
SQLiteTransactionListener transactionListener) {
|
||||
// We assume that after every successful beginTransaction() call there *will* always
|
||||
// be a corresponding endTransaction() call. Without a corresponding
|
||||
// endTransactionCall we will never close the db.
|
||||
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
try {
|
||||
db.beginTransactionWithListenerNonExclusive(transactionListener);
|
||||
} catch (Throwable t) {
|
||||
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||
// fails since there won't be a corresponding endTransaction call.
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTransaction() {
|
||||
if (mAutoCloser.getDelegateDatabase() == null) {
|
||||
// This should never happen.
|
||||
throw new IllegalStateException("End transaction called but delegateDb is null");
|
||||
}
|
||||
|
||||
try {
|
||||
mAutoCloser.getDelegateDatabase().endTransaction();
|
||||
} finally {
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransactionSuccessful() {
|
||||
SupportSQLiteDatabase delegate = mAutoCloser.getDelegateDatabase();
|
||||
|
||||
if (delegate == null) {
|
||||
// This should never happen.
|
||||
throw new IllegalStateException("setTransactionSuccessful called but delegateDb "
|
||||
+ "is null");
|
||||
}
|
||||
|
||||
delegate.setTransactionSuccessful();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean inTransaction() {
|
||||
if (mAutoCloser.getDelegateDatabase() == null) {
|
||||
return false;
|
||||
}
|
||||
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDbLockedByCurrentThread() {
|
||||
if (mAutoCloser.getDelegateDatabase() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mAutoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::isDbLockedByCurrentThread);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean yieldIfContendedSafely() {
|
||||
return mAutoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::yieldIfContendedSafely);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
|
||||
return mAutoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::yieldIfContendedSafely);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVersion(int version) {
|
||||
mAutoCloser.executeRefCountingFunction(db -> {
|
||||
db.setVersion(version);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaximumSize() {
|
||||
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getMaximumSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long setMaximumSize(long numBytes) {
|
||||
return mAutoCloser.executeRefCountingFunction(db -> db.setMaximumSize(numBytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPageSize() {
|
||||
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPageSize(long numBytes) {
|
||||
mAutoCloser.executeRefCountingFunction(db -> {
|
||||
db.setPageSize(numBytes);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(String query) {
|
||||
Cursor result;
|
||||
try {
|
||||
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
result = db.query(query);
|
||||
} catch (Throwable throwable) {
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
throw throwable;
|
||||
}
|
||||
|
||||
return new KeepAliveCursor(result, mAutoCloser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(String query, Object[] bindArgs) {
|
||||
Cursor result;
|
||||
try {
|
||||
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
result = db.query(query, bindArgs);
|
||||
} catch (Throwable throwable) {
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
throw throwable;
|
||||
}
|
||||
|
||||
return new KeepAliveCursor(result, mAutoCloser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(SupportSQLiteQuery query) {
|
||||
|
||||
Cursor result;
|
||||
try {
|
||||
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
result = db.query(query);
|
||||
} catch (Throwable throwable) {
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
throw throwable;
|
||||
}
|
||||
|
||||
return new KeepAliveCursor(result, mAutoCloser);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) {
|
||||
Cursor result;
|
||||
try {
|
||||
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
result = db.query(query, cancellationSignal);
|
||||
} catch (Throwable throwable) {
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
throw throwable;
|
||||
}
|
||||
|
||||
return new KeepAliveCursor(result, mAutoCloser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long insert(String table, int conflictAlgorithm, ContentValues values)
|
||||
throws SQLException {
|
||||
return mAutoCloser.executeRefCountingFunction(db -> db.insert(table, conflictAlgorithm,
|
||||
values));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(String table, String whereClause, Object[] whereArgs) {
|
||||
return mAutoCloser.executeRefCountingFunction(
|
||||
db -> db.delete(table, whereClause, whereArgs));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(String table, int conflictAlgorithm, ContentValues values,
|
||||
String whereClause, Object[] whereArgs) {
|
||||
return mAutoCloser.executeRefCountingFunction(db -> db.update(table, conflictAlgorithm,
|
||||
values, whereClause, whereArgs));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execSQL(String sql) throws SQLException {
|
||||
mAutoCloser.executeRefCountingFunction(db -> {
|
||||
db.execSQL(sql);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execSQL(String sql, Object[] bindArgs) throws SQLException {
|
||||
mAutoCloser.executeRefCountingFunction(db -> {
|
||||
db.execSQL(sql, bindArgs);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::isReadOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
// Get the db without incrementing the reference cause we don't want to open
|
||||
// the db for an isOpen call.
|
||||
SupportSQLiteDatabase localDelegate = mAutoCloser.getDelegateDatabase();
|
||||
|
||||
if (localDelegate == null) {
|
||||
return false;
|
||||
}
|
||||
return localDelegate.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needUpgrade(int newVersion) {
|
||||
return mAutoCloser.executeRefCountingFunction(db -> db.needUpgrade(newVersion));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocale(Locale locale) {
|
||||
mAutoCloser.executeRefCountingFunction(db -> {
|
||||
db.setLocale(locale);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxSqlCacheSize(int cacheSize) {
|
||||
mAutoCloser.executeRefCountingFunction(db -> {
|
||||
db.setMaxSqlCacheSize(cacheSize);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
@Override
|
||||
public void setForeignKeyConstraintsEnabled(boolean enable) {
|
||||
mAutoCloser.executeRefCountingFunction(db -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
db.setForeignKeyConstraintsEnabled(enable);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableWriteAheadLogging() {
|
||||
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
|
||||
+ "OpenHelper instead of on the database directly.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableWriteAheadLogging() {
|
||||
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
|
||||
+ "OpenHelper instead of on the database directly.");
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
@Override
|
||||
public boolean isWriteAheadLoggingEnabled() {
|
||||
return mAutoCloser.executeRefCountingFunction(db -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
return db.isWriteAheadLoggingEnabled();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Pair<String, String>> getAttachedDbs() {
|
||||
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getAttachedDbs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDatabaseIntegrityOk() {
|
||||
return mAutoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::isDatabaseIntegrityOk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
mAutoCloser.closeDatabaseIfOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExecPerConnectionSQLSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to keep the db alive until the cursor is closed, so we can't decrement our
|
||||
* reference count until the cursor is closed. The underlying database will not close until
|
||||
* this cursor is closed.
|
||||
*/
|
||||
private static final class KeepAliveCursor implements Cursor {
|
||||
private final Cursor mDelegate;
|
||||
private final AutoCloser mAutoCloser;
|
||||
|
||||
KeepAliveCursor(Cursor delegate, AutoCloser autoCloser) {
|
||||
mDelegate = delegate;
|
||||
mAutoCloser = autoCloser;
|
||||
}
|
||||
|
||||
// close is the only important/changed method here:
|
||||
@Override
|
||||
public void close() {
|
||||
mDelegate.close();
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return mDelegate.isClosed();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mDelegate.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
return mDelegate.getPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean move(int offset) {
|
||||
return mDelegate.move(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToPosition(int position) {
|
||||
return mDelegate.moveToPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToFirst() {
|
||||
return mDelegate.moveToFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToLast() {
|
||||
return mDelegate.moveToLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToNext() {
|
||||
return mDelegate.moveToNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moveToPrevious() {
|
||||
return mDelegate.moveToPrevious();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFirst() {
|
||||
return mDelegate.isFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLast() {
|
||||
return mDelegate.isLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBeforeFirst() {
|
||||
return mDelegate.isBeforeFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAfterLast() {
|
||||
return mDelegate.isAfterLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnIndex(String columnName) {
|
||||
return mDelegate.getColumnIndex(columnName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
|
||||
return mDelegate.getColumnIndexOrThrow(columnName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int columnIndex) {
|
||||
return mDelegate.getColumnName(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getColumnNames() {
|
||||
return mDelegate.getColumnNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return mDelegate.getColumnCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBlob(int columnIndex) {
|
||||
return mDelegate.getBlob(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(int columnIndex) {
|
||||
return mDelegate.getString(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
|
||||
mDelegate.copyStringToBuffer(columnIndex, buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getShort(int columnIndex) {
|
||||
return mDelegate.getShort(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(int columnIndex) {
|
||||
return mDelegate.getInt(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(int columnIndex) {
|
||||
return mDelegate.getLong(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(int columnIndex) {
|
||||
return mDelegate.getFloat(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(int columnIndex) {
|
||||
return mDelegate.getDouble(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType(int columnIndex) {
|
||||
return mDelegate.getType(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNull(int columnIndex) {
|
||||
return mDelegate.isNull(columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated see Cursor.deactivate
|
||||
*/
|
||||
@Override
|
||||
@Deprecated
|
||||
public void deactivate() {
|
||||
mDelegate.deactivate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated see Cursor.requery
|
||||
*/
|
||||
@Override
|
||||
@Deprecated
|
||||
public boolean requery() {
|
||||
return mDelegate.requery();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerContentObserver(ContentObserver observer) {
|
||||
mDelegate.registerContentObserver(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterContentObserver(ContentObserver observer) {
|
||||
mDelegate.unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerDataSetObserver(DataSetObserver observer) {
|
||||
mDelegate.registerDataSetObserver(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterDataSetObserver(DataSetObserver observer) {
|
||||
mDelegate.unregisterDataSetObserver(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotificationUri(ContentResolver cr, Uri uri) {
|
||||
mDelegate.setNotificationUri(cr, uri);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
@Override
|
||||
public void setNotificationUris(@NonNull ContentResolver cr,
|
||||
@NonNull List<Uri> uris) {
|
||||
SupportSQLiteCompat.Api29Impl.setNotificationUris(mDelegate, cr, uris);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
@Override
|
||||
public Uri getNotificationUri() {
|
||||
return SupportSQLiteCompat.Api19Impl.getNotificationUri(mDelegate);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
@Nullable
|
||||
@Override
|
||||
public List<Uri> getNotificationUris() {
|
||||
return SupportSQLiteCompat.Api29Impl.getNotificationUris(mDelegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getWantsAllOnMoveCalls() {
|
||||
return mDelegate.getWantsAllOnMoveCalls();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
@Override
|
||||
public void setExtras(Bundle extras) {
|
||||
SupportSQLiteCompat.Api23Impl.setExtras(mDelegate, extras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getExtras() {
|
||||
return mDelegate.getExtras();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle respond(Bundle extras) {
|
||||
return mDelegate.respond(extras);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can't close our db if the SupportSqliteStatement is open.
|
||||
*
|
||||
* Each of these that are created need to be registered with RefCounter.
|
||||
*
|
||||
* On auto-close, RefCounter needs to close each of these before closing the db that these
|
||||
* were constructed from.
|
||||
*
|
||||
* Each of the methods here need to get
|
||||
*/
|
||||
//TODO(rohitsat) cache the prepared statement... I'm not sure what the performance implications
|
||||
// are for the way it's done here, but caching the prepared statement would definitely be more
|
||||
// complicated since we need to invalidate any of the PreparedStatements that were created
|
||||
// with this db
|
||||
private static class AutoClosingSupportSqliteStatement implements SupportSQLiteStatement {
|
||||
private final String mSql;
|
||||
private final ArrayList<Object> mBinds = new ArrayList<>();
|
||||
private final AutoCloser mAutoCloser;
|
||||
|
||||
AutoClosingSupportSqliteStatement(
|
||||
String sql, AutoCloser autoCloser) {
|
||||
mSql = sql;
|
||||
mAutoCloser = autoCloser;
|
||||
}
|
||||
|
||||
private <T> T executeSqliteStatementWithRefCount(Function<SupportSQLiteStatement, T> func) {
|
||||
return mAutoCloser.executeRefCountingFunction(
|
||||
db -> {
|
||||
SupportSQLiteStatement statement = db.compileStatement(mSql);
|
||||
doBinds(statement);
|
||||
return func.apply(statement);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void doBinds(SupportSQLiteStatement supportSQLiteStatement) {
|
||||
// Replay the binds
|
||||
for (int i = 0; i < mBinds.size(); i++) {
|
||||
int bindIndex = i + 1; // Bind indices are 1 based so we start at 1 not 0
|
||||
Object bind = mBinds.get(i);
|
||||
if (bind == null) {
|
||||
supportSQLiteStatement.bindNull(bindIndex);
|
||||
} else if (bind instanceof Long) {
|
||||
supportSQLiteStatement.bindLong(bindIndex, (Long) bind);
|
||||
} else if (bind instanceof Double) {
|
||||
supportSQLiteStatement.bindDouble(bindIndex, (Double) bind);
|
||||
} else if (bind instanceof String) {
|
||||
supportSQLiteStatement.bindString(bindIndex, (String) bind);
|
||||
} else if (bind instanceof byte[]) {
|
||||
supportSQLiteStatement.bindBlob(bindIndex, (byte[]) bind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveBinds(int bindIndex, Object value) {
|
||||
int index = bindIndex - 1;
|
||||
if (index >= mBinds.size()) {
|
||||
// Add null entries to the list until we have the desired # of indices
|
||||
for (int i = mBinds.size(); i <= index; i++) {
|
||||
mBinds.add(null);
|
||||
}
|
||||
}
|
||||
mBinds.set(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
// Nothing to do here since we re-compile the statement each time.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
executeSqliteStatementWithRefCount(statement -> {
|
||||
statement.execute();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int executeUpdateDelete() {
|
||||
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::executeUpdateDelete);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long executeInsert() {
|
||||
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::executeInsert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long simpleQueryForLong() {
|
||||
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::simpleQueryForLong);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String simpleQueryForString() {
|
||||
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::simpleQueryForString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindNull(int index) {
|
||||
saveBinds(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindLong(int index, long value) {
|
||||
saveBinds(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindDouble(int index, double value) {
|
||||
saveBinds(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindString(int index, String value) {
|
||||
saveBinds(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindBlob(int index, byte[] value) {
|
||||
saveBinds(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearBindings() {
|
||||
mBinds.clear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,570 @@
|
|||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.database.SQLException
|
||||
import android.database.sqlite.SQLiteTransactionListener
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.CancellationSignal
|
||||
import android.util.Pair
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.sqlite.db.SupportSQLiteCompat
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteStatement
|
||||
import java.io.IOException
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* A SupportSQLiteOpenHelper that has auto close enabled for database connections.
|
||||
*/
|
||||
internal class AutoClosingRoomOpenHelper(
|
||||
override val delegate: SupportSQLiteOpenHelper,
|
||||
@JvmField
|
||||
internal val autoCloser: AutoCloser
|
||||
) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper {
|
||||
private val autoClosingDb: AutoClosingSupportSQLiteDatabase
|
||||
|
||||
init {
|
||||
autoCloser.init(delegate)
|
||||
autoClosingDb = AutoClosingSupportSQLiteDatabase(
|
||||
autoCloser
|
||||
)
|
||||
}
|
||||
|
||||
@get:RequiresApi(api = Build.VERSION_CODES.N)
|
||||
override val writableDatabase: SupportSQLiteDatabase
|
||||
get() {
|
||||
autoClosingDb.pokeOpen()
|
||||
return autoClosingDb
|
||||
}
|
||||
|
||||
@get:RequiresApi(api = Build.VERSION_CODES.N)
|
||||
override val readableDatabase: SupportSQLiteDatabase
|
||||
get() {
|
||||
// Note we don't differentiate between writable db and readable db
|
||||
// We try to open the db so the open callbacks run
|
||||
autoClosingDb.pokeOpen()
|
||||
return autoClosingDb
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
autoClosingDb.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* SupportSQLiteDatabase that also keeps refcounts and autocloses the database
|
||||
*/
|
||||
internal class AutoClosingSupportSQLiteDatabase(
|
||||
private val autoCloser: AutoCloser
|
||||
) : SupportSQLiteDatabase {
|
||||
fun pokeOpen() {
|
||||
autoCloser.executeRefCountingFunction<Any?> { null }
|
||||
}
|
||||
|
||||
override fun compileStatement(sql: String): SupportSQLiteStatement {
|
||||
return AutoClosingSupportSqliteStatement(sql, autoCloser)
|
||||
}
|
||||
|
||||
override fun beginTransaction() {
|
||||
// We assume that after every successful beginTransaction() call there *must* be a
|
||||
// endTransaction() call.
|
||||
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
|
||||
try {
|
||||
db.beginTransaction()
|
||||
} catch (t: Throwable) {
|
||||
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||
// fails since there won't be a corresponding endTransaction call.
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginTransactionNonExclusive() {
|
||||
// We assume that after every successful beginTransaction() call there *must* be a
|
||||
// endTransaction() call.
|
||||
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
|
||||
try {
|
||||
db.beginTransactionNonExclusive()
|
||||
} catch (t: Throwable) {
|
||||
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||
// fails since there won't be a corresponding endTransaction call.
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) {
|
||||
// We assume that after every successful beginTransaction() call there *must* be a
|
||||
// endTransaction() call.
|
||||
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
|
||||
try {
|
||||
db.beginTransactionWithListener(transactionListener)
|
||||
} catch (t: Throwable) {
|
||||
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||
// fails since there won't be a corresponding endTransaction call.
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginTransactionWithListenerNonExclusive(
|
||||
transactionListener: SQLiteTransactionListener
|
||||
) {
|
||||
// We assume that after every successful beginTransaction() call there *will* always
|
||||
// be a corresponding endTransaction() call. Without a corresponding
|
||||
// endTransactionCall we will never close the db.
|
||||
val db = autoCloser.incrementCountAndEnsureDbIsOpen()
|
||||
try {
|
||||
db.beginTransactionWithListenerNonExclusive(transactionListener)
|
||||
} catch (t: Throwable) {
|
||||
// Note: we only want to decrement the ref count if the beginTransaction call
|
||||
// fails since there won't be a corresponding endTransaction call.
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
override fun endTransaction() {
|
||||
checkNotNull(autoCloser.delegateDatabase) {
|
||||
"End transaction called but delegateDb is null"
|
||||
}
|
||||
try {
|
||||
autoCloser.delegateDatabase!!.endTransaction()
|
||||
} finally {
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTransactionSuccessful() {
|
||||
autoCloser.delegateDatabase?.setTransactionSuccessful() ?: error(
|
||||
"setTransactionSuccessful called but delegateDb is null"
|
||||
)
|
||||
}
|
||||
|
||||
override fun inTransaction(): Boolean {
|
||||
return if (autoCloser.delegateDatabase == null) {
|
||||
false
|
||||
} else {
|
||||
autoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
override val isDbLockedByCurrentThread: Boolean
|
||||
get() = if (autoCloser.delegateDatabase == null) {
|
||||
false
|
||||
} else {
|
||||
autoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::isDbLockedByCurrentThread
|
||||
)
|
||||
}
|
||||
|
||||
override fun yieldIfContendedSafely(): Boolean {
|
||||
return autoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::yieldIfContendedSafely
|
||||
)
|
||||
}
|
||||
|
||||
override fun yieldIfContendedSafely(sleepAfterYieldDelayMillis: Long): Boolean {
|
||||
return autoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::yieldIfContendedSafely
|
||||
)
|
||||
}
|
||||
|
||||
override var version: Int
|
||||
get() = autoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::version
|
||||
)
|
||||
set(version) {
|
||||
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
|
||||
db.version = version
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override val maximumSize: Long
|
||||
get() = autoCloser.executeRefCountingFunction(
|
||||
SupportSQLiteDatabase::maximumSize
|
||||
)
|
||||
|
||||
override fun setMaximumSize(numBytes: Long): Long {
|
||||
return autoCloser.executeRefCountingFunction {
|
||||
db: SupportSQLiteDatabase -> db.setMaximumSize(numBytes)
|
||||
}
|
||||
}
|
||||
|
||||
override var pageSize: Long
|
||||
get() = autoCloser.executeRefCountingFunction(SupportSQLiteDatabase::pageSize)
|
||||
set(numBytes) {
|
||||
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
|
||||
db.pageSize = numBytes
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun query(query: String): Cursor {
|
||||
val result = try {
|
||||
autoCloser.incrementCountAndEnsureDbIsOpen().query(query)
|
||||
} catch (throwable: Throwable) {
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
throw throwable
|
||||
}
|
||||
return KeepAliveCursor(result, autoCloser)
|
||||
}
|
||||
|
||||
override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
|
||||
val result = try {
|
||||
autoCloser.incrementCountAndEnsureDbIsOpen().query(query, bindArgs)
|
||||
} catch (throwable: Throwable) {
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
throw throwable
|
||||
}
|
||||
return KeepAliveCursor(result, autoCloser)
|
||||
}
|
||||
|
||||
override fun query(query: SupportSQLiteQuery): Cursor {
|
||||
val result = try {
|
||||
autoCloser.incrementCountAndEnsureDbIsOpen().query(query)
|
||||
} catch (throwable: Throwable) {
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
throw throwable
|
||||
}
|
||||
return KeepAliveCursor(result, autoCloser)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
override fun query(
|
||||
query: SupportSQLiteQuery,
|
||||
cancellationSignal: CancellationSignal?
|
||||
): Cursor {
|
||||
val result = try {
|
||||
autoCloser.incrementCountAndEnsureDbIsOpen().query(query, cancellationSignal)
|
||||
} catch (throwable: Throwable) {
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
throw throwable
|
||||
}
|
||||
return KeepAliveCursor(result, autoCloser)
|
||||
}
|
||||
|
||||
@Throws(SQLException::class)
|
||||
override fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long {
|
||||
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
|
||||
db.insert(
|
||||
table, conflictAlgorithm,
|
||||
values
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int {
|
||||
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
|
||||
db.delete(
|
||||
table,
|
||||
whereClause,
|
||||
whereArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(
|
||||
table: String,
|
||||
conflictAlgorithm: Int,
|
||||
values: ContentValues,
|
||||
whereClause: String?,
|
||||
whereArgs: Array<out Any?>?
|
||||
): Int {
|
||||
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
|
||||
db.update(
|
||||
table, conflictAlgorithm,
|
||||
values, whereClause, whereArgs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SQLException::class)
|
||||
override fun execSQL(sql: String) {
|
||||
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
|
||||
db.execSQL(sql)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SQLException::class)
|
||||
override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
|
||||
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
|
||||
db.execSQL(sql, bindArgs)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override val isReadOnly: Boolean
|
||||
get() = autoCloser.executeRefCountingFunction { obj: SupportSQLiteDatabase ->
|
||||
obj.isReadOnly
|
||||
}
|
||||
|
||||
override val isOpen: Boolean
|
||||
get() {
|
||||
// Get the db without incrementing the reference cause we don't want to open
|
||||
// the db for an isOpen call.
|
||||
val localDelegate = autoCloser.delegateDatabase ?: return false
|
||||
return localDelegate.isOpen
|
||||
}
|
||||
|
||||
override fun needUpgrade(newVersion: Int): Boolean {
|
||||
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
|
||||
db.needUpgrade(
|
||||
newVersion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override val path: String?
|
||||
get() = autoCloser.executeRefCountingFunction { obj: SupportSQLiteDatabase ->
|
||||
obj.path
|
||||
}
|
||||
|
||||
override fun setLocale(locale: Locale) {
|
||||
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
|
||||
db.setLocale(locale)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun setMaxSqlCacheSize(cacheSize: Int) {
|
||||
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
|
||||
db.setMaxSqlCacheSize(cacheSize)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
override fun setForeignKeyConstraintsEnabled(enabled: Boolean) {
|
||||
autoCloser.executeRefCountingFunction<Any?> { db: SupportSQLiteDatabase ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
db.setForeignKeyConstraintsEnabled(enabled)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun enableWriteAheadLogging(): Boolean {
|
||||
throw UnsupportedOperationException(
|
||||
"Enable/disable write ahead logging on the " +
|
||||
"OpenHelper instead of on the database directly."
|
||||
)
|
||||
}
|
||||
|
||||
override fun disableWriteAheadLogging() {
|
||||
throw UnsupportedOperationException(
|
||||
"Enable/disable write ahead logging on the " +
|
||||
"OpenHelper instead of on the database directly."
|
||||
)
|
||||
}
|
||||
|
||||
@get:RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
override val isWriteAheadLoggingEnabled: Boolean
|
||||
get() = autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
return@executeRefCountingFunction db.isWriteAheadLoggingEnabled
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
override val attachedDbs: List<Pair<String, String>>?
|
||||
get() = autoCloser.executeRefCountingFunction {
|
||||
obj: SupportSQLiteDatabase -> obj.attachedDbs
|
||||
}
|
||||
|
||||
override val isDatabaseIntegrityOk: Boolean
|
||||
get() = autoCloser.executeRefCountingFunction {
|
||||
obj: SupportSQLiteDatabase -> obj.isDatabaseIntegrityOk
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
autoCloser.closeDatabaseIfOpen()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to keep the db alive until the cursor is closed, so we can't decrement our
|
||||
* reference count until the cursor is closed. The underlying database will not close until
|
||||
* this cursor is closed.
|
||||
*/
|
||||
private class KeepAliveCursor(
|
||||
private val delegate: Cursor,
|
||||
private val autoCloser: AutoCloser
|
||||
) : Cursor by delegate {
|
||||
// close is the only important/changed method here:
|
||||
override fun close() {
|
||||
delegate.close()
|
||||
autoCloser.decrementCountAndScheduleClose()
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
override fun setNotificationUris(
|
||||
cr: ContentResolver,
|
||||
uris: List<Uri>
|
||||
) {
|
||||
SupportSQLiteCompat.Api29Impl.setNotificationUris(delegate, cr, uris)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
override fun getNotificationUri(): Uri {
|
||||
return SupportSQLiteCompat.Api19Impl.getNotificationUri(delegate)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
override fun getNotificationUris(): List<Uri> {
|
||||
return SupportSQLiteCompat.Api29Impl.getNotificationUris(delegate)
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
override fun setExtras(extras: Bundle) {
|
||||
SupportSQLiteCompat.Api23Impl.setExtras(delegate, extras)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We can't close our db if the SupportSqliteStatement is open.
|
||||
*
|
||||
* Each of these that are created need to be registered with RefCounter.
|
||||
*
|
||||
* On auto-close, RefCounter needs to close each of these before closing the db that these
|
||||
* were constructed from.
|
||||
*
|
||||
* Each of the methods here need to get
|
||||
*/
|
||||
// TODO(rohitsat) cache the prepared statement... I'm not sure what the performance implications
|
||||
// are for the way it's done here, but caching the prepared statement would definitely be more
|
||||
// complicated since we need to invalidate any of the PreparedStatements that were created
|
||||
// with this db
|
||||
private class AutoClosingSupportSqliteStatement(
|
||||
private val sql: String,
|
||||
private val autoCloser: AutoCloser
|
||||
) : SupportSQLiteStatement {
|
||||
private val binds = ArrayList<Any?>()
|
||||
private fun <T> executeSqliteStatementWithRefCount(
|
||||
block: (SupportSQLiteStatement) -> T
|
||||
): T {
|
||||
return autoCloser.executeRefCountingFunction { db: SupportSQLiteDatabase ->
|
||||
val statement: SupportSQLiteStatement = db.compileStatement(sql)
|
||||
doBinds(statement)
|
||||
block(statement)
|
||||
}
|
||||
}
|
||||
|
||||
private fun doBinds(supportSQLiteStatement: SupportSQLiteStatement) {
|
||||
// Replay the binds
|
||||
binds.forEachIndexed { i, _ ->
|
||||
val bindIndex = i + 1 // Bind indices are 1 based so we start at 1 not 0
|
||||
when (val bind = binds[i]) {
|
||||
null -> {
|
||||
supportSQLiteStatement.bindNull(bindIndex)
|
||||
}
|
||||
is Long -> {
|
||||
supportSQLiteStatement.bindLong(bindIndex, bind)
|
||||
}
|
||||
is Double -> {
|
||||
supportSQLiteStatement.bindDouble(bindIndex, bind)
|
||||
}
|
||||
is String -> {
|
||||
supportSQLiteStatement.bindString(bindIndex, bind)
|
||||
}
|
||||
is ByteArray -> {
|
||||
supportSQLiteStatement.bindBlob(bindIndex, bind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveBinds(bindIndex: Int, value: Any?) {
|
||||
val index = bindIndex - 1
|
||||
if (index >= binds.size) {
|
||||
// Add null entries to the list until we have the desired # of indices
|
||||
for (i in binds.size..index) {
|
||||
binds.add(null)
|
||||
}
|
||||
}
|
||||
binds[index] = value
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
// Nothing to do here since we re-compile the statement each time.
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
executeSqliteStatementWithRefCount<Any?> { statement: SupportSQLiteStatement ->
|
||||
statement.execute()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun executeUpdateDelete(): Int {
|
||||
return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement ->
|
||||
obj.executeUpdateDelete() }
|
||||
}
|
||||
|
||||
override fun executeInsert(): Long {
|
||||
return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement ->
|
||||
obj.executeInsert()
|
||||
}
|
||||
}
|
||||
|
||||
override fun simpleQueryForLong(): Long {
|
||||
return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement ->
|
||||
obj.simpleQueryForLong()
|
||||
}
|
||||
}
|
||||
|
||||
override fun simpleQueryForString(): String? {
|
||||
return executeSqliteStatementWithRefCount { obj: SupportSQLiteStatement ->
|
||||
obj.simpleQueryForString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindNull(index: Int) {
|
||||
saveBinds(index, null)
|
||||
}
|
||||
|
||||
override fun bindLong(index: Int, value: Long) {
|
||||
saveBinds(index, value)
|
||||
}
|
||||
|
||||
override fun bindDouble(index: Int, value: Double) {
|
||||
saveBinds(index, value)
|
||||
}
|
||||
|
||||
override fun bindString(index: Int, value: String) {
|
||||
saveBinds(index, value)
|
||||
}
|
||||
|
||||
override fun bindBlob(index: Int, value: ByteArray) {
|
||||
saveBinds(index, value)
|
||||
}
|
||||
|
||||
override fun clearBindings() {
|
||||
binds.clear()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
|
||||
/**
|
||||
* Factory class for AutoClosingRoomOpenHelper
|
||||
*/
|
||||
final class AutoClosingRoomOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
|
||||
@NonNull
|
||||
private final SupportSQLiteOpenHelper.Factory mDelegate;
|
||||
|
||||
@NonNull
|
||||
private final AutoCloser mAutoCloser;
|
||||
|
||||
AutoClosingRoomOpenHelperFactory(
|
||||
@NonNull SupportSQLiteOpenHelper.Factory factory,
|
||||
@NonNull AutoCloser autoCloser) {
|
||||
mDelegate = factory;
|
||||
mAutoCloser = autoCloser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AutoClosingRoomOpenHelper instances.
|
||||
*/
|
||||
@Override
|
||||
@NonNull
|
||||
public AutoClosingRoomOpenHelper create(
|
||||
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
|
||||
return new AutoClosingRoomOpenHelper(mDelegate.create(configuration), mAutoCloser);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
|
||||
/**
|
||||
* Factory class for AutoClosingRoomOpenHelper
|
||||
*/
|
||||
internal class AutoClosingRoomOpenHelperFactory(
|
||||
private val delegate: SupportSQLiteOpenHelper.Factory,
|
||||
private val autoCloser: AutoCloser
|
||||
) : SupportSQLiteOpenHelper.Factory {
|
||||
/**
|
||||
* @return AutoClosingRoomOpenHelper instances.
|
||||
*/
|
||||
override fun create(
|
||||
configuration: SupportSQLiteOpenHelper.Configuration
|
||||
): AutoClosingRoomOpenHelper {
|
||||
return AutoClosingRoomOpenHelper(delegate.create(configuration), autoCloser)
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Utility class for in-process and multi-process key-based lock mechanism for safely copying
|
||||
* database files.
|
||||
* <p>
|
||||
* Acquiring the lock will be quick if no other thread or process has a lock with the same key.
|
||||
* But if the lock is already held then acquiring it will block, until the other thread or process
|
||||
* releases the lock. Note that the key and lock directory must be the same to achieve
|
||||
* synchronization.
|
||||
* <p>
|
||||
* Locking is done via two levels:
|
||||
* <ol>
|
||||
* <li>
|
||||
* Thread locking within the same JVM process is done via a map of String key to ReentrantLock
|
||||
* objects.
|
||||
* <li>
|
||||
* Multi-process locking is done via a lock file whose name contains the key and FileLock
|
||||
* objects.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public class CopyLock {
|
||||
|
||||
// in-process lock map
|
||||
private static final Map<String, Lock> sThreadLocks = new HashMap<>();
|
||||
|
||||
private final File mCopyLockFile;
|
||||
private final Lock mThreadLock;
|
||||
private final boolean mFileLevelLock;
|
||||
private FileChannel mLockChannel;
|
||||
|
||||
/**
|
||||
* Creates a lock with {@code name} and using {@code lockDir} as the directory for the
|
||||
* lock files.
|
||||
* @param name the name of this lock.
|
||||
* @param lockDir the directory where the lock files will be located.
|
||||
* @param processLock whether to use file for process level locking or not.
|
||||
*/
|
||||
public CopyLock(@NonNull String name, @NonNull File lockDir, boolean processLock) {
|
||||
mCopyLockFile = new File(lockDir, name + ".lck");
|
||||
mThreadLock = getThreadLock(mCopyLockFile.getAbsolutePath());
|
||||
mFileLevelLock = processLock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to grab the lock, blocking if already held by another thread or process.
|
||||
*/
|
||||
public void lock() {
|
||||
mThreadLock.lock();
|
||||
if (mFileLevelLock) {
|
||||
try {
|
||||
mLockChannel = new FileOutputStream(mCopyLockFile).getChannel();
|
||||
mLockChannel.lock();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to grab copy lock.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the lock.
|
||||
*/
|
||||
public void unlock() {
|
||||
if (mLockChannel != null) {
|
||||
try {
|
||||
mLockChannel.close();
|
||||
} catch (IOException ignored) { }
|
||||
}
|
||||
mThreadLock.unlock();
|
||||
}
|
||||
|
||||
private static Lock getThreadLock(String key) {
|
||||
synchronized (sThreadLocks) {
|
||||
Lock threadLock = sThreadLocks.get(key);
|
||||
if (threadLock == null) {
|
||||
threadLock = new ReentrantLock();
|
||||
sThreadLocks.put(key, threadLock);
|
||||
}
|
||||
return threadLock;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Cursor utilities for Room
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public class CursorUtil {
|
||||
|
||||
/**
|
||||
* Copies the given cursor into a in-memory cursor and then closes it.
|
||||
* <p>
|
||||
* This is useful for iterating over a cursor multiple times without the cost of JNI while
|
||||
* reading or IO while filling the window at the expense of memory consumption.
|
||||
*
|
||||
* @param c the cursor to copy.
|
||||
* @return a new cursor containing the same data as the given cursor.
|
||||
*/
|
||||
@NonNull
|
||||
public static Cursor copyAndClose(@NonNull Cursor c) {
|
||||
final MatrixCursor matrixCursor;
|
||||
try {
|
||||
matrixCursor = new MatrixCursor(c.getColumnNames(), c.getCount());
|
||||
while (c.moveToNext()) {
|
||||
final Object[] row = new Object[c.getColumnCount()];
|
||||
for (int i = 0; i < c.getColumnCount(); i++) {
|
||||
switch (c.getType(i)) {
|
||||
case Cursor.FIELD_TYPE_NULL:
|
||||
row[i] = null;
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_INTEGER:
|
||||
row[i] = c.getLong(i);
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_FLOAT:
|
||||
row[i] = c.getDouble(i);
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_STRING:
|
||||
row[i] = c.getString(i);
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_BLOB:
|
||||
row[i] = c.getBlob(i);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
matrixCursor.addRow(row);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
return matrixCursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches {@link Cursor#getColumnIndex(String)} to work around issues on older devices.
|
||||
* If the column is not found, it retries with the specified name surrounded by backticks.
|
||||
*
|
||||
* @param c The cursor.
|
||||
* @param name The name of the target column.
|
||||
* @return The index of the column, or -1 if not found.
|
||||
*/
|
||||
public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) {
|
||||
int index = c.getColumnIndex(name);
|
||||
if (index >= 0) {
|
||||
return index;
|
||||
}
|
||||
index = c.getColumnIndex("`" + name + "`");
|
||||
if (index >= 0) {
|
||||
return index;
|
||||
}
|
||||
return findColumnIndexBySuffix(c, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches {@link Cursor#getColumnIndexOrThrow(String)} to work around issues on older devices.
|
||||
* If the column is not found, it retries with the specified name surrounded by backticks.
|
||||
*
|
||||
* @param c The cursor.
|
||||
* @param name The name of the target column.
|
||||
* @return The index of the column.
|
||||
* @throws IllegalArgumentException if the column does not exist.
|
||||
*/
|
||||
public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) {
|
||||
final int index = getColumnIndex(c, name);
|
||||
if (index >= 0) {
|
||||
return index;
|
||||
}
|
||||
String availableColumns = "";
|
||||
try {
|
||||
availableColumns = Arrays.toString(c.getColumnNames());
|
||||
} catch (Exception e) {
|
||||
Log.d("RoomCursorUtil", "Cannot collect column names for debug purposes", e);
|
||||
}
|
||||
throw new IllegalArgumentException("column '" + name
|
||||
+ "' does not exist. Available columns: " + availableColumns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a column by name by appending `.` in front of it and checking by suffix match.
|
||||
* Also checks for the version wrapped with `` (backticks).
|
||||
* workaround for b/157261134 for API levels 25 and below
|
||||
*
|
||||
* e.g. "foo" will match "any.foo" and "`any.foo`"
|
||||
*/
|
||||
private static int findColumnIndexBySuffix(@NonNull Cursor cursor, @NonNull String name) {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
|
||||
// we need this workaround only on APIs < 26. So just return not found on newer APIs
|
||||
return -1;
|
||||
}
|
||||
if (name.length() == 0) {
|
||||
return -1;
|
||||
}
|
||||
final String[] columnNames = cursor.getColumnNames();
|
||||
return findColumnIndexBySuffix(columnNames, name);
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
static int findColumnIndexBySuffix(String[] columnNames, String name) {
|
||||
String dotSuffix = "." + name;
|
||||
String backtickSuffix = "." + name + "`";
|
||||
for (int index = 0; index < columnNames.length; index++) {
|
||||
String columnName = columnNames[index];
|
||||
// do not check if column name is not long enough. 1 char for table name, 1 char for '.'
|
||||
if (columnName.length() >= name.length() + 2) {
|
||||
if (columnName.endsWith(dotSuffix)) {
|
||||
return index;
|
||||
} else if (columnName.charAt(0) == '`'
|
||||
&& columnName.endsWith(backtickSuffix)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private CursorUtil() {
|
||||
}
|
||||
}
|
|
@ -1,243 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import android.database.AbstractWindowedCursor;
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.room.RoomDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteCompat;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Database utilities for Room
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public class DBUtil {
|
||||
|
||||
/**
|
||||
* Performs the SQLiteQuery on the given database.
|
||||
* <p>
|
||||
* This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
|
||||
* {@code true} and either the api level is below a certain threshold or the full result of the
|
||||
* query does not fit in a single window.
|
||||
*
|
||||
* @param db The database to perform the query on.
|
||||
* @param sqLiteQuery The query to perform.
|
||||
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
|
||||
* @return Result of the query.
|
||||
*
|
||||
* @deprecated This is only used in the generated code and shouldn't be called directly.
|
||||
*/
|
||||
@Deprecated
|
||||
@NonNull
|
||||
public static Cursor query(RoomDatabase db, SupportSQLiteQuery sqLiteQuery, boolean maybeCopy) {
|
||||
return query(db, sqLiteQuery, maybeCopy, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the SQLiteQuery on the given database.
|
||||
* <p>
|
||||
* This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
|
||||
* {@code true} and either the api level is below a certain threshold or the full result of the
|
||||
* query does not fit in a single window.
|
||||
*
|
||||
* @param db The database to perform the query on.
|
||||
* @param sqLiteQuery The query to perform.
|
||||
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
|
||||
* @param signal The cancellation signal to be attached to the query.
|
||||
* @return Result of the query.
|
||||
*/
|
||||
@NonNull
|
||||
public static Cursor query(@NonNull RoomDatabase db, @NonNull SupportSQLiteQuery sqLiteQuery,
|
||||
boolean maybeCopy, @Nullable CancellationSignal signal) {
|
||||
final Cursor cursor = db.query(sqLiteQuery, signal);
|
||||
if (maybeCopy && cursor instanceof AbstractWindowedCursor) {
|
||||
AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
|
||||
int rowsInCursor = windowedCursor.getCount(); // Should fill the window.
|
||||
int rowsInWindow;
|
||||
if (windowedCursor.hasWindow()) {
|
||||
rowsInWindow = windowedCursor.getWindow().getNumRows();
|
||||
} else {
|
||||
rowsInWindow = rowsInCursor;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
|
||||
return CursorUtil.copyAndClose(windowedCursor);
|
||||
}
|
||||
}
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all FTS content sync triggers created by Room.
|
||||
* <p>
|
||||
* FTS content sync triggers created by Room are those that are found in the sqlite_master table
|
||||
* who's names start with 'room_fts_content_sync_'.
|
||||
*
|
||||
* @param db The database.
|
||||
*/
|
||||
public static void dropFtsSyncTriggers(SupportSQLiteDatabase db) {
|
||||
List<String> existingTriggers = new ArrayList<>();
|
||||
Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'");
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
existingTriggers.add(cursor.getString(0));
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
for (String triggerName : existingTriggers) {
|
||||
if (triggerName.startsWith("room_fts_content_sync_")) {
|
||||
db.execSQL("DROP TRIGGER IF EXISTS " + triggerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for foreign key violations by executing a PRAGMA foreign_key_check.
|
||||
*/
|
||||
public static void foreignKeyCheck(@NonNull SupportSQLiteDatabase db,
|
||||
@NonNull String tableName) {
|
||||
Cursor cursor = db.query("PRAGMA foreign_key_check(`" + tableName + "`)");
|
||||
try {
|
||||
if (cursor.getCount() > 0) {
|
||||
String errorMsg = processForeignKeyCheckFailure(cursor);
|
||||
throw new IllegalStateException(errorMsg);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user version number out of the database header from the given file.
|
||||
*
|
||||
* @param databaseFile the database file.
|
||||
* @return the database version
|
||||
* @throws IOException if something goes wrong reading the file, such as bad database header or
|
||||
* missing permissions.
|
||||
*
|
||||
* @see <a href="https://www.sqlite.org/fileformat.html#user_version_number">User Version
|
||||
* Number</a>.
|
||||
*/
|
||||
public static int readVersion(@NonNull File databaseFile) throws IOException {
|
||||
FileChannel input = null;
|
||||
try {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
input = new FileInputStream(databaseFile).getChannel();
|
||||
input.tryLock(60, 4, true);
|
||||
input.position(60);
|
||||
int read = input.read(buffer);
|
||||
if (read != 4) {
|
||||
throw new IOException("Bad database header, unable to read 4 bytes at offset 60");
|
||||
}
|
||||
buffer.rewind();
|
||||
return buffer.getInt(); // ByteBuffer is big-endian by default
|
||||
} finally {
|
||||
if (input != null) {
|
||||
input.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CancellationSignal is only available from API 16 on. This function will create a new
|
||||
* instance of the Cancellation signal only if the current API > 16.
|
||||
*
|
||||
* @return A new instance of CancellationSignal or null.
|
||||
*/
|
||||
@Nullable
|
||||
public static CancellationSignal createCancellationSignal() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
return SupportSQLiteCompat.Api16Impl.createCancellationSignal();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the {@link Cursor} returned in case of a foreign key violation into a detailed
|
||||
* error message for debugging.
|
||||
* <p>
|
||||
* The foreign_key_check pragma returns one row output for each foreign key violation.
|
||||
* <p>
|
||||
* The cursor received has four columns for each row output. The first column is the name of
|
||||
* the child table. The second column is the rowId of the row that contains the foreign key
|
||||
* violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the
|
||||
* name of the parent table. The fourth column is the index of the specific foreign key
|
||||
* constraint that failed.
|
||||
*
|
||||
* @param cursor Cursor containing information regarding the FK violation
|
||||
* @return Error message generated containing debugging information
|
||||
*/
|
||||
private static String processForeignKeyCheckFailure(Cursor cursor) {
|
||||
int rowCount = cursor.getCount();
|
||||
String childTableName = null;
|
||||
Map<String, String> fkParentTables = new HashMap<>();
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
if (childTableName == null) {
|
||||
childTableName = cursor.getString(0);
|
||||
}
|
||||
String constraintIndex = cursor.getString(3);
|
||||
if (!fkParentTables.containsKey(constraintIndex)) {
|
||||
fkParentTables.put(constraintIndex, cursor.getString(2));
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Foreign key violation(s) detected in '")
|
||||
.append(childTableName).append("'.\n");
|
||||
sb.append("Number of different violations discovered: ")
|
||||
.append(fkParentTables.keySet().size()).append("\n");
|
||||
sb.append("Number of rows in violation: ")
|
||||
.append(rowCount).append("\n");
|
||||
sb.append("Violation(s) detected in the following constraint(s):\n");
|
||||
|
||||
for (Map.Entry<String, String> entry : fkParentTables.entrySet()) {
|
||||
sb.append("\tParent Table = ")
|
||||
.append(entry.getValue());
|
||||
sb.append(", Foreign Key Constraint Index = ")
|
||||
.append(entry.getKey()).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private DBUtil() {
|
||||
}
|
||||
}
|
|
@ -1,629 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.room.migration.AutoMigrationSpec;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Configuration class for a {@link RoomDatabase}.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class DatabaseConfiguration {
|
||||
|
||||
/**
|
||||
* The factory to use to access the database.
|
||||
*/
|
||||
@NonNull
|
||||
public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
|
||||
/**
|
||||
* The context to use while connecting to the database.
|
||||
*/
|
||||
@NonNull
|
||||
public final Context context;
|
||||
/**
|
||||
* The name of the database file or null if it is an in-memory database.
|
||||
*/
|
||||
@Nullable
|
||||
public final String name;
|
||||
|
||||
/**
|
||||
* Collection of available migrations.
|
||||
*/
|
||||
@NonNull
|
||||
public final RoomDatabase.MigrationContainer migrationContainer;
|
||||
|
||||
@Nullable
|
||||
public final List<RoomDatabase.Callback> callbacks;
|
||||
|
||||
@Nullable
|
||||
public final RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback;
|
||||
|
||||
@NonNull
|
||||
public final List<Object> typeConverters;
|
||||
|
||||
@NonNull
|
||||
public final List<AutoMigrationSpec> autoMigrationSpecs;
|
||||
|
||||
/**
|
||||
* Whether Room should throw an exception for queries run on the main thread.
|
||||
*/
|
||||
public final boolean allowMainThreadQueries;
|
||||
|
||||
/**
|
||||
* The journal mode for this database.
|
||||
*/
|
||||
public final RoomDatabase.JournalMode journalMode;
|
||||
|
||||
/**
|
||||
* The Executor used to execute asynchronous queries.
|
||||
*/
|
||||
@NonNull
|
||||
public final Executor queryExecutor;
|
||||
|
||||
/**
|
||||
* The Executor used to execute asynchronous transactions.
|
||||
*/
|
||||
@NonNull
|
||||
public final Executor transactionExecutor;
|
||||
|
||||
/**
|
||||
* If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and
|
||||
* synchronized with other instances of the same {@link RoomDatabase} file, including those
|
||||
* in a separate process.
|
||||
*/
|
||||
public final boolean multiInstanceInvalidation;
|
||||
|
||||
/**
|
||||
* Intent that should be bound to acquire the invalidation service or {@code null} if not used.
|
||||
*
|
||||
* @see {@link #multiInstanceInvalidation}
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public final Intent multiInstanceInvalidationServiceIntent;
|
||||
|
||||
/**
|
||||
* If true, Room should crash if a migration is missing.
|
||||
*/
|
||||
public final boolean requireMigration;
|
||||
|
||||
/**
|
||||
* If true, Room should perform a destructive migration when downgrading without an available
|
||||
* migration.
|
||||
*/
|
||||
public final boolean allowDestructiveMigrationOnDowngrade;
|
||||
|
||||
/**
|
||||
* The collection of schema versions from which migrations aren't required.
|
||||
*/
|
||||
private final Set<Integer> mMigrationNotRequiredFrom;
|
||||
|
||||
/**
|
||||
* The assets path to a pre-packaged database to copy from.
|
||||
*/
|
||||
@Nullable
|
||||
public final String copyFromAssetPath;
|
||||
|
||||
/**
|
||||
* The pre-packaged database file to copy from.
|
||||
*/
|
||||
@Nullable
|
||||
public final File copyFromFile;
|
||||
|
||||
/**
|
||||
* The callable to get the input stream from which a pre-package database file will be copied
|
||||
* from.
|
||||
*/
|
||||
@Nullable
|
||||
public final Callable<InputStream> copyFromInputStream;
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
|
||||
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
|
||||
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
|
||||
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* instead of recreating the tables.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
||||
@Nullable List<androidx.room.RoomDatabase.Callback> callbacks,
|
||||
boolean allowMainThreadQueries,
|
||||
RoomDatabase.JournalMode journalMode,
|
||||
@NonNull Executor queryExecutor,
|
||||
boolean requireMigration,
|
||||
@Nullable Set<Integer> migrationNotRequiredFrom) {
|
||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
||||
allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false,
|
||||
requireMigration, false, migrationNotRequiredFrom, null, null, null, null, null,
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
|
||||
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
|
||||
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
|
||||
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
||||
boolean allowMainThreadQueries,
|
||||
RoomDatabase.JournalMode journalMode,
|
||||
@NonNull Executor queryExecutor,
|
||||
@NonNull Executor transactionExecutor,
|
||||
boolean multiInstanceInvalidation,
|
||||
boolean requireMigration,
|
||||
boolean allowDestructiveMigrationOnDowngrade,
|
||||
@Nullable Set<Integer> migrationNotRequiredFrom) {
|
||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
||||
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
|
||||
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
|
||||
migrationNotRequiredFrom, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
|
||||
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
|
||||
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
|
||||
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
||||
boolean allowMainThreadQueries,
|
||||
RoomDatabase.JournalMode journalMode,
|
||||
@NonNull Executor queryExecutor,
|
||||
@NonNull Executor transactionExecutor,
|
||||
boolean multiInstanceInvalidation,
|
||||
boolean requireMigration,
|
||||
boolean allowDestructiveMigrationOnDowngrade,
|
||||
@Nullable Set<Integer> migrationNotRequiredFrom,
|
||||
@Nullable String copyFromAssetPath,
|
||||
@Nullable File copyFromFile) {
|
||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
||||
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
|
||||
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
|
||||
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, null, null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
|
||||
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
|
||||
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
|
||||
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
||||
boolean allowMainThreadQueries,
|
||||
@NonNull RoomDatabase.JournalMode journalMode,
|
||||
@NonNull Executor queryExecutor,
|
||||
@NonNull Executor transactionExecutor,
|
||||
boolean multiInstanceInvalidation,
|
||||
boolean requireMigration,
|
||||
boolean allowDestructiveMigrationOnDowngrade,
|
||||
@Nullable Set<Integer> migrationNotRequiredFrom,
|
||||
@Nullable String copyFromAssetPath,
|
||||
@Nullable File copyFromFile,
|
||||
@Nullable Callable<InputStream> copyFromInputStream) {
|
||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
||||
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
|
||||
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
|
||||
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream,
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
|
||||
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
|
||||
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
|
||||
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
* @param prepackagedDatabaseCallback The pre-packaged callback.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressLint("LambdaLast")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
||||
boolean allowMainThreadQueries,
|
||||
@NonNull RoomDatabase.JournalMode journalMode,
|
||||
@NonNull Executor queryExecutor,
|
||||
@NonNull Executor transactionExecutor,
|
||||
boolean multiInstanceInvalidation,
|
||||
boolean requireMigration,
|
||||
boolean allowDestructiveMigrationOnDowngrade,
|
||||
@Nullable Set<Integer> migrationNotRequiredFrom,
|
||||
@Nullable String copyFromAssetPath,
|
||||
@Nullable File copyFromFile,
|
||||
@Nullable Callable<InputStream> copyFromInputStream,
|
||||
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback) {
|
||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
||||
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
|
||||
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
|
||||
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream,
|
||||
prepackagedDatabaseCallback, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
|
||||
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
|
||||
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
|
||||
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
* @param prepackagedDatabaseCallback The pre-packaged callback.
|
||||
* @param typeConverters The type converters.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressLint("LambdaLast")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
||||
boolean allowMainThreadQueries,
|
||||
@NonNull RoomDatabase.JournalMode journalMode,
|
||||
@NonNull Executor queryExecutor,
|
||||
@NonNull Executor transactionExecutor,
|
||||
boolean multiInstanceInvalidation,
|
||||
boolean requireMigration,
|
||||
boolean allowDestructiveMigrationOnDowngrade,
|
||||
@Nullable Set<Integer> migrationNotRequiredFrom,
|
||||
@Nullable String copyFromAssetPath,
|
||||
@Nullable File copyFromFile,
|
||||
@Nullable Callable<InputStream> copyFromInputStream,
|
||||
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback,
|
||||
@Nullable List<Object> typeConverters) {
|
||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
||||
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
|
||||
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
|
||||
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream,
|
||||
prepackagedDatabaseCallback, typeConverters, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
|
||||
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
|
||||
* RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, boolean, Set, String, File,
|
||||
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List, List)}
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
* @param prepackagedDatabaseCallback The pre-packaged callback.
|
||||
* @param typeConverters The type converters.
|
||||
* @param autoMigrationSpecs The auto migration specs.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressLint("LambdaLast")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
||||
boolean allowMainThreadQueries,
|
||||
@NonNull RoomDatabase.JournalMode journalMode,
|
||||
@NonNull Executor queryExecutor,
|
||||
@NonNull Executor transactionExecutor,
|
||||
boolean multiInstanceInvalidation,
|
||||
boolean requireMigration,
|
||||
boolean allowDestructiveMigrationOnDowngrade,
|
||||
@Nullable Set<Integer> migrationNotRequiredFrom,
|
||||
@Nullable String copyFromAssetPath,
|
||||
@Nullable File copyFromFile,
|
||||
@Nullable Callable<InputStream> copyFromInputStream,
|
||||
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback,
|
||||
@Nullable List<Object> typeConverters,
|
||||
@Nullable List<AutoMigrationSpec> autoMigrationSpecs) {
|
||||
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
|
||||
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
|
||||
multiInstanceInvalidation ? new Intent(context,
|
||||
MultiInstanceInvalidationService.class) : null,
|
||||
requireMigration, allowDestructiveMigrationOnDowngrade, migrationNotRequiredFrom,
|
||||
copyFromAssetPath, copyFromFile, copyFromInputStream, prepackagedDatabaseCallback,
|
||||
typeConverters, autoMigrationSpecs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidationServiceIntent The intent to use to bind to the
|
||||
* invalidation service or {@code null} if not
|
||||
* used.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
* @param prepackagedDatabaseCallback The pre-packaged callback.
|
||||
* @param typeConverters The type converters.
|
||||
* @param autoMigrationSpecs The auto migration specs.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressLint("LambdaLast")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
|
||||
@NonNull RoomDatabase.MigrationContainer migrationContainer,
|
||||
@Nullable List<RoomDatabase.Callback> callbacks,
|
||||
boolean allowMainThreadQueries,
|
||||
@NonNull RoomDatabase.JournalMode journalMode,
|
||||
@NonNull Executor queryExecutor,
|
||||
@NonNull Executor transactionExecutor,
|
||||
@Nullable Intent multiInstanceInvalidationServiceIntent,
|
||||
boolean requireMigration,
|
||||
boolean allowDestructiveMigrationOnDowngrade,
|
||||
@Nullable Set<Integer> migrationNotRequiredFrom,
|
||||
@Nullable String copyFromAssetPath,
|
||||
@Nullable File copyFromFile,
|
||||
@Nullable Callable<InputStream> copyFromInputStream,
|
||||
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback,
|
||||
@Nullable List<Object> typeConverters,
|
||||
@Nullable List<AutoMigrationSpec> autoMigrationSpecs) {
|
||||
this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
|
||||
this.context = context;
|
||||
this.name = name;
|
||||
this.migrationContainer = migrationContainer;
|
||||
this.callbacks = callbacks;
|
||||
this.allowMainThreadQueries = allowMainThreadQueries;
|
||||
this.journalMode = journalMode;
|
||||
this.queryExecutor = queryExecutor;
|
||||
this.transactionExecutor = transactionExecutor;
|
||||
this.multiInstanceInvalidationServiceIntent =
|
||||
multiInstanceInvalidationServiceIntent;
|
||||
this.multiInstanceInvalidation = multiInstanceInvalidationServiceIntent != null;
|
||||
this.requireMigration = requireMigration;
|
||||
this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade;
|
||||
this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
|
||||
this.copyFromAssetPath = copyFromAssetPath;
|
||||
this.copyFromFile = copyFromFile;
|
||||
this.copyFromInputStream = copyFromInputStream;
|
||||
this.prepackagedDatabaseCallback = prepackagedDatabaseCallback;
|
||||
this.typeConverters = typeConverters == null ? Collections.emptyList() : typeConverters;
|
||||
this.autoMigrationSpecs = autoMigrationSpecs == null
|
||||
? Collections.emptyList() : autoMigrationSpecs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a migration is required from the specified version.
|
||||
*
|
||||
* @param version The schema version.
|
||||
* @return True if a valid migration is required, false otherwise.
|
||||
*
|
||||
* @deprecated Use {@link #isMigrationRequired(int, int)} which takes
|
||||
* {@link #allowDestructiveMigrationOnDowngrade} into account.
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean isMigrationRequiredFrom(int version) {
|
||||
return isMigrationRequired(version, version + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a migration is required between two versions.
|
||||
*
|
||||
* @param fromVersion The old schema version.
|
||||
* @param toVersion The new schema version.
|
||||
* @return True if a valid migration is required, false otherwise.
|
||||
*/
|
||||
public boolean isMigrationRequired(int fromVersion, int toVersion) {
|
||||
// Migrations are not required if its a downgrade AND destructive migration during downgrade
|
||||
// has been allowed.
|
||||
final boolean isDowngrade = fromVersion > toVersion;
|
||||
if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Migrations are required between the two versions if we generally require migrations
|
||||
// AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
|
||||
// exceptions.
|
||||
return requireMigration
|
||||
&& (mMigrationNotRequiredFrom == null
|
||||
|| !mMigrationNotRequiredFrom.contains(fromVersion));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,680 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* Configuration class for a [RoomDatabase].
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
open class DatabaseConfiguration @SuppressLint("LambdaLast")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
constructor(
|
||||
/**
|
||||
* The context to use while connecting to the database.
|
||||
*/
|
||||
@JvmField
|
||||
val context: Context,
|
||||
|
||||
/**
|
||||
* The name of the database file or null if it is an in-memory database.
|
||||
*/
|
||||
@JvmField
|
||||
val name: String?,
|
||||
|
||||
/**
|
||||
* The factory to use to access the database.
|
||||
*/
|
||||
@JvmField
|
||||
val sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
||||
|
||||
/**
|
||||
* Collection of available migrations.
|
||||
*/
|
||||
@JvmField
|
||||
val migrationContainer: RoomDatabase.MigrationContainer,
|
||||
|
||||
@JvmField
|
||||
val callbacks: List<RoomDatabase.Callback>?,
|
||||
|
||||
/**
|
||||
* Whether Room should throw an exception for queries run on the main thread.
|
||||
*/
|
||||
@JvmField
|
||||
val allowMainThreadQueries: Boolean,
|
||||
|
||||
/**
|
||||
* The journal mode for this database.
|
||||
*/
|
||||
@JvmField
|
||||
val journalMode: RoomDatabase.JournalMode,
|
||||
|
||||
/**
|
||||
* The Executor used to execute asynchronous queries.
|
||||
*/
|
||||
@JvmField
|
||||
val queryExecutor: Executor,
|
||||
|
||||
/**
|
||||
* The Executor used to execute asynchronous transactions.
|
||||
*/
|
||||
@JvmField
|
||||
val transactionExecutor: Executor,
|
||||
|
||||
/**
|
||||
* Intent that should be bound to acquire the invalidation service or `null` if not used.
|
||||
*
|
||||
* @see [multiInstanceInvalidation]
|
||||
*/
|
||||
@field:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@JvmField
|
||||
val multiInstanceInvalidationServiceIntent: Intent?,
|
||||
|
||||
@JvmField
|
||||
val requireMigration: Boolean,
|
||||
|
||||
@JvmField
|
||||
val allowDestructiveMigrationOnDowngrade: Boolean,
|
||||
|
||||
private val migrationNotRequiredFrom: Set<Int>?,
|
||||
@JvmField
|
||||
val copyFromAssetPath: String?,
|
||||
|
||||
@JvmField
|
||||
val copyFromFile: File?,
|
||||
|
||||
@JvmField
|
||||
val copyFromInputStream: Callable<InputStream>?,
|
||||
|
||||
@JvmField
|
||||
val prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?,
|
||||
|
||||
@JvmField
|
||||
val typeConverters: List<Any>,
|
||||
|
||||
@JvmField
|
||||
val autoMigrationSpecs: List<AutoMigrationSpec>
|
||||
) {
|
||||
/**
|
||||
* If true, table invalidation in an instance of [RoomDatabase] is broadcast and
|
||||
* synchronized with other instances of the same [RoomDatabase] file, including those
|
||||
* in a separate process.
|
||||
*/
|
||||
@JvmField
|
||||
val multiInstanceInvalidation: Boolean = multiInstanceInvalidationServiceIntent != null
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* instead of recreating the tables.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@Deprecated(
|
||||
"This constructor is deprecated.",
|
||||
ReplaceWith("DatabaseConfiguration(Context, String, " +
|
||||
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
|
||||
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
|
||||
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
|
||||
"List, List)")
|
||||
)
|
||||
constructor(
|
||||
context: Context,
|
||||
name: String?,
|
||||
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
||||
migrationContainer: RoomDatabase.MigrationContainer,
|
||||
callbacks: List<RoomDatabase.Callback>?,
|
||||
allowMainThreadQueries: Boolean,
|
||||
journalMode: RoomDatabase.JournalMode,
|
||||
queryExecutor: Executor,
|
||||
requireMigration: Boolean,
|
||||
migrationNotRequiredFrom: Set<Int>?
|
||||
) : this(
|
||||
context = context,
|
||||
name = name,
|
||||
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
|
||||
migrationContainer = migrationContainer,
|
||||
callbacks = callbacks,
|
||||
allowMainThreadQueries = allowMainThreadQueries,
|
||||
journalMode = journalMode,
|
||||
queryExecutor = queryExecutor,
|
||||
transactionExecutor = queryExecutor,
|
||||
multiInstanceInvalidationServiceIntent = null,
|
||||
allowDestructiveMigrationOnDowngrade = false,
|
||||
requireMigration = requireMigration,
|
||||
migrationNotRequiredFrom = migrationNotRequiredFrom,
|
||||
copyFromAssetPath = null,
|
||||
copyFromFile = null,
|
||||
prepackagedDatabaseCallback = null,
|
||||
copyFromInputStream = null,
|
||||
typeConverters = emptyList(),
|
||||
autoMigrationSpecs = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@Deprecated(
|
||||
"This constructor is deprecated.",
|
||||
ReplaceWith("DatabaseConfiguration(Context, String, " +
|
||||
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
|
||||
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
|
||||
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
|
||||
"List, List)")
|
||||
)
|
||||
constructor(
|
||||
context: Context,
|
||||
name: String?,
|
||||
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
||||
migrationContainer: RoomDatabase.MigrationContainer,
|
||||
callbacks: List<RoomDatabase.Callback>?,
|
||||
allowMainThreadQueries: Boolean,
|
||||
journalMode: RoomDatabase.JournalMode,
|
||||
queryExecutor: Executor,
|
||||
transactionExecutor: Executor,
|
||||
multiInstanceInvalidation: Boolean,
|
||||
requireMigration: Boolean,
|
||||
allowDestructiveMigrationOnDowngrade: Boolean,
|
||||
migrationNotRequiredFrom: Set<Int>?
|
||||
) : this(
|
||||
context = context,
|
||||
name = name,
|
||||
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
|
||||
migrationContainer = migrationContainer,
|
||||
callbacks = callbacks,
|
||||
allowMainThreadQueries = allowMainThreadQueries,
|
||||
journalMode = journalMode,
|
||||
queryExecutor = queryExecutor,
|
||||
transactionExecutor = transactionExecutor,
|
||||
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
|
||||
context,
|
||||
MultiInstanceInvalidationService::class.java
|
||||
) else null,
|
||||
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
|
||||
requireMigration = requireMigration,
|
||||
migrationNotRequiredFrom = migrationNotRequiredFrom,
|
||||
copyFromAssetPath = null,
|
||||
copyFromFile = null,
|
||||
prepackagedDatabaseCallback = null,
|
||||
copyFromInputStream = null,
|
||||
typeConverters = emptyList(),
|
||||
autoMigrationSpecs = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@Deprecated(
|
||||
"This constructor is deprecated.",
|
||||
ReplaceWith("DatabaseConfiguration(Context, String, " +
|
||||
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
|
||||
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
|
||||
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
|
||||
"List, List)")
|
||||
)
|
||||
constructor(
|
||||
context: Context,
|
||||
name: String?,
|
||||
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
||||
migrationContainer: RoomDatabase.MigrationContainer,
|
||||
callbacks: List<RoomDatabase.Callback>?,
|
||||
allowMainThreadQueries: Boolean,
|
||||
journalMode: RoomDatabase.JournalMode,
|
||||
queryExecutor: Executor,
|
||||
transactionExecutor: Executor,
|
||||
multiInstanceInvalidation: Boolean,
|
||||
requireMigration: Boolean,
|
||||
allowDestructiveMigrationOnDowngrade: Boolean,
|
||||
migrationNotRequiredFrom: Set<Int>?,
|
||||
copyFromAssetPath: String?,
|
||||
copyFromFile: File?
|
||||
) : this(
|
||||
context = context,
|
||||
name = name,
|
||||
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
|
||||
migrationContainer = migrationContainer,
|
||||
callbacks = callbacks,
|
||||
allowMainThreadQueries = allowMainThreadQueries,
|
||||
journalMode = journalMode,
|
||||
queryExecutor = queryExecutor,
|
||||
transactionExecutor = transactionExecutor,
|
||||
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
|
||||
context,
|
||||
MultiInstanceInvalidationService::class.java
|
||||
) else null,
|
||||
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
|
||||
requireMigration = requireMigration,
|
||||
migrationNotRequiredFrom = migrationNotRequiredFrom,
|
||||
copyFromAssetPath = copyFromAssetPath,
|
||||
copyFromFile = copyFromFile,
|
||||
prepackagedDatabaseCallback = null,
|
||||
copyFromInputStream = null,
|
||||
typeConverters = emptyList(),
|
||||
autoMigrationSpecs = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@Deprecated(
|
||||
"This constructor is deprecated.",
|
||||
ReplaceWith("DatabaseConfiguration(Context, String, " +
|
||||
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
|
||||
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
|
||||
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
|
||||
"List, List)")
|
||||
)
|
||||
constructor(
|
||||
context: Context,
|
||||
name: String?,
|
||||
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
||||
migrationContainer: RoomDatabase.MigrationContainer,
|
||||
callbacks: List<RoomDatabase.Callback>?,
|
||||
allowMainThreadQueries: Boolean,
|
||||
journalMode: RoomDatabase.JournalMode,
|
||||
queryExecutor: Executor,
|
||||
transactionExecutor: Executor,
|
||||
multiInstanceInvalidation: Boolean,
|
||||
requireMigration: Boolean,
|
||||
allowDestructiveMigrationOnDowngrade: Boolean,
|
||||
migrationNotRequiredFrom: Set<Int>?,
|
||||
copyFromAssetPath: String?,
|
||||
copyFromFile: File?,
|
||||
copyFromInputStream: Callable<InputStream>?
|
||||
) : this(
|
||||
context = context,
|
||||
name = name,
|
||||
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
|
||||
migrationContainer = migrationContainer,
|
||||
callbacks = callbacks,
|
||||
allowMainThreadQueries = allowMainThreadQueries,
|
||||
journalMode = journalMode,
|
||||
queryExecutor = queryExecutor,
|
||||
transactionExecutor = transactionExecutor,
|
||||
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
|
||||
context,
|
||||
MultiInstanceInvalidationService::class.java
|
||||
) else null,
|
||||
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
|
||||
requireMigration = requireMigration,
|
||||
migrationNotRequiredFrom = migrationNotRequiredFrom,
|
||||
copyFromAssetPath = copyFromAssetPath,
|
||||
copyFromFile = copyFromFile,
|
||||
prepackagedDatabaseCallback = null,
|
||||
copyFromInputStream = copyFromInputStream,
|
||||
typeConverters = emptyList(),
|
||||
autoMigrationSpecs = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
* @param prepackagedDatabaseCallback The pre-packaged callback.
|
||||
*
|
||||
*/
|
||||
@SuppressLint("LambdaLast")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@Deprecated(
|
||||
"This constructor is deprecated.",
|
||||
ReplaceWith("DatabaseConfiguration(Context, String, " +
|
||||
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
|
||||
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
|
||||
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
|
||||
"List, List)")
|
||||
)
|
||||
constructor(
|
||||
context: Context,
|
||||
name: String?,
|
||||
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
||||
migrationContainer: RoomDatabase.MigrationContainer,
|
||||
callbacks: List<RoomDatabase.Callback>?,
|
||||
allowMainThreadQueries: Boolean,
|
||||
journalMode: RoomDatabase.JournalMode,
|
||||
queryExecutor: Executor,
|
||||
transactionExecutor: Executor,
|
||||
multiInstanceInvalidation: Boolean,
|
||||
requireMigration: Boolean,
|
||||
allowDestructiveMigrationOnDowngrade: Boolean,
|
||||
migrationNotRequiredFrom: Set<Int>?,
|
||||
copyFromAssetPath: String?,
|
||||
copyFromFile: File?,
|
||||
copyFromInputStream: Callable<InputStream>?,
|
||||
prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?
|
||||
) : this(
|
||||
context = context,
|
||||
name = name,
|
||||
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
|
||||
migrationContainer = migrationContainer,
|
||||
callbacks = callbacks,
|
||||
allowMainThreadQueries = allowMainThreadQueries,
|
||||
journalMode = journalMode,
|
||||
queryExecutor = queryExecutor,
|
||||
transactionExecutor = transactionExecutor,
|
||||
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
|
||||
context,
|
||||
MultiInstanceInvalidationService::class.java
|
||||
) else null,
|
||||
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
|
||||
requireMigration = requireMigration,
|
||||
migrationNotRequiredFrom = migrationNotRequiredFrom,
|
||||
copyFromAssetPath = copyFromAssetPath,
|
||||
copyFromFile = copyFromFile,
|
||||
prepackagedDatabaseCallback = prepackagedDatabaseCallback,
|
||||
copyFromInputStream = copyFromInputStream,
|
||||
typeConverters = emptyList(),
|
||||
autoMigrationSpecs = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
* @param prepackagedDatabaseCallback The pre-packaged callback.
|
||||
* @param typeConverters The type converters.
|
||||
*
|
||||
*/
|
||||
@SuppressLint("LambdaLast")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@Deprecated(
|
||||
"This constructor is deprecated.",
|
||||
ReplaceWith("DatabaseConfiguration(Context, String, " +
|
||||
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
|
||||
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
|
||||
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
|
||||
"List, List)")
|
||||
)
|
||||
constructor(
|
||||
context: Context,
|
||||
name: String?,
|
||||
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
||||
migrationContainer: RoomDatabase.MigrationContainer,
|
||||
callbacks: List<RoomDatabase.Callback>?,
|
||||
allowMainThreadQueries: Boolean,
|
||||
journalMode: RoomDatabase.JournalMode,
|
||||
queryExecutor: Executor,
|
||||
transactionExecutor: Executor,
|
||||
multiInstanceInvalidation: Boolean,
|
||||
requireMigration: Boolean,
|
||||
allowDestructiveMigrationOnDowngrade: Boolean,
|
||||
migrationNotRequiredFrom: Set<Int>?,
|
||||
copyFromAssetPath: String?,
|
||||
copyFromFile: File?,
|
||||
copyFromInputStream: Callable<InputStream>?,
|
||||
prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?,
|
||||
typeConverters: List<Any>
|
||||
) : this(
|
||||
context = context,
|
||||
name = name,
|
||||
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
|
||||
migrationContainer = migrationContainer,
|
||||
callbacks = callbacks,
|
||||
allowMainThreadQueries = allowMainThreadQueries,
|
||||
journalMode = journalMode,
|
||||
queryExecutor = queryExecutor,
|
||||
transactionExecutor = transactionExecutor,
|
||||
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
|
||||
context,
|
||||
MultiInstanceInvalidationService::class.java
|
||||
) else null,
|
||||
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
|
||||
requireMigration = requireMigration,
|
||||
migrationNotRequiredFrom = migrationNotRequiredFrom,
|
||||
copyFromAssetPath = copyFromAssetPath,
|
||||
copyFromFile = copyFromFile,
|
||||
prepackagedDatabaseCallback = prepackagedDatabaseCallback,
|
||||
copyFromInputStream = copyFromInputStream,
|
||||
typeConverters = typeConverters,
|
||||
autoMigrationSpecs = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a database configuration with the given values.
|
||||
*
|
||||
* @param context The application context.
|
||||
* @param name Name of the database, can be null if it is in memory.
|
||||
* @param sqliteOpenHelperFactory The open helper factory to use.
|
||||
* @param migrationContainer The migration container for migrations.
|
||||
* @param callbacks The list of callbacks for database events.
|
||||
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
|
||||
* @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
|
||||
* @param queryExecutor The Executor used to execute asynchronous queries.
|
||||
* @param transactionExecutor The Executor used to execute asynchronous transactions.
|
||||
* @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
|
||||
* @param requireMigration True if Room should require a valid migration if version changes,
|
||||
* @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
|
||||
* migration is supplied during a downgrade.
|
||||
* @param migrationNotRequiredFrom The collection of schema versions from which migrations
|
||||
* aren't required.
|
||||
* @param copyFromAssetPath The assets path to the pre-packaged database.
|
||||
* @param copyFromFile The pre-packaged database file.
|
||||
* @param copyFromInputStream The callable to get the input stream from which a
|
||||
* pre-package database file will be copied from.
|
||||
* @param prepackagedDatabaseCallback The pre-packaged callback.
|
||||
* @param typeConverters The type converters.
|
||||
* @param autoMigrationSpecs The auto migration specs.
|
||||
*
|
||||
*/
|
||||
@SuppressLint("LambdaLast")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@Deprecated(
|
||||
"This constructor is deprecated.",
|
||||
ReplaceWith("DatabaseConfiguration(Context, String, " +
|
||||
"SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, " +
|
||||
"List, boolean, RoomDatabase.JournalMode, Executor, Executor, Intent, boolean, " +
|
||||
"boolean, Set, String, File, Callable, RoomDatabase.PrepackagedDatabaseCallback, " +
|
||||
"List, List)")
|
||||
)
|
||||
constructor(
|
||||
context: Context,
|
||||
name: String?,
|
||||
sqliteOpenHelperFactory: SupportSQLiteOpenHelper.Factory,
|
||||
migrationContainer: RoomDatabase.MigrationContainer,
|
||||
callbacks: List<RoomDatabase.Callback>?,
|
||||
allowMainThreadQueries: Boolean,
|
||||
journalMode: RoomDatabase.JournalMode,
|
||||
queryExecutor: Executor,
|
||||
transactionExecutor: Executor,
|
||||
multiInstanceInvalidation: Boolean,
|
||||
requireMigration: Boolean,
|
||||
allowDestructiveMigrationOnDowngrade: Boolean,
|
||||
migrationNotRequiredFrom: Set<Int>?,
|
||||
copyFromAssetPath: String?,
|
||||
copyFromFile: File?,
|
||||
copyFromInputStream: Callable<InputStream>?,
|
||||
prepackagedDatabaseCallback: RoomDatabase.PrepackagedDatabaseCallback?,
|
||||
typeConverters: List<Any>,
|
||||
autoMigrationSpecs: List<AutoMigrationSpec>
|
||||
) : this(
|
||||
context = context,
|
||||
name = name,
|
||||
sqliteOpenHelperFactory = sqliteOpenHelperFactory,
|
||||
migrationContainer = migrationContainer,
|
||||
callbacks = callbacks,
|
||||
allowMainThreadQueries = allowMainThreadQueries,
|
||||
journalMode = journalMode,
|
||||
queryExecutor = queryExecutor,
|
||||
transactionExecutor = transactionExecutor,
|
||||
multiInstanceInvalidationServiceIntent = if (multiInstanceInvalidation) Intent(
|
||||
context,
|
||||
MultiInstanceInvalidationService::class.java
|
||||
) else null,
|
||||
allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade,
|
||||
requireMigration = requireMigration,
|
||||
migrationNotRequiredFrom = migrationNotRequiredFrom,
|
||||
copyFromAssetPath = copyFromAssetPath,
|
||||
copyFromFile = copyFromFile,
|
||||
prepackagedDatabaseCallback = null,
|
||||
copyFromInputStream = copyFromInputStream,
|
||||
typeConverters = typeConverters,
|
||||
autoMigrationSpecs = autoMigrationSpecs
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns whether a migration is required from the specified version.
|
||||
*
|
||||
* @param version The schema version.
|
||||
* @return True if a valid migration is required, false otherwise.
|
||||
*
|
||||
*/
|
||||
@Deprecated(
|
||||
"""Use [isMigrationRequired(int, int)] which takes
|
||||
[allowDestructiveMigrationOnDowngrade] into account.""",
|
||||
ReplaceWith("isMigrationRequired(version, version + 1)")
|
||||
)
|
||||
open fun isMigrationRequiredFrom(version: Int): Boolean {
|
||||
return isMigrationRequired(version, version + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a migration is required between two versions.
|
||||
*
|
||||
* @param fromVersion The old schema version.
|
||||
* @param toVersion The new schema version.
|
||||
* @return True if a valid migration is required, false otherwise.
|
||||
*/
|
||||
open fun isMigrationRequired(fromVersion: Int, toVersion: Int): Boolean {
|
||||
// Migrations are not required if its a downgrade AND destructive migration during downgrade
|
||||
// has been allowed.
|
||||
val isDowngrade = fromVersion > toVersion
|
||||
if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
|
||||
return false
|
||||
} else {
|
||||
// Migrations are required between the two versions if we generally require migrations
|
||||
// AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
|
||||
// exceptions.
|
||||
return requireMigration && (migrationNotRequiredFrom == null ||
|
||||
!migrationNotRequiredFrom.contains(fromVersion))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,24 +13,19 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room;
|
||||
package androidx.room
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
|
||||
/**
|
||||
* Package private interface for OpenHelpers which delegate to other open helpers.
|
||||
* Internal interface for OpenHelpers which delegate to other open helpers.
|
||||
*
|
||||
* TODO(b/175612939): delete this interface once implementations are merged.
|
||||
*/
|
||||
interface DelegatingOpenHelper {
|
||||
|
||||
internal interface DelegatingOpenHelper {
|
||||
/**
|
||||
* Returns the delegate open helper (which may itself be a DelegatingOpenHelper) so
|
||||
* The delegate open helper (which may itself be a DelegatingOpenHelper) so
|
||||
* configurations on specific instances can be applied.
|
||||
*
|
||||
* @return the delegate
|
||||
*/
|
||||
@NonNull
|
||||
SupportSQLiteOpenHelper getDelegate();
|
||||
val delegate: SupportSQLiteOpenHelper
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This file exists to trick AGP/lint to work around b/234865137
|
|
@ -13,49 +13,40 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.sqlite.db.SupportSQLiteStatement
|
||||
|
||||
/**
|
||||
* Implementations of this class knows how to delete or update a particular entity.
|
||||
* <p>
|
||||
*
|
||||
* This is an internal library class and all of its implementations are auto-generated.
|
||||
*
|
||||
* @param <T> The type parameter of the entity to be deleted
|
||||
* @hide
|
||||
*/
|
||||
* @constructor Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the
|
||||
* given database.
|
||||
*
|
||||
* @param T The type parameter of the entity to be deleted
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteStatement {
|
||||
/**
|
||||
* Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given
|
||||
* database.
|
||||
*
|
||||
* @param database The database to delete / update the item in.
|
||||
*/
|
||||
public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
|
||||
super(database);
|
||||
}
|
||||
|
||||
abstract class EntityDeletionOrUpdateAdapter<T> (
|
||||
database: RoomDatabase
|
||||
) : SharedSQLiteStatement(database) {
|
||||
/**
|
||||
* Create the deletion or update query
|
||||
*
|
||||
* @return An SQL query that can delete or update instances of T.
|
||||
*/
|
||||
@Override
|
||||
protected abstract String createQuery();
|
||||
abstract override fun createQuery(): String
|
||||
|
||||
/**
|
||||
* Binds the entity into the given statement.
|
||||
*
|
||||
* @param statement The SQLite statement that prepared for the query returned from
|
||||
* createQuery.
|
||||
* createQuery.
|
||||
* @param entity The entity of type T.
|
||||
*/
|
||||
protected abstract void bind(SupportSQLiteStatement statement, T entity);
|
||||
protected abstract fun bind(statement: SupportSQLiteStatement, entity: T)
|
||||
|
||||
/**
|
||||
* Deletes or updates the given entities in the database and returns the affected row count.
|
||||
|
@ -63,13 +54,13 @@ public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteState
|
|||
* @param entity The entity to delete or update
|
||||
* @return The number of affected rows
|
||||
*/
|
||||
public final int handle(T entity) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
bind(stmt, entity);
|
||||
return stmt.executeUpdateDelete();
|
||||
fun handle(entity: T): Int {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
return try {
|
||||
bind(stmt, entity)
|
||||
stmt.executeUpdateDelete()
|
||||
} finally {
|
||||
release(stmt);
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,17 +70,17 @@ public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteState
|
|||
* @param entities Entities to delete or update
|
||||
* @return The number of affected rows
|
||||
*/
|
||||
public final int handleMultiple(Iterable<? extends T> entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
int total = 0;
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
total += stmt.executeUpdateDelete();
|
||||
fun handleMultiple(entities: Iterable<T>): Int {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
return try {
|
||||
var total = 0
|
||||
entities.forEach { entity ->
|
||||
bind(stmt, entity)
|
||||
total += stmt.executeUpdateDelete()
|
||||
}
|
||||
return total;
|
||||
total
|
||||
} finally {
|
||||
release(stmt);
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,17 +90,17 @@ public abstract class EntityDeletionOrUpdateAdapter<T> extends SharedSQLiteState
|
|||
* @param entities Entities to delete or update
|
||||
* @return The number of affected rows
|
||||
*/
|
||||
public final int handleMultiple(T[] entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
int total = 0;
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
total += stmt.executeUpdateDelete();
|
||||
fun handleMultiple(entities: Array<out T>): Int {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
return try {
|
||||
var total = 0
|
||||
entities.forEach { entity ->
|
||||
bind(stmt, entity)
|
||||
total += stmt.executeUpdateDelete()
|
||||
}
|
||||
return total;
|
||||
total
|
||||
} finally {
|
||||
release(stmt);
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementations of this class knows how to insert a particular entity.
|
||||
* <p>
|
||||
* This is an internal library class and all of its implementations are auto-generated.
|
||||
*
|
||||
* @param <T> The type parameter of the entity to be inserted
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public abstract class EntityInsertionAdapter<T> extends SharedSQLiteStatement {
|
||||
/**
|
||||
* Creates an InsertionAdapter that can insert the entity type T into the given database.
|
||||
*
|
||||
* @param database The database to insert into.
|
||||
*/
|
||||
public EntityInsertionAdapter(RoomDatabase database) {
|
||||
super(database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the entity into the given statement.
|
||||
*
|
||||
* @param statement The SQLite statement that prepared for the query returned from
|
||||
* createInsertQuery.
|
||||
* @param entity The entity of type T.
|
||||
*/
|
||||
protected abstract void bind(SupportSQLiteStatement statement, T entity);
|
||||
|
||||
/**
|
||||
* Inserts the entity into the database.
|
||||
*
|
||||
* @param entity The entity to insert
|
||||
*/
|
||||
public final void insert(T entity) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
bind(stmt, entity);
|
||||
stmt.executeInsert();
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
*/
|
||||
public final void insert(T[] entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
stmt.executeInsert();
|
||||
}
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
*/
|
||||
public final void insert(Iterable<? extends T> entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
stmt.executeInsert();
|
||||
}
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entity into the database and returns the row id.
|
||||
*
|
||||
* @param entity The entity to insert
|
||||
* @return The SQLite row id or -1 if no row is inserted
|
||||
*/
|
||||
public final long insertAndReturnId(T entity) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
bind(stmt, entity);
|
||||
return stmt.executeInsert();
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
public final long[] insertAndReturnIdsArray(Collection<? extends T> entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
final long[] result = new long[entities.size()];
|
||||
int index = 0;
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
result[index] = stmt.executeInsert();
|
||||
index++;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
public final long[] insertAndReturnIdsArray(T[] entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
final long[] result = new long[entities.length];
|
||||
int index = 0;
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
result[index] = stmt.executeInsert();
|
||||
index++;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
public final Long[] insertAndReturnIdsArrayBox(Collection<? extends T> entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
final Long[] result = new Long[entities.size()];
|
||||
int index = 0;
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
result[index] = stmt.executeInsert();
|
||||
index++;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
public final Long[] insertAndReturnIdsArrayBox(T[] entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
final Long[] result = new Long[entities.length];
|
||||
int index = 0;
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
result[index] = stmt.executeInsert();
|
||||
index++;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
public final List<Long> insertAndReturnIdsList(T[] entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
final List<Long> result = new ArrayList<>(entities.length);
|
||||
int index = 0;
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
result.add(index, stmt.executeInsert());
|
||||
index++;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
public final List<Long> insertAndReturnIdsList(Collection<? extends T> entities) {
|
||||
final SupportSQLiteStatement stmt = acquire();
|
||||
try {
|
||||
final List<Long> result = new ArrayList<>(entities.size());
|
||||
int index = 0;
|
||||
for (T entity : entities) {
|
||||
bind(stmt, entity);
|
||||
result.add(index, stmt.executeInsert());
|
||||
index++;
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
release(stmt);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.sqlite.db.SupportSQLiteStatement
|
||||
|
||||
/**
|
||||
* Implementations of this class knows how to insert a particular entity.
|
||||
*
|
||||
* This is an internal library class and all of its implementations are auto-generated.
|
||||
*
|
||||
* @constructor Creates an InsertionAdapter that can insert the entity type T into the given
|
||||
* database.
|
||||
*
|
||||
* @param T The type parameter of the entity to be inserted
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
abstract class EntityInsertionAdapter<T>(database: RoomDatabase) : SharedSQLiteStatement(database) {
|
||||
/**
|
||||
* Binds the entity into the given statement.
|
||||
*
|
||||
* @param statement The SQLite statement that prepared for the query returned from
|
||||
* createInsertQuery.
|
||||
* @param entity The entity of type T.
|
||||
*/
|
||||
protected abstract fun bind(statement: SupportSQLiteStatement, entity: T)
|
||||
|
||||
/**
|
||||
* Inserts the entity into the database.
|
||||
*
|
||||
* @param entity The entity to insert
|
||||
*/
|
||||
fun insert(entity: T) {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
try {
|
||||
bind(stmt, entity)
|
||||
stmt.executeInsert()
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
*/
|
||||
fun insert(entities: Array<out T>) {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
try {
|
||||
entities.forEach { entity ->
|
||||
bind(stmt, entity)
|
||||
stmt.executeInsert()
|
||||
}
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
*/
|
||||
fun insert(entities: Iterable<T>) {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
try {
|
||||
entities.forEach { entity ->
|
||||
bind(stmt, entity)
|
||||
stmt.executeInsert()
|
||||
}
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entity into the database and returns the row id.
|
||||
*
|
||||
* @param entity The entity to insert
|
||||
* @return The SQLite row id or -1 if no row is inserted
|
||||
*/
|
||||
fun insertAndReturnId(entity: T): Long {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
return try {
|
||||
bind(stmt, entity)
|
||||
stmt.executeInsert()
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
fun insertAndReturnIdsArray(entities: Collection<T>): LongArray {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
return try {
|
||||
val result = LongArray(entities.size)
|
||||
entities.forEachIndexed { index, entity ->
|
||||
bind(stmt, entity)
|
||||
result[index] = stmt.executeInsert()
|
||||
}
|
||||
result
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
fun insertAndReturnIdsArray(entities: Array<out T>): LongArray {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
return try {
|
||||
val result = LongArray(entities.size)
|
||||
entities.forEachIndexed { index, entity ->
|
||||
bind(stmt, entity)
|
||||
result[index] = stmt.executeInsert()
|
||||
}
|
||||
result
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
fun insertAndReturnIdsArrayBox(entities: Collection<T>): Array<out Long> {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
val iterator = entities.iterator()
|
||||
return try {
|
||||
val result = Array(entities.size) {
|
||||
val entity = iterator.next()
|
||||
bind(stmt, entity)
|
||||
stmt.executeInsert()
|
||||
}
|
||||
result
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
fun insertAndReturnIdsArrayBox(entities: Array<out T>): Array<out Long> {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
val iterator = entities.iterator()
|
||||
return try {
|
||||
val result = Array(entities.size) {
|
||||
val entity = iterator.next()
|
||||
bind(stmt, entity)
|
||||
stmt.executeInsert()
|
||||
}
|
||||
result
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
fun insertAndReturnIdsList(entities: Array<out T>): List<Long> {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
return try {
|
||||
buildList {
|
||||
entities.forEach { entity ->
|
||||
bind(stmt, entity)
|
||||
add(stmt.executeInsert())
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to insert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
fun insertAndReturnIdsList(entities: Collection<T>): List<Long> {
|
||||
val stmt: SupportSQLiteStatement = acquire()
|
||||
return try {
|
||||
buildList {
|
||||
entities.forEach { entity ->
|
||||
bind(stmt, entity)
|
||||
add(stmt.executeInsert())
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
release(stmt)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.annotation.RestrictTo
|
||||
|
||||
/**
|
||||
* The error code defined by SQLite Library for SQLITE_CONSTRAINT_PRIMARYKEY error
|
||||
* Only used by android of version newer than 19.
|
||||
*/
|
||||
private const val SQLITE_CONSTRAINT_PRIMARYKEY = "1555"
|
||||
|
||||
/**
|
||||
* The error code defined by SQLite Library for SQLITE_CONSTRAINT_UNIQUE error.
|
||||
*/
|
||||
private const val SQLITE_CONSTRAINT_UNIQUE = "2067"
|
||||
|
||||
/**
|
||||
* For android of version below and including 19, use error message instead of
|
||||
* error code to check
|
||||
*/
|
||||
private const val ErrorMsg = "unique"
|
||||
|
||||
/**
|
||||
* This class knows how to insert an entity. When the insertion fails
|
||||
* due to a unique constraint conflict (i.e. primary key conflict),
|
||||
* it will perform an update.
|
||||
*
|
||||
* @constructor Creates an EntityUpsertionAdapter that can upsert entity of type T
|
||||
* into the database using the given insertionAdapter to perform insertion and
|
||||
* updateAdapter to perform update when the insertion fails
|
||||
*
|
||||
* @param T the type param of the entity to be upserted
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
class EntityUpsertionAdapter<T>(
|
||||
private val insertionAdapter: EntityInsertionAdapter<T>,
|
||||
private val updateAdapter: EntityDeletionOrUpdateAdapter<T>
|
||||
) {
|
||||
/**
|
||||
* Inserts the entity into the database. If a constraint exception is thrown
|
||||
* i.e. a primary key conflict, update the existing entity.
|
||||
*
|
||||
* @param entity The entity to insert
|
||||
*/
|
||||
fun upsert(entity: T) {
|
||||
try {
|
||||
insertionAdapter.insert(entity)
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upserts (insert or update) the given entities into the database.
|
||||
* For each entity, insert if it is not already in the database
|
||||
* update if there is a constraint conflict.
|
||||
*
|
||||
* @param entities array of entities to upsert
|
||||
*/
|
||||
fun upsert(entities: Array<out T>) {
|
||||
entities.forEach { entity ->
|
||||
try {
|
||||
insertionAdapter.insert(entity)
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun upsert(entities: Iterable<T>) {
|
||||
entities.forEach { entity ->
|
||||
try {
|
||||
insertionAdapter.insert(entity)
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upserts the given entity into the database and returns the row id.
|
||||
* If the insertion failed, update the existing entity and return -1.
|
||||
*
|
||||
* @param entity The entity to upsert
|
||||
* @return The SQLite row id or -1 if the insertion failed and update
|
||||
* is performed
|
||||
*/
|
||||
fun upsertAndReturnId(entity: T): Long {
|
||||
return try {
|
||||
insertionAdapter.insertAndReturnId(entity)
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entity)
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upserts the given entities into the database and returns the row ids.
|
||||
*
|
||||
* @param entities Entities to upsert
|
||||
* @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
|
||||
*/
|
||||
fun upsertAndReturnIdsArray(entities: Array<out T>): LongArray {
|
||||
return LongArray(entities.size) { index ->
|
||||
try {
|
||||
insertionAdapter.insertAndReturnId(entities[index])
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entities[index])
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertAndReturnIdsArray(entities: Collection<T>): LongArray {
|
||||
val iterator = entities.iterator()
|
||||
return LongArray(entities.size) {
|
||||
val entity = iterator.next()
|
||||
try {
|
||||
insertionAdapter.insertAndReturnId(entity)
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entity)
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertAndReturnIdsList(entities: Array<out T>): List<Long> {
|
||||
return buildList {
|
||||
entities.forEach { entity ->
|
||||
try {
|
||||
add(insertionAdapter.insertAndReturnId(entity))
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entity)
|
||||
add(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertAndReturnIdsList(entities: Collection<T>): List<Long> {
|
||||
return buildList {
|
||||
entities.forEach { entity ->
|
||||
try {
|
||||
add(insertionAdapter.insertAndReturnId(entity))
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entity)
|
||||
add(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertAndReturnIdsArrayBox(entities: Array<out T>): Array<out Long> {
|
||||
return Array(entities.size) { index ->
|
||||
try {
|
||||
insertionAdapter.insertAndReturnId(entities[index])
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entities[index])
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertAndReturnIdsArrayBox(entities: Collection<T>): Array<out Long> {
|
||||
val iterator = entities.iterator()
|
||||
return Array(entities.size) {
|
||||
val entity = iterator.next()
|
||||
try {
|
||||
insertionAdapter.insertAndReturnId(entity)
|
||||
} catch (ex: SQLiteConstraintException) {
|
||||
checkUniquenessException(ex)
|
||||
updateAdapter.handle(entity)
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if the exception is caused by Uniqueness constraint (Primary Key Conflict).
|
||||
* If yes, upsert should update the existing one. If not, upsert should re-throw the
|
||||
* exception.
|
||||
* For android of version newer than KITKAT(19), SQLite supports ErrorCode. Otherwise,
|
||||
* check with Error Message.
|
||||
*
|
||||
* @param ex the exception thrown by the insert attempt
|
||||
*/
|
||||
private fun checkUniquenessException(ex: SQLiteConstraintException) {
|
||||
val message = ex.message ?: throw ex
|
||||
val hasUniqueConstraintEx =
|
||||
message.contains(ErrorMsg, ignoreCase = true) ||
|
||||
message.contains(SQLITE_CONSTRAINT_UNIQUE) ||
|
||||
message.contains(SQLITE_CONSTRAINT_PRIMARYKEY)
|
||||
|
||||
if (!hasUniqueConstraintEx) {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,17 +13,18 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.RequiresOptIn;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Target;
|
||||
package androidx.room
|
||||
|
||||
import androidx.annotation.RequiresOptIn
|
||||
|
||||
/**
|
||||
* APIs marked with ExperimentalRoomApi are experimental and may change.
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@RequiresOptIn()
|
||||
public @interface ExperimentalRoomApi {}
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.FUNCTION
|
||||
)
|
||||
@Suppress("UnsafeOptInUsageError")
|
||||
@RequiresOptIn
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class ExperimentalRoomApi
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
|
||||
/**
|
||||
* File utilities for Room
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public class FileUtil {
|
||||
|
||||
/**
|
||||
* Copies data from the input channel to the output file channel.
|
||||
*
|
||||
* @param input the input channel to copy.
|
||||
* @param output the output channel to copy.
|
||||
* @throws IOException if there is an I/O error.
|
||||
*/
|
||||
@SuppressLint("LambdaLast")
|
||||
public static void copy(@NonNull ReadableByteChannel input, @NonNull FileChannel output)
|
||||
throws IOException {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
||||
output.transferFrom(input, 0, Long.MAX_VALUE);
|
||||
} else {
|
||||
InputStream inputStream = Channels.newInputStream(input);
|
||||
OutputStream outputStream = Channels.newOutputStream(output);
|
||||
int length;
|
||||
byte[] buffer = new byte[1024 * 4];
|
||||
while ((length = inputStream.read(buffer)) > 0) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
}
|
||||
output.force(false);
|
||||
} finally {
|
||||
input.close();
|
||||
output.close();
|
||||
}
|
||||
}
|
||||
|
||||
private FileUtil() {
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A data class that holds the information about an FTS table.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public final class FtsTableInfo {
|
||||
|
||||
// A set of valid FTS Options
|
||||
private static final String[] FTS_OPTIONS = new String[] {
|
||||
"tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
|
||||
"order=", "prefix=", "uncompress="};
|
||||
|
||||
/**
|
||||
* The table name
|
||||
*/
|
||||
public final String name;
|
||||
|
||||
/**
|
||||
* The column names
|
||||
*/
|
||||
public final Set<String> columns;
|
||||
|
||||
/**
|
||||
* The set of options. Each value in the set contains the option in the following format:
|
||||
* <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
|
||||
+ '}';
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* A helper class that maintains {@link RoomTrackingLiveData} instances for an
|
||||
* {@link InvalidationTracker}.
|
||||
* <p>
|
||||
* We keep a strong reference to active LiveData instances to avoid garbage collection in case
|
||||
* developer does not hold onto the returned LiveData.
|
||||
*/
|
||||
class InvalidationLiveDataContainer {
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
final Set<LiveData> mLiveDataSet = Collections.newSetFromMap(
|
||||
new IdentityHashMap<LiveData, Boolean>()
|
||||
);
|
||||
private final RoomDatabase mDatabase;
|
||||
|
||||
InvalidationLiveDataContainer(RoomDatabase database) {
|
||||
mDatabase = database;
|
||||
}
|
||||
|
||||
<T> LiveData<T> create(String[] tableNames, boolean inTransaction,
|
||||
Callable<T> computeFunction) {
|
||||
return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
|
||||
tableNames);
|
||||
}
|
||||
|
||||
void onActive(LiveData liveData) {
|
||||
mLiveDataSet.add(liveData);
|
||||
}
|
||||
|
||||
void onInactive(LiveData liveData) {
|
||||
mLiveDataSet.remove(liveData);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import java.util.Collections
|
||||
import java.util.IdentityHashMap
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
/**
|
||||
* A helper class that maintains [RoomTrackingLiveData] instances for an
|
||||
* [InvalidationTracker].
|
||||
*
|
||||
* We keep a strong reference to active LiveData instances to avoid garbage collection in case
|
||||
* developer does not hold onto the returned LiveData.
|
||||
*/
|
||||
internal class InvalidationLiveDataContainer(private val database: RoomDatabase) {
|
||||
internal val liveDataSet: MutableSet<LiveData<*>> = Collections.newSetFromMap(IdentityHashMap())
|
||||
|
||||
fun <T> create(
|
||||
tableNames: Array<out String>,
|
||||
inTransaction: Boolean,
|
||||
computeFunction: Callable<T?>
|
||||
): LiveData<T> {
|
||||
return RoomTrackingLiveData(
|
||||
database,
|
||||
this,
|
||||
inTransaction,
|
||||
computeFunction,
|
||||
tableNames
|
||||
)
|
||||
}
|
||||
|
||||
fun onActive(liveData: LiveData<*>) {
|
||||
liveDataSet.add(liveData)
|
||||
}
|
||||
|
||||
fun onInactive(liveData: LiveData<*>) {
|
||||
liveDataSet.remove(liveData)
|
||||
}
|
||||
}
|
|
@ -1,902 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteException;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.arch.core.internal.SafeIterableMap;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
/**
|
||||
* InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
|
||||
* these tables.
|
||||
*/
|
||||
// Some details on how the InvalidationTracker works:
|
||||
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
|
||||
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
|
||||
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
|
||||
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
|
||||
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
|
||||
// tables.
|
||||
// * Each update (write operation) on one of the observed tables triggers an update into the
|
||||
// memory table table, flipping the invalidated flag ON.
|
||||
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
|
||||
// It works as an Observer, and notifies other instances of table invalidation.
|
||||
public class InvalidationTracker {
|
||||
|
||||
private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
|
||||
|
||||
private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
|
||||
|
||||
private static final String TABLE_ID_COLUMN_NAME = "table_id";
|
||||
|
||||
private static final String INVALIDATED_COLUMN_NAME = "invalidated";
|
||||
|
||||
private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
|
||||
+ "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, "
|
||||
+ INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
|
||||
+ " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
|
||||
+ " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";
|
||||
|
||||
@NonNull
|
||||
final HashMap<String, Integer> mTableIdLookup;
|
||||
final String[] mTableNames;
|
||||
|
||||
@NonNull
|
||||
private Map<String, Set<String>> mViewTables;
|
||||
|
||||
@Nullable
|
||||
AutoCloser mAutoCloser = null;
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
final RoomDatabase mDatabase;
|
||||
|
||||
AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
|
||||
|
||||
private volatile boolean mInitialized = false;
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
volatile SupportSQLiteStatement mCleanupStatement;
|
||||
|
||||
private final ObservedTableTracker mObservedTableTracker;
|
||||
|
||||
private final InvalidationLiveDataContainer mInvalidationLiveDataContainer;
|
||||
|
||||
// should be accessed with synchronization only.
|
||||
@VisibleForTesting
|
||||
@SuppressLint("RestrictedApi")
|
||||
final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();
|
||||
|
||||
private MultiInstanceInvalidationClient mMultiInstanceInvalidationClient;
|
||||
|
||||
private final Object mSyncTriggersLock = new Object();
|
||||
|
||||
/**
|
||||
* Used by the generated code.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public InvalidationTracker(RoomDatabase database, String... tableNames) {
|
||||
this(database, new HashMap<String, String>(), Collections.<String, Set<String>>emptyMap(),
|
||||
tableNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the generated code.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public InvalidationTracker(RoomDatabase database, Map<String, String> shadowTablesMap,
|
||||
Map<String, Set<String>> viewTables, String... tableNames) {
|
||||
mDatabase = database;
|
||||
mObservedTableTracker = new ObservedTableTracker(tableNames.length);
|
||||
mTableIdLookup = new HashMap<>();
|
||||
mViewTables = viewTables;
|
||||
mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase);
|
||||
final int size = tableNames.length;
|
||||
mTableNames = new String[size];
|
||||
for (int id = 0; id < size; id++) {
|
||||
final String tableName = tableNames[id].toLowerCase(Locale.US);
|
||||
mTableIdLookup.put(tableName, id);
|
||||
String shadowTableName = shadowTablesMap.get(tableNames[id]);
|
||||
if (shadowTableName != null) {
|
||||
mTableNames[id] = shadowTableName.toLowerCase(Locale.US);
|
||||
} else {
|
||||
mTableNames[id] = tableName;
|
||||
}
|
||||
}
|
||||
// Adjust table id lookup for those tables whose shadow table is another already mapped
|
||||
// table (e.g. external content fts tables).
|
||||
for (Map.Entry<String, String> shadowTableEntry : shadowTablesMap.entrySet()) {
|
||||
String shadowTableName = shadowTableEntry.getValue().toLowerCase(Locale.US);
|
||||
if (mTableIdLookup.containsKey(shadowTableName)) {
|
||||
String tableName = shadowTableEntry.getKey().toLowerCase(Locale.US);
|
||||
mTableIdLookup.put(tableName, mTableIdLookup.get(shadowTableName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the auto closer for this invalidation tracker so that the invalidation tracker can
|
||||
* ensure that the database is not closed if there are pending invalidations that haven't yet
|
||||
* been flushed.
|
||||
*
|
||||
* This also adds a callback to the autocloser to ensure that the InvalidationTracker is in
|
||||
* an ok state once the table is invalidated.
|
||||
*
|
||||
* This must be called before the database is used.
|
||||
*
|
||||
* @param autoCloser the autocloser associated with the db
|
||||
*/
|
||||
void setAutoCloser(AutoCloser autoCloser) {
|
||||
this.mAutoCloser = autoCloser;
|
||||
mAutoCloser.setAutoCloseCallback(this::onAutoCloseCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to initialize table tracking.
|
||||
* <p>
|
||||
* You should never call this method, it is called by the generated code.
|
||||
*/
|
||||
void internalInit(SupportSQLiteDatabase database) {
|
||||
synchronized (this) {
|
||||
if (mInitialized) {
|
||||
Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
|
||||
return;
|
||||
}
|
||||
|
||||
// These actions are not in a transaction because temp_store is not allowed to be
|
||||
// performed on a transaction, and recursive_triggers is not affected by transactions.
|
||||
database.execSQL("PRAGMA temp_store = MEMORY;");
|
||||
database.execSQL("PRAGMA recursive_triggers='ON';");
|
||||
database.execSQL(CREATE_TRACKING_TABLE_SQL);
|
||||
syncTriggers(database);
|
||||
mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);
|
||||
mInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
void onAutoCloseCallback() {
|
||||
synchronized (this) {
|
||||
mInitialized = false;
|
||||
mObservedTableTracker.resetTriggerState();
|
||||
}
|
||||
}
|
||||
|
||||
void startMultiInstanceInvalidation(Context context, String name, Intent serviceIntent) {
|
||||
mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name,
|
||||
serviceIntent, this, mDatabase.getQueryExecutor());
|
||||
}
|
||||
|
||||
void stopMultiInstanceInvalidation() {
|
||||
if (mMultiInstanceInvalidationClient != null) {
|
||||
mMultiInstanceInvalidationClient.stop();
|
||||
mMultiInstanceInvalidationClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendTriggerName(StringBuilder builder, String tableName,
|
||||
String triggerType) {
|
||||
builder.append("`")
|
||||
.append("room_table_modification_trigger_")
|
||||
.append(tableName)
|
||||
.append("_")
|
||||
.append(triggerType)
|
||||
.append("`");
|
||||
}
|
||||
|
||||
private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
|
||||
final String tableName = mTableNames[tableId];
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (String trigger : TRIGGERS) {
|
||||
stringBuilder.setLength(0);
|
||||
stringBuilder.append("DROP TRIGGER IF EXISTS ");
|
||||
appendTriggerName(stringBuilder, tableName, trigger);
|
||||
writableDb.execSQL(stringBuilder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
|
||||
writableDb.execSQL(
|
||||
"INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
|
||||
final String tableName = mTableNames[tableId];
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (String trigger : TRIGGERS) {
|
||||
stringBuilder.setLength(0);
|
||||
stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
|
||||
appendTriggerName(stringBuilder, tableName, trigger);
|
||||
stringBuilder.append(" AFTER ")
|
||||
.append(trigger)
|
||||
.append(" ON `")
|
||||
.append(tableName)
|
||||
.append("` BEGIN UPDATE ")
|
||||
.append(UPDATE_TABLE_NAME)
|
||||
.append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1")
|
||||
.append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId)
|
||||
.append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0")
|
||||
.append("; END");
|
||||
writableDb.execSQL(stringBuilder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given observer to the observers list and it will be notified if any table it
|
||||
* observes changes.
|
||||
* <p>
|
||||
* Database changes are pulled on another thread so in some race conditions, the observer might
|
||||
* be invoked for changes that were done before it is added.
|
||||
* <p>
|
||||
* If the observer already exists, this is a no-op call.
|
||||
* <p>
|
||||
* If one of the tables in the Observer does not exist in the database, this method throws an
|
||||
* {@link IllegalArgumentException}.
|
||||
* <p>
|
||||
* This method should be called on a background/worker thread as it performs database
|
||||
* operations.
|
||||
*
|
||||
* @param observer The observer which listens the database for changes.
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
@WorkerThread
|
||||
public void addObserver(@NonNull Observer observer) {
|
||||
final String[] tableNames = resolveViews(observer.mTables);
|
||||
int[] tableIds = new int[tableNames.length];
|
||||
final int size = tableNames.length;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
|
||||
if (tableId == null) {
|
||||
throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
|
||||
}
|
||||
tableIds[i] = tableId;
|
||||
}
|
||||
ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);
|
||||
ObserverWrapper currentObserver;
|
||||
synchronized (mObserverMap) {
|
||||
currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
|
||||
}
|
||||
if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
|
||||
syncTriggers();
|
||||
}
|
||||
}
|
||||
|
||||
private String[] validateAndResolveTableNames(String[] tableNames) {
|
||||
String[] resolved = resolveViews(tableNames);
|
||||
for (String tableName : resolved) {
|
||||
if (!mTableIdLookup.containsKey(tableName.toLowerCase(Locale.US))) {
|
||||
throw new IllegalArgumentException("There is no table with name " + tableName);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the list of tables and views into a list of unique tables that are underlying them.
|
||||
*
|
||||
* @param names The names of tables or views.
|
||||
* @return The names of the underlying tables.
|
||||
*/
|
||||
private String[] resolveViews(String[] names) {
|
||||
Set<String> tables = new HashSet<>();
|
||||
for (String name : names) {
|
||||
final String lowercase = name.toLowerCase(Locale.US);
|
||||
if (mViewTables.containsKey(lowercase)) {
|
||||
tables.addAll(mViewTables.get(lowercase));
|
||||
} else {
|
||||
tables.add(name);
|
||||
}
|
||||
}
|
||||
return tables.toArray(new String[tables.size()]);
|
||||
}
|
||||
|
||||
private static void beginTransactionInternal(SupportSQLiteDatabase database) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
|
||||
&& database.isWriteAheadLoggingEnabled()) {
|
||||
database.beginTransactionNonExclusive();
|
||||
} else {
|
||||
database.beginTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an observer but keeps a weak reference back to it.
|
||||
* <p>
|
||||
* Note that you cannot remove this observer once added. It will be automatically removed
|
||||
* when the observer is GC'ed.
|
||||
*
|
||||
* @param observer The observer to which InvalidationTracker will keep a weak reference.
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public void addWeakObserver(Observer observer) {
|
||||
addObserver(new WeakObserver(this, observer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the observer from the observers list.
|
||||
* <p>
|
||||
* This method should be called on a background/worker thread as it performs database
|
||||
* operations.
|
||||
*
|
||||
* @param observer The observer to remove.
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@WorkerThread
|
||||
public void removeObserver(@NonNull final Observer observer) {
|
||||
ObserverWrapper wrapper;
|
||||
synchronized (mObserverMap) {
|
||||
wrapper = mObserverMap.remove(observer);
|
||||
}
|
||||
if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
|
||||
syncTriggers();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic access */
|
||||
boolean ensureInitialization() {
|
||||
if (!mDatabase.isOpen()) {
|
||||
return false;
|
||||
}
|
||||
if (!mInitialized) {
|
||||
// trigger initialization
|
||||
mDatabase.getOpenHelper().getWritableDatabase();
|
||||
}
|
||||
if (!mInitialized) {
|
||||
Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Runnable mRefreshRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final Lock closeLock = mDatabase.getCloseLock();
|
||||
Set<Integer> invalidatedTableIds = null;
|
||||
closeLock.lock();
|
||||
try {
|
||||
|
||||
if (!ensureInitialization()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mPendingRefresh.compareAndSet(true, false)) {
|
||||
// no pending refresh
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDatabase.inTransaction()) {
|
||||
// current thread is in a transaction. when it ends, it will invoke
|
||||
// refreshRunnable again. mPendingRefresh is left as false on purpose
|
||||
// so that the last transaction can flip it on again.
|
||||
return;
|
||||
}
|
||||
|
||||
// This transaction has to be on the underlying DB rather than the RoomDatabase
|
||||
// in order to avoid a recursive loop after endTransaction.
|
||||
SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
|
||||
db.beginTransactionNonExclusive();
|
||||
try {
|
||||
invalidatedTableIds = checkUpdatedTable();
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
} catch (IllegalStateException | SQLiteException exception) {
|
||||
// may happen if db is closed. just log.
|
||||
Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
|
||||
exception);
|
||||
} finally {
|
||||
closeLock.unlock();
|
||||
|
||||
if (mAutoCloser != null) {
|
||||
mAutoCloser.decrementCountAndScheduleClose();
|
||||
}
|
||||
}
|
||||
if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
|
||||
synchronized (mObserverMap) {
|
||||
for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
|
||||
entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Integer> checkUpdatedTable() {
|
||||
HashSet<Integer> invalidatedTableIds = new HashSet<>();
|
||||
Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
final int tableId = cursor.getInt(0);
|
||||
invalidatedTableIds.add(tableId);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
eu.faircode.email.Log.w(ex);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
if (!invalidatedTableIds.isEmpty()) {
|
||||
mCleanupStatement.executeUpdateDelete();
|
||||
}
|
||||
return invalidatedTableIds;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Enqueues a task to refresh the list of updated tables.
|
||||
* <p>
|
||||
* This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
|
||||
* if you have another connection to the database or directly use {@link
|
||||
* SupportSQLiteDatabase}, you may need to call this manually.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public void refreshVersionsAsync() {
|
||||
// TODO we should consider doing this sync instead of async.
|
||||
if (mPendingRefresh.compareAndSet(false, true)) {
|
||||
if (mAutoCloser != null) {
|
||||
// refreshVersionsAsync is called with the ref count incremented from
|
||||
// RoomDatabase, so the db can't be closed here, but we need to be sure that our
|
||||
// db isn't closed until refresh is completed. This increment call must be
|
||||
// matched with a corresponding call in mRefreshRunnable.
|
||||
mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
}
|
||||
mDatabase.getQueryExecutor().execute(mRefreshRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check versions for tables, and run observers synchronously if tables have been updated.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@WorkerThread
|
||||
public void refreshVersionsSync() {
|
||||
if (mAutoCloser != null) {
|
||||
// This increment call must be matched with a corresponding call in mRefreshRunnable.
|
||||
mAutoCloser.incrementCountAndEnsureDbIsOpen();
|
||||
}
|
||||
syncTriggers();
|
||||
mRefreshRunnable.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all the registered {@link Observer}s of table changes.
|
||||
* <p>
|
||||
* This can be used for notifying invalidation that cannot be detected by this
|
||||
* {@link InvalidationTracker}, for example, invalidation from another process.
|
||||
*
|
||||
* @param tables The invalidated tables.
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
|
||||
public void notifyObserversByTableNames(String... tables) {
|
||||
synchronized (mObserverMap) {
|
||||
for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
|
||||
if (!entry.getKey().isRemote()) {
|
||||
entry.getValue().notifyByTableNames(tables);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void syncTriggers(SupportSQLiteDatabase database) {
|
||||
if (database.inTransaction()) {
|
||||
// we won't run this inside another transaction.
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Lock closeLock = mDatabase.getCloseLock();
|
||||
closeLock.lock();
|
||||
try {
|
||||
// Serialize adding and removing table trackers, this is specifically important
|
||||
// to avoid missing invalidation before a transaction starts but there are
|
||||
// pending (possibly concurrent) observer changes.
|
||||
synchronized (mSyncTriggersLock) {
|
||||
final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
|
||||
if (tablesToSync == null) {
|
||||
return;
|
||||
}
|
||||
final int limit = tablesToSync.length;
|
||||
beginTransactionInternal(database);
|
||||
try {
|
||||
for (int tableId = 0; tableId < limit; tableId++) {
|
||||
switch (tablesToSync[tableId]) {
|
||||
case ObservedTableTracker.ADD:
|
||||
startTrackingTable(database, tableId);
|
||||
break;
|
||||
case ObservedTableTracker.REMOVE:
|
||||
stopTrackingTable(database, tableId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
database.setTransactionSuccessful();
|
||||
} finally {
|
||||
database.endTransaction();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
closeLock.unlock();
|
||||
}
|
||||
} catch (IllegalStateException | SQLiteException exception) {
|
||||
// may happen if db is closed. just log.
|
||||
Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by RoomDatabase before each beginTransaction call.
|
||||
* <p>
|
||||
* It is important that pending trigger changes are applied to the database before any query
|
||||
* runs. Otherwise, we may miss some changes.
|
||||
* <p>
|
||||
* This api should eventually be public.
|
||||
*/
|
||||
void syncTriggers() {
|
||||
if (!mDatabase.isOpen()) {
|
||||
return;
|
||||
}
|
||||
syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LiveData that computes the given function once and for every other invalidation
|
||||
* of the database.
|
||||
* <p>
|
||||
* Holds a strong reference to the created LiveData as long as it is active.
|
||||
*
|
||||
* @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
|
||||
*
|
||||
* @param computeFunction The function that calculates the value
|
||||
* @param tableNames The list of tables to observe
|
||||
* @param <T> The return type
|
||||
* @return A new LiveData that computes the given function when the given list of tables
|
||||
* invalidates.
|
||||
* @hide
|
||||
*/
|
||||
@Deprecated
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public <T> LiveData<T> createLiveData(String[] tableNames, Callable<T> computeFunction) {
|
||||
return createLiveData(tableNames, false, computeFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LiveData that computes the given function once and for every other invalidation
|
||||
* of the database.
|
||||
* <p>
|
||||
* Holds a strong reference to the created LiveData as long as it is active.
|
||||
*
|
||||
* @param tableNames The list of tables to observe
|
||||
* @param inTransaction True if the computeFunction will be done in a transaction, false
|
||||
* otherwise.
|
||||
* @param computeFunction The function that calculates the value
|
||||
* @param <T> The return type
|
||||
* @return A new LiveData that computes the given function when the given list of tables
|
||||
* invalidates.
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
|
||||
Callable<T> computeFunction) {
|
||||
return mInvalidationLiveDataContainer.create(
|
||||
validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an observer and keeps the table information.
|
||||
* <p>
|
||||
* Internally table ids are used which may change from database to database so the table
|
||||
* related information is kept here rather than in the Observer.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
static class ObserverWrapper {
|
||||
final int[] mTableIds;
|
||||
private final String[] mTableNames;
|
||||
final Observer mObserver;
|
||||
private final Set<String> mSingleTableSet;
|
||||
|
||||
ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) {
|
||||
mObserver = observer;
|
||||
mTableIds = tableIds;
|
||||
mTableNames = tableNames;
|
||||
if (tableIds.length == 1) {
|
||||
HashSet<String> set = new HashSet<>();
|
||||
set.add(mTableNames[0]);
|
||||
mSingleTableSet = Collections.unmodifiableSet(set);
|
||||
} else {
|
||||
mSingleTableSet = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the underlying {@link #mObserver} if any of the observed tables are invalidated
|
||||
* based on the given invalid status set.
|
||||
*
|
||||
* @param invalidatedTablesIds The table ids of the tables that are invalidated.
|
||||
*/
|
||||
void notifyByTableInvalidStatus(Set<Integer> invalidatedTablesIds) {
|
||||
Set<String> invalidatedTables = null;
|
||||
final int size = mTableIds.length;
|
||||
for (int index = 0; index < size; index++) {
|
||||
final int tableId = mTableIds[index];
|
||||
if (invalidatedTablesIds.contains(tableId)) {
|
||||
if (size == 1) {
|
||||
// Optimization for a single-table observer
|
||||
invalidatedTables = mSingleTableSet;
|
||||
} else {
|
||||
if (invalidatedTables == null) {
|
||||
invalidatedTables = new HashSet<>(size);
|
||||
}
|
||||
invalidatedTables.add(mTableNames[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (invalidatedTables != null) {
|
||||
mObserver.onInvalidated(invalidatedTables);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the underlying {@link #mObserver} if it observes any of the specified
|
||||
* {@code tables}.
|
||||
*
|
||||
* @param tables The invalidated table names.
|
||||
*/
|
||||
void notifyByTableNames(String[] tables) {
|
||||
Set<String> invalidatedTables = null;
|
||||
if (mTableNames.length == 1) {
|
||||
for (String table : tables) {
|
||||
if (table.equalsIgnoreCase(mTableNames[0])) {
|
||||
// Optimization for a single-table observer
|
||||
invalidatedTables = mSingleTableSet;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HashSet<String> set = new HashSet<>();
|
||||
for (String table : tables) {
|
||||
for (String ourTable : mTableNames) {
|
||||
if (ourTable.equalsIgnoreCase(table)) {
|
||||
set.add(ourTable);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (set.size() > 0) {
|
||||
invalidatedTables = set;
|
||||
}
|
||||
}
|
||||
if (invalidatedTables != null) {
|
||||
mObserver.onInvalidated(invalidatedTables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An observer that can listen for changes in the database.
|
||||
*/
|
||||
public abstract static class Observer {
|
||||
final String[] mTables;
|
||||
|
||||
/**
|
||||
* Observes the given list of tables and views.
|
||||
*
|
||||
* @param firstTable The name of the table or view.
|
||||
* @param rest More names of tables or views.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
protected Observer(@NonNull String firstTable, String... rest) {
|
||||
mTables = Arrays.copyOf(rest, rest.length + 1);
|
||||
mTables[rest.length] = firstTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes the given list of tables and views.
|
||||
*
|
||||
* @param tables The list of tables or views to observe for changes.
|
||||
*/
|
||||
public Observer(@NonNull String[] tables) {
|
||||
// copy tables in case user modifies them afterwards
|
||||
mTables = Arrays.copyOf(tables, tables.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the observed tables is invalidated in the database.
|
||||
*
|
||||
* @param tables A set of invalidated tables. This is useful when the observer targets
|
||||
* multiple tables and you want to know which table is invalidated. This will
|
||||
* be names of underlying tables when you are observing views.
|
||||
*/
|
||||
public abstract void onInvalidated(@NonNull Set<String> tables);
|
||||
|
||||
boolean isRemote() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
|
||||
* triggers in the database.
|
||||
* <p>
|
||||
* This class is thread safe
|
||||
*/
|
||||
static class ObservedTableTracker {
|
||||
static final int NO_OP = 0; // don't change trigger state for this table
|
||||
static final int ADD = 1; // add triggers for this table
|
||||
static final int REMOVE = 2; // remove triggers for this table
|
||||
|
||||
// number of observers per table
|
||||
final long[] mTableObservers;
|
||||
// trigger state for each table at last sync
|
||||
// this field is updated when syncAndGet is called.
|
||||
final boolean[] mTriggerStates;
|
||||
// when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
|
||||
final int[] mTriggerStateChanges;
|
||||
|
||||
boolean mNeedsSync;
|
||||
|
||||
ObservedTableTracker(int tableCount) {
|
||||
mTableObservers = new long[tableCount];
|
||||
mTriggerStates = new boolean[tableCount];
|
||||
mTriggerStateChanges = new int[tableCount];
|
||||
Arrays.fill(mTableObservers, 0);
|
||||
Arrays.fill(mTriggerStates, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if # of triggers is affected.
|
||||
*/
|
||||
boolean onAdded(int... tableIds) {
|
||||
boolean needTriggerSync = false;
|
||||
synchronized (this) {
|
||||
for (int tableId : tableIds) {
|
||||
final long prevObserverCount = mTableObservers[tableId];
|
||||
mTableObservers[tableId] = prevObserverCount + 1;
|
||||
if (prevObserverCount == 0) {
|
||||
mNeedsSync = true;
|
||||
needTriggerSync = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return needTriggerSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if # of triggers is affected.
|
||||
*/
|
||||
boolean onRemoved(int... tableIds) {
|
||||
boolean needTriggerSync = false;
|
||||
synchronized (this) {
|
||||
for (int tableId : tableIds) {
|
||||
final long prevObserverCount = mTableObservers[tableId];
|
||||
mTableObservers[tableId] = prevObserverCount - 1;
|
||||
if (prevObserverCount == 1) {
|
||||
mNeedsSync = true;
|
||||
needTriggerSync = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return needTriggerSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are re-opening the db we'll need to add all the triggers that we need so change
|
||||
* the current state to false for all.
|
||||
*/
|
||||
void resetTriggerState() {
|
||||
synchronized (this) {
|
||||
Arrays.fill(mTriggerStates, false);
|
||||
mNeedsSync = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this returns non-null there are no pending sync operations.
|
||||
*
|
||||
* @return int[] An int array where the index for each tableId has the action for that
|
||||
* table.
|
||||
*/
|
||||
@Nullable
|
||||
int[] getTablesToSync() {
|
||||
synchronized (this) {
|
||||
if (!mNeedsSync) {
|
||||
return null;
|
||||
}
|
||||
final int tableCount = mTableObservers.length;
|
||||
for (int i = 0; i < tableCount; i++) {
|
||||
final boolean newState = mTableObservers[i] > 0;
|
||||
if (newState != mTriggerStates[i]) {
|
||||
mTriggerStateChanges[i] = newState ? ADD : REMOVE;
|
||||
} else {
|
||||
mTriggerStateChanges[i] = NO_OP;
|
||||
}
|
||||
mTriggerStates[i] = newState;
|
||||
}
|
||||
mNeedsSync = false;
|
||||
return mTriggerStateChanges.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An Observer wrapper that keeps a weak reference to the given object.
|
||||
* <p>
|
||||
* This class will automatically unsubscribe when the wrapped observer goes out of memory.
|
||||
*/
|
||||
static class WeakObserver extends Observer {
|
||||
final InvalidationTracker mTracker;
|
||||
final WeakReference<Observer> mDelegateRef;
|
||||
|
||||
WeakObserver(InvalidationTracker tracker, Observer delegate) {
|
||||
super(delegate.mTables);
|
||||
mTracker = tracker;
|
||||
mDelegateRef = new WeakReference<>(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidated(@NonNull Set<String> tables) {
|
||||
final Observer observer = mDelegateRef.get();
|
||||
if (observer == null) {
|
||||
mTracker.removeObserver(this);
|
||||
} else {
|
||||
observer.onInvalidated(tables);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,839 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.sqlite.SQLiteException
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.GuardedBy
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.arch.core.internal.SafeIterableMap
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Room.LOG_TAG
|
||||
import androidx.room.util.useCursor
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteStatement
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Arrays
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
|
||||
* these tables.
|
||||
*/
|
||||
// Some details on how the InvalidationTracker works:
|
||||
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
|
||||
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
|
||||
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
|
||||
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
|
||||
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
|
||||
// tables.
|
||||
// * Each update (write operation) on one of the observed tables triggers an update into the
|
||||
// memory table table, flipping the invalidated flag ON.
|
||||
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
|
||||
// It works as an Observer, and notifies other instances of table invalidation.
|
||||
open class InvalidationTracker @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) constructor(
|
||||
internal val database: RoomDatabase,
|
||||
private val shadowTablesMap: Map<String, String>,
|
||||
private val viewTables: Map<String, @JvmSuppressWildcards Set<String>>,
|
||||
vararg tableNames: String
|
||||
) {
|
||||
internal val tableIdLookup: Map<String, Int>
|
||||
internal val tablesNames: Array<out String>
|
||||
|
||||
private var autoCloser: AutoCloser? = null
|
||||
|
||||
@get:RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
@field:RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
val pendingRefresh = AtomicBoolean(false)
|
||||
|
||||
@Volatile
|
||||
private var initialized = false
|
||||
|
||||
@Volatile
|
||||
internal var cleanupStatement: SupportSQLiteStatement? = null
|
||||
|
||||
private val observedTableTracker: ObservedTableTracker = ObservedTableTracker(tableNames.size)
|
||||
|
||||
private val invalidationLiveDataContainer: InvalidationLiveDataContainer =
|
||||
InvalidationLiveDataContainer(database)
|
||||
|
||||
@GuardedBy("observerMap")
|
||||
internal val observerMap = SafeIterableMap<Observer, ObserverWrapper>()
|
||||
|
||||
private var multiInstanceInvalidationClient: MultiInstanceInvalidationClient? = null
|
||||
|
||||
private val syncTriggersLock = Any()
|
||||
|
||||
private val trackerLock = Any()
|
||||
|
||||
init {
|
||||
tableIdLookup = mutableMapOf()
|
||||
tablesNames = Array(tableNames.size) { id ->
|
||||
val tableName = tableNames[id].lowercase(Locale.US)
|
||||
tableIdLookup[tableName] = id
|
||||
val shadowTableName = shadowTablesMap[tableNames[id]]?.lowercase(Locale.US)
|
||||
shadowTableName ?: tableName
|
||||
}
|
||||
|
||||
// Adjust table id lookup for those tables whose shadow table is another already mapped
|
||||
// table (e.g. external content fts tables).
|
||||
shadowTablesMap.forEach { entry ->
|
||||
val shadowTableName = entry.value.lowercase(Locale.US)
|
||||
if (tableIdLookup.containsKey(shadowTableName)) {
|
||||
val tableName = entry.key.lowercase(Locale.US)
|
||||
tableIdLookup[tableName] = tableIdLookup.getValue(shadowTableName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by the generated code.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
constructor(database: RoomDatabase, vararg tableNames: String) :
|
||||
this(
|
||||
database = database,
|
||||
shadowTablesMap = emptyMap(),
|
||||
viewTables = emptyMap(),
|
||||
tableNames = tableNames
|
||||
)
|
||||
|
||||
/**
|
||||
* Sets the auto closer for this invalidation tracker so that the invalidation tracker can
|
||||
* ensure that the database is not closed if there are pending invalidations that haven't yet
|
||||
* been flushed.
|
||||
*
|
||||
* This also adds a callback to the autocloser to ensure that the InvalidationTracker is in
|
||||
* an ok state once the table is invalidated.
|
||||
*
|
||||
* This must be called before the database is used.
|
||||
*
|
||||
* @param autoCloser the autocloser associated with the db
|
||||
*/
|
||||
internal fun setAutoCloser(autoCloser: AutoCloser) {
|
||||
this.autoCloser = autoCloser
|
||||
autoCloser.setAutoCloseCallback(::onAutoCloseCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to initialize table tracking.
|
||||
*
|
||||
* You should never call this method, it is called by the generated code.
|
||||
*/
|
||||
internal fun internalInit(database: SupportSQLiteDatabase) {
|
||||
synchronized(trackerLock) {
|
||||
if (initialized) {
|
||||
Log.e(LOG_TAG, "Invalidation tracker is initialized twice :/.")
|
||||
return
|
||||
}
|
||||
|
||||
// These actions are not in a transaction because temp_store is not allowed to be
|
||||
// performed on a transaction, and recursive_triggers is not affected by transactions.
|
||||
database.execSQL("PRAGMA temp_store = MEMORY;")
|
||||
database.execSQL("PRAGMA recursive_triggers='ON';")
|
||||
database.execSQL(CREATE_TRACKING_TABLE_SQL)
|
||||
syncTriggers(database)
|
||||
cleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL)
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAutoCloseCallback() {
|
||||
synchronized(trackerLock) {
|
||||
initialized = false
|
||||
observedTableTracker.resetTriggerState()
|
||||
cleanupStatement?.close()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun startMultiInstanceInvalidation(
|
||||
context: Context,
|
||||
name: String,
|
||||
serviceIntent: Intent
|
||||
) {
|
||||
multiInstanceInvalidationClient = MultiInstanceInvalidationClient(
|
||||
context = context,
|
||||
name = name,
|
||||
serviceIntent = serviceIntent,
|
||||
invalidationTracker = this,
|
||||
executor = database.queryExecutor
|
||||
)
|
||||
}
|
||||
|
||||
internal fun stopMultiInstanceInvalidation() {
|
||||
multiInstanceInvalidationClient?.stop()
|
||||
multiInstanceInvalidationClient = null
|
||||
}
|
||||
|
||||
private fun stopTrackingTable(db: SupportSQLiteDatabase, tableId: Int) {
|
||||
val tableName = tablesNames[tableId]
|
||||
for (trigger in TRIGGERS) {
|
||||
val sql = buildString {
|
||||
append("DROP TRIGGER IF EXISTS ")
|
||||
append(getTriggerName(tableName, trigger))
|
||||
}
|
||||
db.execSQL(sql)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTrackingTable(db: SupportSQLiteDatabase, tableId: Int) {
|
||||
db.execSQL(
|
||||
"INSERT OR IGNORE INTO $UPDATE_TABLE_NAME VALUES($tableId, 0)"
|
||||
)
|
||||
val tableName = tablesNames[tableId]
|
||||
for (trigger in TRIGGERS) {
|
||||
val sql = buildString {
|
||||
append("CREATE TEMP TRIGGER IF NOT EXISTS ")
|
||||
append(getTriggerName(tableName, trigger))
|
||||
append(" AFTER ")
|
||||
append(trigger)
|
||||
append(" ON `")
|
||||
append(tableName)
|
||||
append("` BEGIN UPDATE ")
|
||||
append(UPDATE_TABLE_NAME)
|
||||
append(" SET ").append(INVALIDATED_COLUMN_NAME)
|
||||
append(" = 1")
|
||||
append(" WHERE ").append(TABLE_ID_COLUMN_NAME)
|
||||
append(" = ").append(tableId)
|
||||
append(" AND ").append(INVALIDATED_COLUMN_NAME)
|
||||
append(" = 0")
|
||||
append("; END")
|
||||
}
|
||||
db.execSQL(sql)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given observer to the observers list and it will be notified if any table it
|
||||
* observes changes.
|
||||
*
|
||||
* Database changes are pulled on another thread so in some race conditions, the observer might
|
||||
* be invoked for changes that were done before it is added.
|
||||
*
|
||||
* If the observer already exists, this is a no-op call.
|
||||
*
|
||||
* If one of the tables in the Observer does not exist in the database, this method throws an
|
||||
* [IllegalArgumentException].
|
||||
*
|
||||
* This method should be called on a background/worker thread as it performs database
|
||||
* operations.
|
||||
*
|
||||
* @param observer The observer which listens the database for changes.
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
@WorkerThread
|
||||
open fun addObserver(observer: Observer) {
|
||||
val tableNames = resolveViews(observer.tables)
|
||||
val tableIds = tableNames.map { tableName ->
|
||||
tableIdLookup[tableName.lowercase(Locale.US)]
|
||||
?: throw IllegalArgumentException("There is no table with name $tableName")
|
||||
}.toIntArray()
|
||||
|
||||
val wrapper = ObserverWrapper(
|
||||
observer = observer,
|
||||
tableIds = tableIds,
|
||||
tableNames = tableNames
|
||||
)
|
||||
|
||||
val currentObserver = synchronized(observerMap) {
|
||||
observerMap.putIfAbsent(observer, wrapper)
|
||||
}
|
||||
if (currentObserver == null && observedTableTracker.onAdded(*tableIds)) {
|
||||
syncTriggers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateAndResolveTableNames(tableNames: Array<out String>): Array<out String> {
|
||||
val resolved = resolveViews(tableNames)
|
||||
resolved.forEach { tableName ->
|
||||
require(tableIdLookup.containsKey(tableName.lowercase(Locale.US))) {
|
||||
"There is no table with name $tableName"
|
||||
}
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the list of tables and views into a list of unique tables that are underlying them.
|
||||
*
|
||||
* @param names The names of tables or views.
|
||||
* @return The names of the underlying tables.
|
||||
*/
|
||||
private fun resolveViews(names: Array<out String>): Array<out String> {
|
||||
return buildSet {
|
||||
names.forEach { name ->
|
||||
if (viewTables.containsKey(name.lowercase(Locale.US))) {
|
||||
addAll(viewTables[name.lowercase(Locale.US)]!!)
|
||||
} else {
|
||||
add(name)
|
||||
}
|
||||
}
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an observer but keeps a weak reference back to it.
|
||||
*
|
||||
* Note that you cannot remove this observer once added. It will be automatically removed
|
||||
* when the observer is GC'ed.
|
||||
*
|
||||
* @param observer The observer to which InvalidationTracker will keep a weak reference.
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
open fun addWeakObserver(observer: Observer) {
|
||||
addObserver(WeakObserver(this, observer))
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the observer from the observers list.
|
||||
*
|
||||
* This method should be called on a background/worker thread as it performs database
|
||||
* operations.
|
||||
*
|
||||
* @param observer The observer to remove.
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
@WorkerThread
|
||||
open fun removeObserver(observer: Observer) {
|
||||
val wrapper = synchronized(observerMap) {
|
||||
observerMap.remove(observer)
|
||||
}
|
||||
if (wrapper != null && observedTableTracker.onRemoved(tableIds = wrapper.tableIds)) {
|
||||
syncTriggers()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ensureInitialization(): Boolean {
|
||||
if (!database.isOpenInternal) {
|
||||
return false
|
||||
}
|
||||
if (!initialized) {
|
||||
// trigger initialization
|
||||
database.openHelper.writableDatabase
|
||||
}
|
||||
if (!initialized) {
|
||||
Log.e(LOG_TAG, "database is not initialized even though it is open")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@JvmField
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
val refreshRunnable: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
val closeLock = database.getCloseLock()
|
||||
closeLock.lock()
|
||||
val invalidatedTableIds: Set<Int> =
|
||||
try {
|
||||
if (!ensureInitialization()) {
|
||||
return
|
||||
}
|
||||
if (!pendingRefresh.compareAndSet(true, false)) {
|
||||
// no pending refresh
|
||||
return
|
||||
}
|
||||
if (database.inTransaction()) {
|
||||
// current thread is in a transaction. when it ends, it will invoke
|
||||
// refreshRunnable again. pendingRefresh is left as false on purpose
|
||||
// so that the last transaction can flip it on again.
|
||||
return
|
||||
}
|
||||
|
||||
// This transaction has to be on the underlying DB rather than the RoomDatabase
|
||||
// in order to avoid a recursive loop after endTransaction.
|
||||
val db = database.openHelper.writableDatabase
|
||||
db.beginTransactionNonExclusive()
|
||||
val invalidatedTableIds: Set<Int>
|
||||
try {
|
||||
invalidatedTableIds = checkUpdatedTable()
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
invalidatedTableIds
|
||||
} catch (ex: IllegalStateException) {
|
||||
// may happen if db is closed. just log.
|
||||
Log.e(
|
||||
LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
|
||||
ex
|
||||
)
|
||||
emptySet()
|
||||
} catch (ex: SQLiteException) {
|
||||
Log.e(
|
||||
LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
|
||||
ex
|
||||
)
|
||||
emptySet()
|
||||
} finally {
|
||||
closeLock.unlock()
|
||||
autoCloser?.decrementCountAndScheduleClose()
|
||||
}
|
||||
|
||||
if (invalidatedTableIds.isNotEmpty()) {
|
||||
synchronized(observerMap) {
|
||||
observerMap.forEach {
|
||||
it.value.notifyByTableInvalidStatus(invalidatedTableIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkUpdatedTable(): Set<Int> {
|
||||
val invalidatedTableIds = buildSet {
|
||||
database.query(SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL)).useCursor { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
add(cursor.getInt(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (invalidatedTableIds.isNotEmpty()) {
|
||||
checkNotNull(cleanupStatement)
|
||||
val statement = cleanupStatement
|
||||
requireNotNull(statement)
|
||||
statement.executeUpdateDelete()
|
||||
}
|
||||
return invalidatedTableIds
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues a task to refresh the list of updated tables.
|
||||
*
|
||||
* This method is automatically called when [RoomDatabase.endTransaction] is called but
|
||||
* if you have another connection to the database or directly use [ ], you may need to call this
|
||||
* manually.
|
||||
*/
|
||||
open fun refreshVersionsAsync() {
|
||||
// TODO we should consider doing this sync instead of async.
|
||||
if (pendingRefresh.compareAndSet(false, true)) {
|
||||
// refreshVersionsAsync is called with the ref count incremented from
|
||||
// RoomDatabase, so the db can't be closed here, but we need to be sure that our
|
||||
// db isn't closed until refresh is completed. This increment call must be
|
||||
// matched with a corresponding call in refreshRunnable.
|
||||
autoCloser?.incrementCountAndEnsureDbIsOpen()
|
||||
database.queryExecutor.execute(refreshRunnable)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check versions for tables, and run observers synchronously if tables have been updated.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@WorkerThread
|
||||
open fun refreshVersionsSync() {
|
||||
// This increment call must be matched with a corresponding call in refreshRunnable.
|
||||
autoCloser?.incrementCountAndEnsureDbIsOpen()
|
||||
syncTriggers()
|
||||
refreshRunnable.run()
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all the registered [Observer]s of table changes.
|
||||
*
|
||||
* This can be used for notifying invalidation that cannot be detected by this
|
||||
* [InvalidationTracker], for example, invalidation from another process.
|
||||
*
|
||||
* @param tables The invalidated tables.
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
fun notifyObserversByTableNames(vararg tables: String) {
|
||||
synchronized(observerMap) {
|
||||
observerMap.forEach { (observer, wrapper) ->
|
||||
if (!observer.isRemote) {
|
||||
wrapper.notifyByTableNames(tables)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun syncTriggers(database: SupportSQLiteDatabase) {
|
||||
if (database.inTransaction()) {
|
||||
// we won't run this inside another transaction.
|
||||
return
|
||||
}
|
||||
try {
|
||||
val closeLock = this.database.getCloseLock()
|
||||
closeLock.lock()
|
||||
try {
|
||||
// Serialize adding and removing table trackers, this is specifically important
|
||||
// to avoid missing invalidation before a transaction starts but there are
|
||||
// pending (possibly concurrent) observer changes.
|
||||
synchronized(syncTriggersLock) {
|
||||
val tablesToSync = observedTableTracker.getTablesToSync() ?: return
|
||||
beginTransactionInternal(database)
|
||||
try {
|
||||
tablesToSync.forEachIndexed { tableId, syncState ->
|
||||
when (syncState) {
|
||||
ObservedTableTracker.ADD ->
|
||||
startTrackingTable(database, tableId)
|
||||
ObservedTableTracker.REMOVE ->
|
||||
stopTrackingTable(database, tableId)
|
||||
}
|
||||
}
|
||||
database.setTransactionSuccessful()
|
||||
} finally {
|
||||
database.endTransaction()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
closeLock.unlock()
|
||||
}
|
||||
} catch (ex: IllegalStateException) {
|
||||
// may happen if db is closed. just log.
|
||||
Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex)
|
||||
} catch (ex: SQLiteException) {
|
||||
Log.e(LOG_TAG, "Cannot run invalidation tracker. Is the db closed?", ex)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by RoomDatabase before each beginTransaction call.
|
||||
*
|
||||
* It is important that pending trigger changes are applied to the database before any query
|
||||
* runs. Otherwise, we may miss some changes.
|
||||
*
|
||||
* This api should eventually be public.
|
||||
*/
|
||||
internal fun syncTriggers() {
|
||||
if (!database.isOpenInternal) {
|
||||
return
|
||||
}
|
||||
syncTriggers(database.openHelper.writableDatabase)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LiveData that computes the given function once and for every other invalidation
|
||||
* of the database.
|
||||
*
|
||||
* Holds a strong reference to the created LiveData as long as it is active.
|
||||
*
|
||||
* @param computeFunction The function that calculates the value
|
||||
* @param tableNames The list of tables to observe
|
||||
* @param T The return type
|
||||
* @return A new LiveData that computes the given function when the given list of tables
|
||||
* invalidates.
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@Deprecated("Use [createLiveData(String[], boolean, Callable)]")
|
||||
open fun <T> createLiveData(
|
||||
tableNames: Array<out String>,
|
||||
computeFunction: Callable<T?>
|
||||
): LiveData<T> {
|
||||
return createLiveData(tableNames, false, computeFunction)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a LiveData that computes the given function once and for every other invalidation
|
||||
* of the database.
|
||||
*
|
||||
* Holds a strong reference to the created LiveData as long as it is active.
|
||||
*
|
||||
* @param tableNames The list of tables to observe
|
||||
* @param inTransaction True if the computeFunction will be done in a transaction, false
|
||||
* otherwise.
|
||||
* @param computeFunction The function that calculates the value
|
||||
* @param T The return type
|
||||
* @return A new LiveData that computes the given function when the given list of tables
|
||||
* invalidates.
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
open fun <T> createLiveData(
|
||||
tableNames: Array<out String>,
|
||||
inTransaction: Boolean,
|
||||
computeFunction: Callable<T?>
|
||||
): LiveData<T> {
|
||||
return invalidationLiveDataContainer.create(
|
||||
validateAndResolveTableNames(tableNames), inTransaction, computeFunction
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an observer and keeps the table information.
|
||||
*
|
||||
* Internally table ids are used which may change from database to database so the table
|
||||
* related information is kept here rather than in the Observer.
|
||||
*/
|
||||
internal class ObserverWrapper(
|
||||
internal val observer: Observer,
|
||||
internal val tableIds: IntArray,
|
||||
private val tableNames: Array<out String>
|
||||
) {
|
||||
private val singleTableSet = if (tableNames.isNotEmpty()) {
|
||||
setOf(tableNames[0])
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
init {
|
||||
check(tableIds.size == tableNames.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the underlying [.mObserver] if any of the observed tables are invalidated
|
||||
* based on the given invalid status set.
|
||||
*
|
||||
* @param invalidatedTablesIds The table ids of the tables that are invalidated.
|
||||
*/
|
||||
internal fun notifyByTableInvalidStatus(invalidatedTablesIds: Set<Int?>) {
|
||||
val invalidatedTables = when (tableIds.size) {
|
||||
0 -> emptySet()
|
||||
1 -> if (invalidatedTablesIds.contains(tableIds[0])) {
|
||||
singleTableSet // Optimization for a single-table observer
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
else -> buildSet {
|
||||
tableIds.forEachIndexed { idx, tableId ->
|
||||
if (invalidatedTablesIds.contains(tableId)) {
|
||||
add(tableNames[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidatedTables.isNotEmpty()) {
|
||||
observer.onInvalidated(invalidatedTables)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the underlying [.mObserver] if it observes any of the specified
|
||||
* `tables`.
|
||||
*
|
||||
* @param tables The invalidated table names.
|
||||
*/
|
||||
internal fun notifyByTableNames(tables: Array<out String>) {
|
||||
val invalidatedTables = when (tableNames.size) {
|
||||
0 -> emptySet()
|
||||
1 -> if (tables.any { it.equals(tableNames[0], ignoreCase = true) }) {
|
||||
singleTableSet // Optimization for a single-table observer
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
else -> buildSet {
|
||||
tables.forEach { table ->
|
||||
tableNames.forEach ourTablesLoop@{ ourTable ->
|
||||
if (ourTable.equals(table, ignoreCase = true)) {
|
||||
add(ourTable)
|
||||
return@ourTablesLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (invalidatedTables.isNotEmpty()) {
|
||||
observer.onInvalidated(invalidatedTables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An observer that can listen for changes in the database.
|
||||
*/
|
||||
abstract class Observer(internal val tables: Array<out String>) {
|
||||
/**
|
||||
* Observes the given list of tables and views.
|
||||
*
|
||||
* @param firstTable The name of the table or view.
|
||||
* @param rest More names of tables or views.
|
||||
*/
|
||||
protected constructor(firstTable: String, vararg rest: String) : this(
|
||||
buildList {
|
||||
addAll(rest)
|
||||
add(firstTable)
|
||||
}.toTypedArray()
|
||||
)
|
||||
|
||||
/**
|
||||
* Called when one of the observed tables is invalidated in the database.
|
||||
*
|
||||
* @param tables A set of invalidated tables. This is useful when the observer targets
|
||||
* multiple tables and you want to know which table is invalidated. This will
|
||||
* be names of underlying tables when you are observing views.
|
||||
*/
|
||||
abstract fun onInvalidated(tables: Set<String>)
|
||||
|
||||
internal open val isRemote: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
|
||||
* triggers in the database.
|
||||
*
|
||||
* This class is thread safe
|
||||
*/
|
||||
internal class ObservedTableTracker(tableCount: Int) {
|
||||
// number of observers per table
|
||||
val tableObservers = LongArray(tableCount)
|
||||
|
||||
// trigger state for each table at last sync
|
||||
// this field is updated when syncAndGet is called.
|
||||
private val triggerStates = BooleanArray(tableCount)
|
||||
|
||||
// when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
|
||||
private val triggerStateChanges = IntArray(tableCount)
|
||||
|
||||
var needsSync = false
|
||||
|
||||
/**
|
||||
* @return true if # of triggers is affected.
|
||||
*/
|
||||
fun onAdded(vararg tableIds: Int): Boolean {
|
||||
var needTriggerSync = false
|
||||
synchronized(this) {
|
||||
tableIds.forEach { tableId ->
|
||||
val prevObserverCount = tableObservers[tableId]
|
||||
tableObservers[tableId] = prevObserverCount + 1
|
||||
if (prevObserverCount == 0L) {
|
||||
needsSync = true
|
||||
needTriggerSync = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return needTriggerSync
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if # of triggers is affected.
|
||||
*/
|
||||
fun onRemoved(vararg tableIds: Int): Boolean {
|
||||
var needTriggerSync = false
|
||||
synchronized(this) {
|
||||
tableIds.forEach { tableId ->
|
||||
val prevObserverCount = tableObservers[tableId]
|
||||
tableObservers[tableId] = prevObserverCount - 1
|
||||
if (prevObserverCount == 1L) {
|
||||
needsSync = true
|
||||
needTriggerSync = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return needTriggerSync
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are re-opening the db we'll need to add all the triggers that we need so change
|
||||
* the current state to false for all.
|
||||
*/
|
||||
fun resetTriggerState() {
|
||||
synchronized(this) {
|
||||
Arrays.fill(triggerStates, false)
|
||||
needsSync = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this returns non-null, you must call onSyncCompleted.
|
||||
*
|
||||
* @return int[] An int array where the index for each tableId has the action for that
|
||||
* table.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@JvmName("getTablesToSync")
|
||||
fun getTablesToSync(): IntArray? {
|
||||
synchronized(this) {
|
||||
if (!needsSync) {
|
||||
return null
|
||||
}
|
||||
tableObservers.forEachIndexed { i, observerCount ->
|
||||
val newState = observerCount > 0
|
||||
if (newState != triggerStates[i]) {
|
||||
triggerStateChanges[i] = if (newState) ADD else REMOVE
|
||||
} else {
|
||||
triggerStateChanges[i] = NO_OP
|
||||
}
|
||||
triggerStates[i] = newState
|
||||
}
|
||||
needsSync = false
|
||||
return triggerStateChanges.clone()
|
||||
}
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
const val NO_OP = 0 // don't change trigger state for this table
|
||||
const val ADD = 1 // add triggers for this table
|
||||
const val REMOVE = 2 // remove triggers for this table
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An Observer wrapper that keeps a weak reference to the given object.
|
||||
*
|
||||
* This class will automatically unsubscribe when the wrapped observer goes out of memory.
|
||||
*/
|
||||
internal class WeakObserver(
|
||||
val tracker: InvalidationTracker,
|
||||
delegate: Observer
|
||||
) : Observer(delegate.tables) {
|
||||
val delegateRef: WeakReference<Observer> = WeakReference(delegate)
|
||||
override fun onInvalidated(tables: Set<String>) {
|
||||
val observer = delegateRef.get()
|
||||
if (observer == null) {
|
||||
tracker.removeObserver(this)
|
||||
} else {
|
||||
observer.onInvalidated(tables)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TRIGGERS = arrayOf("UPDATE", "DELETE", "INSERT")
|
||||
private const val UPDATE_TABLE_NAME = "room_table_modification_log"
|
||||
private const val TABLE_ID_COLUMN_NAME = "table_id"
|
||||
private const val INVALIDATED_COLUMN_NAME = "invalidated"
|
||||
private const val CREATE_TRACKING_TABLE_SQL =
|
||||
"CREATE TEMP TABLE $UPDATE_TABLE_NAME ($TABLE_ID_COLUMN_NAME INTEGER PRIMARY KEY, " +
|
||||
"$INVALIDATED_COLUMN_NAME INTEGER NOT NULL DEFAULT 0)"
|
||||
|
||||
@VisibleForTesting
|
||||
internal const val RESET_UPDATED_TABLES_SQL =
|
||||
"UPDATE $UPDATE_TABLE_NAME SET $INVALIDATED_COLUMN_NAME = 0 " +
|
||||
"WHERE $INVALIDATED_COLUMN_NAME = 1"
|
||||
|
||||
@VisibleForTesting
|
||||
internal const val SELECT_UPDATED_TABLES_SQL =
|
||||
"SELECT * FROM $UPDATE_TABLE_NAME WHERE $INVALIDATED_COLUMN_NAME = 1;"
|
||||
|
||||
internal fun getTriggerName(
|
||||
tableName: String,
|
||||
triggerType: String
|
||||
) = "`room_table_modification_trigger_${tableName}_$triggerType`"
|
||||
|
||||
internal fun beginTransactionInternal(database: SupportSQLiteDatabase) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
|
||||
database.isWriteAheadLoggingEnabled
|
||||
) {
|
||||
database.beginTransactionNonExclusive()
|
||||
} else {
|
||||
database.beginTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to
|
||||
* {@link MultiInstanceInvalidationService}.
|
||||
*/
|
||||
class MultiInstanceInvalidationClient {
|
||||
|
||||
/**
|
||||
* The application context.
|
||||
*/
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final Context mAppContext;
|
||||
|
||||
/**
|
||||
* The name of the database file.
|
||||
*/
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final String mName;
|
||||
|
||||
/**
|
||||
* The client ID assigned by {@link MultiInstanceInvalidationService}.
|
||||
*/
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
int mClientId;
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final InvalidationTracker mInvalidationTracker;
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final InvalidationTracker.Observer mObserver;
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Nullable
|
||||
IMultiInstanceInvalidationService mService;
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final Executor mExecutor;
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final IMultiInstanceInvalidationCallback mCallback =
|
||||
new IMultiInstanceInvalidationCallback.Stub() {
|
||||
@Override
|
||||
public void onInvalidation(final String[] tables) {
|
||||
mExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mInvalidationTracker.notifyObserversByTableNames(tables);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final AtomicBoolean mStopped = new AtomicBoolean(false);
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final ServiceConnection mServiceConnection = new ServiceConnection() {
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
mService = IMultiInstanceInvalidationService.Stub.asInterface(service);
|
||||
mExecutor.execute(mSetUpRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
mExecutor.execute(mRemoveObserverRunnable);
|
||||
mService = null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final Runnable mSetUpRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final IMultiInstanceInvalidationService service = mService;
|
||||
if (service != null) {
|
||||
mClientId = service.registerCallback(mCallback, mName);
|
||||
mInvalidationTracker.addObserver(mObserver);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final Runnable mRemoveObserverRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mInvalidationTracker.removeObserver(mObserver);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param context The Context to be used for binding
|
||||
* {@link IMultiInstanceInvalidationService}.
|
||||
* @param name The name of the database file.
|
||||
* @param serviceIntent The {@link Intent} used for binding
|
||||
* {@link IMultiInstanceInvalidationService}.
|
||||
* @param invalidationTracker The {@link InvalidationTracker}
|
||||
* @param executor The background executor.
|
||||
*/
|
||||
MultiInstanceInvalidationClient(Context context, String name, Intent serviceIntent,
|
||||
InvalidationTracker invalidationTracker, Executor executor) {
|
||||
mAppContext = context.getApplicationContext();
|
||||
mName = name;
|
||||
mInvalidationTracker = invalidationTracker;
|
||||
mExecutor = executor;
|
||||
// Use all tables names for observer.
|
||||
final Set<String> tableNames = invalidationTracker.mTableIdLookup.keySet();
|
||||
mObserver = new InvalidationTracker.Observer(tableNames.toArray(new String[0])) {
|
||||
@Override
|
||||
public void onInvalidated(@NonNull Set<String> tables) {
|
||||
if (mStopped.get()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final IMultiInstanceInvalidationService service = mService;
|
||||
if (service != null) {
|
||||
service.broadcastInvalidation(mClientId, tables.toArray(new String[0]));
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Room.LOG_TAG, "Cannot broadcast invalidation", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isRemote() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
mAppContext.bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if (mStopped.compareAndSet(false, true)) {
|
||||
mInvalidationTracker.removeObserver(mObserver);
|
||||
try {
|
||||
final IMultiInstanceInvalidationService service = mService;
|
||||
if (service != null) {
|
||||
service.unregisterCallback(mCallback, mClientId);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Room.LOG_TAG, "Cannot unregister multi-instance invalidation callback", e);
|
||||
}
|
||||
mAppContext.unbindService(mServiceConnection);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.room.Room.LOG_TAG
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Handles all the communication from [RoomDatabase] and [InvalidationTracker] to
|
||||
* [MultiInstanceInvalidationService].
|
||||
*
|
||||
* @param context The Context to be used for binding
|
||||
* [IMultiInstanceInvalidationService].
|
||||
* @param name The name of the database file.
|
||||
* @param serviceIntent The [Intent] used for binding
|
||||
* [IMultiInstanceInvalidationService].
|
||||
* @param invalidationTracker The [InvalidationTracker]
|
||||
* @param executor The background executor.
|
||||
*/
|
||||
internal class MultiInstanceInvalidationClient(
|
||||
context: Context,
|
||||
val name: String,
|
||||
serviceIntent: Intent,
|
||||
val invalidationTracker: InvalidationTracker,
|
||||
val executor: Executor
|
||||
) {
|
||||
private val appContext = context.applicationContext
|
||||
|
||||
/**
|
||||
* The client ID assigned by [MultiInstanceInvalidationService].
|
||||
*/
|
||||
var clientId = 0
|
||||
lateinit var observer: InvalidationTracker.Observer
|
||||
var service: IMultiInstanceInvalidationService? = null
|
||||
|
||||
val callback: IMultiInstanceInvalidationCallback =
|
||||
object : IMultiInstanceInvalidationCallback.Stub() {
|
||||
override fun onInvalidation(tables: Array<out String>) {
|
||||
executor.execute { invalidationTracker.notifyObserversByTableNames(*tables) }
|
||||
}
|
||||
}
|
||||
|
||||
val stopped = AtomicBoolean(false)
|
||||
|
||||
val serviceConnection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
this@MultiInstanceInvalidationClient.service =
|
||||
IMultiInstanceInvalidationService.Stub.asInterface(service)
|
||||
executor.execute(setUpRunnable)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
executor.execute(removeObserverRunnable)
|
||||
service = null
|
||||
}
|
||||
}
|
||||
|
||||
val setUpRunnable = Runnable {
|
||||
try {
|
||||
service?.let {
|
||||
clientId = it.registerCallback(callback, name)
|
||||
invalidationTracker.addObserver(observer)
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(LOG_TAG, "Cannot register multi-instance invalidation callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
val removeObserverRunnable = Runnable { invalidationTracker.removeObserver(observer) }
|
||||
|
||||
init {
|
||||
// Use all tables names for observer.
|
||||
val tableNames: Set<String> = invalidationTracker.tableIdLookup.keys
|
||||
observer = object : InvalidationTracker.Observer(tableNames.toTypedArray()) {
|
||||
override fun onInvalidated(tables: Set<String>) {
|
||||
if (stopped.get()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
service?.broadcastInvalidation(clientId, tables.toTypedArray())
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(LOG_TAG, "Cannot broadcast invalidation", e)
|
||||
}
|
||||
}
|
||||
|
||||
override val isRemote: Boolean
|
||||
get() = true
|
||||
}
|
||||
appContext.bindService(
|
||||
serviceIntent,
|
||||
serviceConnection,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (stopped.compareAndSet(false, true)) {
|
||||
invalidationTracker.removeObserver(observer)
|
||||
try {
|
||||
service?.unregisterCallback(callback, clientId)
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(LOG_TAG, "Cannot unregister multi-instance invalidation callback", e)
|
||||
}
|
||||
appContext.unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteCallbackList;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances.
|
||||
* This service runs in the main app process. All the instances of {@link InvalidationTracker}
|
||||
* (potentially in other processes) has to connect to this service.
|
||||
*
|
||||
* <p>The intent to launch it can be specified by
|
||||
* {@link RoomDatabase.Builder#setMultiInstanceInvalidationServiceIntent}, although the service is
|
||||
* defined in the manifest by default so there should be no need to override it in a normal
|
||||
* situation.
|
||||
*/
|
||||
@ExperimentalRoomApi
|
||||
public class MultiInstanceInvalidationService extends Service {
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
int mMaxClientId = 0;
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final HashMap<Integer, String> mClientNames = new HashMap<>();
|
||||
|
||||
// synthetic access
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final RemoteCallbackList<IMultiInstanceInvalidationCallback> mCallbackList =
|
||||
new RemoteCallbackList<IMultiInstanceInvalidationCallback>() {
|
||||
@Override
|
||||
public void onCallbackDied(IMultiInstanceInvalidationCallback callback,
|
||||
Object cookie) {
|
||||
mClientNames.remove((int) cookie);
|
||||
}
|
||||
};
|
||||
|
||||
private final IMultiInstanceInvalidationService.Stub mBinder =
|
||||
new IMultiInstanceInvalidationService.Stub() {
|
||||
|
||||
// Assigns a client ID to the client.
|
||||
@Override
|
||||
public int registerCallback(IMultiInstanceInvalidationCallback callback,
|
||||
String name) {
|
||||
if (name == null) {
|
||||
return 0;
|
||||
}
|
||||
synchronized (mCallbackList) {
|
||||
int clientId = ++mMaxClientId;
|
||||
// Use the client ID as the RemoteCallbackList cookie.
|
||||
if (mCallbackList.register(callback, clientId)) {
|
||||
mClientNames.put(clientId, name);
|
||||
return clientId;
|
||||
} else {
|
||||
--mMaxClientId;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly removes the client.
|
||||
// The client can die without calling this. In that case, mCallbackList
|
||||
// .onCallbackDied() can take care of removal.
|
||||
@Override
|
||||
public void unregisterCallback(IMultiInstanceInvalidationCallback callback,
|
||||
int clientId) {
|
||||
synchronized (mCallbackList) {
|
||||
mCallbackList.unregister(callback);
|
||||
mClientNames.remove(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcasts table invalidation to other instances of the same database file.
|
||||
// The broadcast is not sent to the caller itself.
|
||||
@Override
|
||||
public void broadcastInvalidation(int clientId, String[] tables) {
|
||||
synchronized (mCallbackList) {
|
||||
String name = mClientNames.get(clientId);
|
||||
if (name == null) {
|
||||
Log.w(Room.LOG_TAG, "Remote invalidation client ID not registered");
|
||||
return;
|
||||
}
|
||||
int count = mCallbackList.beginBroadcast();
|
||||
try {
|
||||
for (int i = 0; i < count; i++) {
|
||||
int targetClientId = (int) mCallbackList.getBroadcastCookie(i);
|
||||
String targetName = mClientNames.get(targetClientId);
|
||||
if (clientId == targetClientId // This is the caller itself.
|
||||
|| !name.equals(targetName)) { // Not the same file.
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
IMultiInstanceInvalidationCallback callback =
|
||||
mCallbackList.getBroadcastItem(i);
|
||||
callback.onInvalidation(tables);
|
||||
} catch (RemoteException e) {
|
||||
Log.w(Room.LOG_TAG, "Error invoking a remote callback", e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
mCallbackList.finishBroadcast();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(@NonNull Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.os.RemoteCallbackList
|
||||
import android.os.RemoteException
|
||||
import android.util.Log
|
||||
import androidx.room.Room.LOG_TAG
|
||||
|
||||
/**
|
||||
* A [Service] for remote invalidation among multiple [InvalidationTracker] instances.
|
||||
* This service runs in the main app process. All the instances of [InvalidationTracker]
|
||||
* (potentially in other processes) has to connect to this service.
|
||||
*
|
||||
* The intent to launch it can be specified by
|
||||
* [RoomDatabase.Builder.setMultiInstanceInvalidationServiceIntent], although the service is
|
||||
* defined in the manifest by default so there should be no need to override it in a normal
|
||||
* situation.
|
||||
*/
|
||||
@ExperimentalRoomApi
|
||||
class MultiInstanceInvalidationService : Service() {
|
||||
internal var maxClientId = 0
|
||||
internal val clientNames = mutableMapOf<Int, String>()
|
||||
|
||||
internal val callbackList: RemoteCallbackList<IMultiInstanceInvalidationCallback> =
|
||||
object : RemoteCallbackList<IMultiInstanceInvalidationCallback>() {
|
||||
override fun onCallbackDied(
|
||||
callback: IMultiInstanceInvalidationCallback,
|
||||
cookie: Any
|
||||
) {
|
||||
clientNames.remove(cookie as Int)
|
||||
}
|
||||
}
|
||||
|
||||
private val binder: IMultiInstanceInvalidationService.Stub =
|
||||
object : IMultiInstanceInvalidationService.Stub() {
|
||||
// Assigns a client ID to the client.
|
||||
override fun registerCallback(
|
||||
callback: IMultiInstanceInvalidationCallback,
|
||||
name: String?
|
||||
): Int {
|
||||
if (name == null) {
|
||||
return 0
|
||||
}
|
||||
synchronized(callbackList) {
|
||||
val clientId = ++maxClientId
|
||||
// Use the client ID as the RemoteCallbackList cookie.
|
||||
return if (callbackList.register(callback, clientId)) {
|
||||
clientNames[clientId] = name
|
||||
clientId
|
||||
} else {
|
||||
--maxClientId
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly removes the client.
|
||||
// The client can die without calling this. In that case, callbackList
|
||||
// .onCallbackDied() can take care of removal.
|
||||
override fun unregisterCallback(
|
||||
callback: IMultiInstanceInvalidationCallback,
|
||||
clientId: Int
|
||||
) {
|
||||
synchronized(callbackList) {
|
||||
callbackList.unregister(callback)
|
||||
clientNames.remove(clientId)
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcasts table invalidation to other instances of the same database file.
|
||||
// The broadcast is not sent to the caller itself.
|
||||
override fun broadcastInvalidation(clientId: Int, tables: Array<out String>) {
|
||||
synchronized(callbackList) {
|
||||
val name = clientNames[clientId]
|
||||
if (name == null) {
|
||||
Log.w(LOG_TAG, "Remote invalidation client ID not registered")
|
||||
return
|
||||
}
|
||||
val count = callbackList.beginBroadcast()
|
||||
try {
|
||||
for (i in 0 until count) {
|
||||
val targetClientId = callbackList.getBroadcastCookie(i) as Int
|
||||
val targetName = clientNames[targetClientId]
|
||||
if (clientId == targetClientId || name != targetName) {
|
||||
// Skip if this is the caller itself or broadcast is for another
|
||||
// database.
|
||||
continue
|
||||
}
|
||||
try {
|
||||
callbackList.getBroadcastItem(i).onInvalidation(tables)
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(LOG_TAG, "Error invoking a remote callback", e)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
callbackList.finishBroadcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return binder
|
||||
}
|
||||
}
|
|
@ -1,312 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.SQLException;
|
||||
import android.database.sqlite.SQLiteTransactionListener;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
|
||||
/**
|
||||
* Implements {@link SupportSQLiteDatabase} for SQLite queries.
|
||||
*/
|
||||
final class QueryInterceptorDatabase implements SupportSQLiteDatabase {
|
||||
|
||||
private final SupportSQLiteDatabase mDelegate;
|
||||
private final RoomDatabase.QueryCallback mQueryCallback;
|
||||
private final Executor mQueryCallbackExecutor;
|
||||
|
||||
QueryInterceptorDatabase(@NonNull SupportSQLiteDatabase supportSQLiteDatabase,
|
||||
@NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
|
||||
queryCallbackExecutor) {
|
||||
mDelegate = supportSQLiteDatabase;
|
||||
mQueryCallback = queryCallback;
|
||||
mQueryCallbackExecutor = queryCallbackExecutor;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SupportSQLiteStatement compileStatement(@NonNull String sql) {
|
||||
return new QueryInterceptorStatement(mDelegate.compileStatement(sql),
|
||||
mQueryCallback, sql, mQueryCallbackExecutor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginTransaction() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
|
||||
Collections.emptyList()));
|
||||
mDelegate.beginTransaction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginTransactionNonExclusive() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
|
||||
Collections.emptyList()));
|
||||
mDelegate.beginTransactionNonExclusive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginTransactionWithListener(@NonNull SQLiteTransactionListener
|
||||
transactionListener) {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
|
||||
Collections.emptyList()));
|
||||
mDelegate.beginTransactionWithListener(transactionListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginTransactionWithListenerNonExclusive(
|
||||
@NonNull SQLiteTransactionListener transactionListener) {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
|
||||
Collections.emptyList()));
|
||||
mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTransaction() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("END TRANSACTION",
|
||||
Collections.emptyList()));
|
||||
mDelegate.endTransaction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTransactionSuccessful() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("TRANSACTION SUCCESSFUL",
|
||||
Collections.emptyList()));
|
||||
mDelegate.setTransactionSuccessful();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean inTransaction() {
|
||||
return mDelegate.inTransaction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDbLockedByCurrentThread() {
|
||||
return mDelegate.isDbLockedByCurrentThread();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean yieldIfContendedSafely() {
|
||||
return mDelegate.yieldIfContendedSafely();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
|
||||
return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return mDelegate.getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVersion(int version) {
|
||||
mDelegate.setVersion(version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getMaximumSize() {
|
||||
return mDelegate.getMaximumSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long setMaximumSize(long numBytes) {
|
||||
return mDelegate.setMaximumSize(numBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPageSize() {
|
||||
return mDelegate.getPageSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPageSize(long numBytes) {
|
||||
mDelegate.setPageSize(numBytes);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Cursor query(@NonNull String query) {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
|
||||
Collections.emptyList()));
|
||||
return mDelegate.query(query);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Cursor query(@NonNull String query, @NonNull Object[] bindArgs) {
|
||||
List<Object> inputArguments = new ArrayList<>();
|
||||
inputArguments.addAll(Arrays.asList(bindArgs));
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
|
||||
inputArguments));
|
||||
return mDelegate.query(query, bindArgs);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Cursor query(@NonNull SupportSQLiteQuery query) {
|
||||
QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
|
||||
query.bindTo(queryInterceptorProgram);
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
|
||||
queryInterceptorProgram.getBindArgs()));
|
||||
return mDelegate.query(query);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Cursor query(@NonNull SupportSQLiteQuery query,
|
||||
@NonNull CancellationSignal cancellationSignal) {
|
||||
QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
|
||||
query.bindTo(queryInterceptorProgram);
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
|
||||
queryInterceptorProgram.getBindArgs()));
|
||||
return mDelegate.query(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long insert(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values)
|
||||
throws SQLException {
|
||||
return mDelegate.insert(table, conflictAlgorithm, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull String table, @NonNull String whereClause,
|
||||
@NonNull Object[] whereArgs) {
|
||||
return mDelegate.delete(table, whereClause, whereArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values,
|
||||
@NonNull String whereClause,
|
||||
@NonNull Object[] whereArgs) {
|
||||
return mDelegate.update(table, conflictAlgorithm, values, whereClause,
|
||||
whereArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execSQL(@NonNull String sql) throws SQLException {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, new ArrayList<>(0)));
|
||||
mDelegate.execSQL(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execSQL(@NonNull String sql, @NonNull Object[] bindArgs) throws SQLException {
|
||||
List<Object> inputArguments = new ArrayList<>();
|
||||
inputArguments.addAll(Arrays.asList(bindArgs));
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, inputArguments));
|
||||
mDelegate.execSQL(sql, inputArguments.toArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly() {
|
||||
return mDelegate.isReadOnly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return mDelegate.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needUpgrade(int newVersion) {
|
||||
return mDelegate.needUpgrade(newVersion);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getPath() {
|
||||
return mDelegate.getPath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocale(@NonNull Locale locale) {
|
||||
mDelegate.setLocale(locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxSqlCacheSize(int cacheSize) {
|
||||
mDelegate.setMaxSqlCacheSize(cacheSize);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
@Override
|
||||
public void setForeignKeyConstraintsEnabled(boolean enable) {
|
||||
mDelegate.setForeignKeyConstraintsEnabled(enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean enableWriteAheadLogging() {
|
||||
return mDelegate.enableWriteAheadLogging();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
@Override
|
||||
public void disableWriteAheadLogging() {
|
||||
mDelegate.disableWriteAheadLogging();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
@Override
|
||||
public boolean isWriteAheadLoggingEnabled() {
|
||||
return mDelegate.isWriteAheadLoggingEnabled();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<Pair<String, String>> getAttachedDbs() {
|
||||
return mDelegate.getAttachedDbs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDatabaseIntegrityOk() {
|
||||
return mDelegate.isDatabaseIntegrityOk();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
mDelegate.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExecPerConnectionSQLSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteTransactionListener
|
||||
import android.os.CancellationSignal
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteStatement
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* Implements [SupportSQLiteDatabase] for SQLite queries.
|
||||
*/
|
||||
internal class QueryInterceptorDatabase(
|
||||
private val delegate: SupportSQLiteDatabase,
|
||||
private val queryCallbackExecutor: Executor,
|
||||
private val queryCallback: RoomDatabase.QueryCallback
|
||||
) : SupportSQLiteDatabase by delegate {
|
||||
|
||||
override fun compileStatement(sql: String): SupportSQLiteStatement {
|
||||
return QueryInterceptorStatement(
|
||||
delegate.compileStatement(sql),
|
||||
sql,
|
||||
queryCallbackExecutor,
|
||||
queryCallback,
|
||||
)
|
||||
}
|
||||
|
||||
override fun beginTransaction() {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", emptyList())
|
||||
}
|
||||
delegate.beginTransaction()
|
||||
}
|
||||
|
||||
override fun beginTransactionNonExclusive() {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery("BEGIN DEFERRED TRANSACTION", emptyList())
|
||||
}
|
||||
delegate.beginTransactionNonExclusive()
|
||||
}
|
||||
|
||||
override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION", emptyList())
|
||||
}
|
||||
delegate.beginTransactionWithListener(transactionListener)
|
||||
}
|
||||
|
||||
override fun beginTransactionWithListenerNonExclusive(
|
||||
transactionListener: SQLiteTransactionListener
|
||||
) {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery("BEGIN DEFERRED TRANSACTION", emptyList())
|
||||
}
|
||||
delegate.beginTransactionWithListenerNonExclusive(transactionListener)
|
||||
}
|
||||
|
||||
override fun endTransaction() {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery("END TRANSACTION", emptyList())
|
||||
}
|
||||
delegate.endTransaction()
|
||||
}
|
||||
|
||||
override fun setTransactionSuccessful() {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery("TRANSACTION SUCCESSFUL", emptyList())
|
||||
}
|
||||
delegate.setTransactionSuccessful()
|
||||
}
|
||||
|
||||
override fun query(query: String): Cursor {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(query, emptyList())
|
||||
}
|
||||
return delegate.query(query)
|
||||
}
|
||||
|
||||
override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
|
||||
queryCallbackExecutor.execute { queryCallback.onQuery(query, bindArgs.toList()) }
|
||||
return delegate.query(query, bindArgs)
|
||||
}
|
||||
|
||||
override fun query(query: SupportSQLiteQuery): Cursor {
|
||||
val queryInterceptorProgram = QueryInterceptorProgram()
|
||||
query.bindTo(queryInterceptorProgram)
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(query.sql, queryInterceptorProgram.bindArgsCache)
|
||||
}
|
||||
return delegate.query(query)
|
||||
}
|
||||
|
||||
override fun query(
|
||||
query: SupportSQLiteQuery,
|
||||
cancellationSignal: CancellationSignal?
|
||||
): Cursor {
|
||||
val queryInterceptorProgram = QueryInterceptorProgram()
|
||||
query.bindTo(queryInterceptorProgram)
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(
|
||||
query.sql,
|
||||
queryInterceptorProgram.bindArgsCache
|
||||
)
|
||||
}
|
||||
return delegate.query(query)
|
||||
}
|
||||
|
||||
// Suppress warning about `SQL` in execSQL not being camel case. This is an override function
|
||||
// and it can't be renamed.
|
||||
@Suppress("AcronymName")
|
||||
override fun execSQL(sql: String) {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(sql, emptyList())
|
||||
}
|
||||
delegate.execSQL(sql)
|
||||
}
|
||||
|
||||
// Suppress warning about `SQL` in execSQL not being camel case. This is an override function
|
||||
// and it can't be renamed.
|
||||
@Suppress("AcronymName")
|
||||
override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
|
||||
val inputArguments = buildList { addAll(bindArgs) }
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(sql, inputArguments)
|
||||
}
|
||||
delegate.execSQL(sql, inputArguments.toTypedArray())
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
|
||||
|
||||
private final SupportSQLiteOpenHelper mDelegate;
|
||||
private final RoomDatabase.QueryCallback mQueryCallback;
|
||||
private final Executor mQueryCallbackExecutor;
|
||||
|
||||
QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
|
||||
@NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
|
||||
queryCallbackExecutor) {
|
||||
mDelegate = supportSQLiteOpenHelper;
|
||||
mQueryCallback = queryCallback;
|
||||
mQueryCallbackExecutor = queryCallbackExecutor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getDatabaseName() {
|
||||
return mDelegate.getDatabaseName();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
@Override
|
||||
public void setWriteAheadLoggingEnabled(boolean enabled) {
|
||||
mDelegate.setWriteAheadLoggingEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SupportSQLiteDatabase getWritableDatabase() {
|
||||
return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback,
|
||||
mQueryCallbackExecutor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SupportSQLiteDatabase getReadableDatabase() {
|
||||
return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback,
|
||||
mQueryCallbackExecutor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mDelegate.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public SupportSQLiteOpenHelper getDelegate() {
|
||||
return mDelegate;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
internal class QueryInterceptorOpenHelper(
|
||||
override val delegate: SupportSQLiteOpenHelper,
|
||||
private val queryCallbackExecutor: Executor,
|
||||
private val queryCallback: RoomDatabase.QueryCallback
|
||||
) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper {
|
||||
override val writableDatabase: SupportSQLiteDatabase
|
||||
get() = QueryInterceptorDatabase(
|
||||
delegate.writableDatabase,
|
||||
queryCallbackExecutor,
|
||||
queryCallback
|
||||
)
|
||||
|
||||
override val readableDatabase: SupportSQLiteDatabase
|
||||
get() = QueryInterceptorDatabase(
|
||||
delegate.readableDatabase,
|
||||
queryCallbackExecutor,
|
||||
queryCallback
|
||||
)
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper.
|
||||
*/
|
||||
@SuppressWarnings("AcronymName")
|
||||
final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
|
||||
|
||||
private final SupportSQLiteOpenHelper.Factory mDelegate;
|
||||
private final RoomDatabase.QueryCallback mQueryCallback;
|
||||
private final Executor mQueryCallbackExecutor;
|
||||
|
||||
@SuppressWarnings("LambdaLast")
|
||||
QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory,
|
||||
@NonNull RoomDatabase.QueryCallback queryCallback,
|
||||
@NonNull Executor queryCallbackExecutor) {
|
||||
mDelegate = factory;
|
||||
mQueryCallback = queryCallback;
|
||||
mQueryCallbackExecutor = queryCallbackExecutor;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SupportSQLiteOpenHelper create(
|
||||
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
|
||||
return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback,
|
||||
mQueryCallbackExecutor);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* Implements [SupportSQLiteOpenHelper.Factory] to wrap [QueryInterceptorOpenHelper].
|
||||
*/
|
||||
internal class QueryInterceptorOpenHelperFactory(
|
||||
private val delegate: SupportSQLiteOpenHelper.Factory,
|
||||
private val queryCallbackExecutor: Executor,
|
||||
private val queryCallback: RoomDatabase.QueryCallback,
|
||||
) : SupportSQLiteOpenHelper.Factory by delegate {
|
||||
override fun create(
|
||||
configuration: SupportSQLiteOpenHelper.Configuration
|
||||
): SupportSQLiteOpenHelper {
|
||||
return QueryInterceptorOpenHelper(
|
||||
delegate.create(configuration),
|
||||
queryCallbackExecutor,
|
||||
queryCallback
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteProgram;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A program implementing an {@link SupportSQLiteProgram} API to record bind arguments.
|
||||
*/
|
||||
final class QueryInterceptorProgram implements SupportSQLiteProgram {
|
||||
private List<Object> mBindArgsCache = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void bindNull(int index) {
|
||||
saveArgsToCache(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindLong(int index, long value) {
|
||||
saveArgsToCache(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindDouble(int index, double value) {
|
||||
saveArgsToCache(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindString(int index, String value) {
|
||||
saveArgsToCache(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindBlob(int index, byte[] value) {
|
||||
saveArgsToCache(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearBindings() {
|
||||
mBindArgsCache.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() { }
|
||||
|
||||
private void saveArgsToCache(int bindIndex, Object value) {
|
||||
// The index into bind methods are 1...n
|
||||
int index = bindIndex - 1;
|
||||
if (index >= mBindArgsCache.size()) {
|
||||
for (int i = mBindArgsCache.size(); i <= index; i++) {
|
||||
mBindArgsCache.add(null);
|
||||
}
|
||||
}
|
||||
mBindArgsCache.set(index, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of arguments associated with the query.
|
||||
*
|
||||
* @return argument list.
|
||||
*/
|
||||
List<Object> getBindArgs() {
|
||||
return mBindArgsCache;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteProgram
|
||||
|
||||
/**
|
||||
* A program implementing an [SupportSQLiteProgram] API to record bind arguments.
|
||||
*/
|
||||
internal class QueryInterceptorProgram : SupportSQLiteProgram {
|
||||
internal val bindArgsCache = mutableListOf<Any?>()
|
||||
|
||||
override fun bindNull(index: Int) {
|
||||
saveArgsToCache(index, null)
|
||||
}
|
||||
|
||||
override fun bindLong(index: Int, value: Long) {
|
||||
saveArgsToCache(index, value)
|
||||
}
|
||||
|
||||
override fun bindDouble(index: Int, value: Double) {
|
||||
saveArgsToCache(index, value)
|
||||
}
|
||||
|
||||
override fun bindString(index: Int, value: String) {
|
||||
saveArgsToCache(index, value)
|
||||
}
|
||||
|
||||
override fun bindBlob(index: Int, value: ByteArray) {
|
||||
saveArgsToCache(index, value)
|
||||
}
|
||||
|
||||
override fun clearBindings() {
|
||||
bindArgsCache.clear()
|
||||
}
|
||||
|
||||
override fun close() {}
|
||||
|
||||
private fun saveArgsToCache(bindIndex: Int, value: Any?) {
|
||||
// The index into bind methods are 1...n
|
||||
val index = bindIndex - 1
|
||||
if (index >= bindArgsCache.size) {
|
||||
for (i in bindArgsCache.size..index) {
|
||||
bindArgsCache.add(null)
|
||||
}
|
||||
}
|
||||
bindArgsCache[index] = value
|
||||
}
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* Implements an instance of {@link SupportSQLiteStatement} for SQLite queries.
|
||||
*/
|
||||
final class QueryInterceptorStatement implements SupportSQLiteStatement {
|
||||
|
||||
private final SupportSQLiteStatement mDelegate;
|
||||
private final RoomDatabase.QueryCallback mQueryCallback;
|
||||
private final String mSqlStatement;
|
||||
private final List<Object> mBindArgsCache = new ArrayList<>();
|
||||
private final Executor mQueryCallbackExecutor;
|
||||
|
||||
QueryInterceptorStatement(@NonNull SupportSQLiteStatement compileStatement,
|
||||
@NonNull RoomDatabase.QueryCallback queryCallback, String sqlStatement,
|
||||
@NonNull Executor queryCallbackExecutor) {
|
||||
mDelegate = compileStatement;
|
||||
mQueryCallback = queryCallback;
|
||||
mSqlStatement = sqlStatement;
|
||||
mQueryCallbackExecutor = queryCallbackExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
|
||||
mDelegate.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int executeUpdateDelete() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
|
||||
return mDelegate.executeUpdateDelete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long executeInsert() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
|
||||
return mDelegate.executeInsert();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long simpleQueryForLong() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
|
||||
return mDelegate.simpleQueryForLong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String simpleQueryForString() {
|
||||
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
|
||||
return mDelegate.simpleQueryForString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindNull(int index) {
|
||||
saveArgsToCache(index, mBindArgsCache.toArray());
|
||||
mDelegate.bindNull(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindLong(int index, long value) {
|
||||
saveArgsToCache(index, value);
|
||||
mDelegate.bindLong(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindDouble(int index, double value) {
|
||||
saveArgsToCache(index, value);
|
||||
mDelegate.bindDouble(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindString(int index, String value) {
|
||||
saveArgsToCache(index, value);
|
||||
mDelegate.bindString(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindBlob(int index, byte[] value) {
|
||||
saveArgsToCache(index, value);
|
||||
mDelegate.bindBlob(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearBindings() {
|
||||
mBindArgsCache.clear();
|
||||
mDelegate.clearBindings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
mDelegate.close();
|
||||
}
|
||||
|
||||
private void saveArgsToCache(int bindIndex, Object value) {
|
||||
int index = bindIndex - 1;
|
||||
if (index >= mBindArgsCache.size()) {
|
||||
// Add null entries to the list until we have the desired # of indices
|
||||
for (int i = mBindArgsCache.size(); i <= index; i++) {
|
||||
mBindArgsCache.add(null);
|
||||
}
|
||||
}
|
||||
mBindArgsCache.set(index, value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteStatement
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* Implements an instance of [SupportSQLiteStatement] for SQLite queries.
|
||||
*/
|
||||
internal class QueryInterceptorStatement(
|
||||
private val delegate: SupportSQLiteStatement,
|
||||
private val sqlStatement: String,
|
||||
private val queryCallbackExecutor: Executor,
|
||||
private val queryCallback: RoomDatabase.QueryCallback,
|
||||
) : SupportSQLiteStatement by delegate {
|
||||
|
||||
private val bindArgsCache = mutableListOf<Any?>()
|
||||
|
||||
override fun execute() {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(sqlStatement, bindArgsCache)
|
||||
}
|
||||
delegate.execute()
|
||||
}
|
||||
|
||||
override fun executeUpdateDelete(): Int {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(sqlStatement, bindArgsCache)
|
||||
}
|
||||
return delegate.executeUpdateDelete()
|
||||
}
|
||||
|
||||
override fun executeInsert(): Long {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(sqlStatement, bindArgsCache)
|
||||
}
|
||||
return delegate.executeInsert()
|
||||
}
|
||||
|
||||
override fun simpleQueryForLong(): Long {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(sqlStatement, bindArgsCache)
|
||||
}
|
||||
return delegate.simpleQueryForLong()
|
||||
}
|
||||
|
||||
override fun simpleQueryForString(): String? {
|
||||
queryCallbackExecutor.execute {
|
||||
queryCallback.onQuery(sqlStatement, bindArgsCache)
|
||||
}
|
||||
return delegate.simpleQueryForString()
|
||||
}
|
||||
|
||||
override fun bindNull(index: Int) {
|
||||
saveArgsToCache(index, null)
|
||||
delegate.bindNull(index)
|
||||
}
|
||||
|
||||
override fun bindLong(index: Int, value: Long) {
|
||||
saveArgsToCache(index, value)
|
||||
delegate.bindLong(index, value)
|
||||
}
|
||||
|
||||
override fun bindDouble(index: Int, value: Double) {
|
||||
saveArgsToCache(index, value)
|
||||
delegate.bindDouble(index, value)
|
||||
}
|
||||
|
||||
override fun bindString(index: Int, value: String) {
|
||||
saveArgsToCache(index, value)
|
||||
delegate.bindString(index, value)
|
||||
}
|
||||
|
||||
override fun bindBlob(index: Int, value: ByteArray) {
|
||||
saveArgsToCache(index, value)
|
||||
delegate.bindBlob(index, value)
|
||||
}
|
||||
|
||||
override fun clearBindings() {
|
||||
bindArgsCache.clear()
|
||||
delegate.clearBindings()
|
||||
}
|
||||
|
||||
private fun saveArgsToCache(bindIndex: Int, value: Any?) {
|
||||
val index = bindIndex - 1
|
||||
if (index >= bindArgsCache.size) {
|
||||
// Add null entries to the list until we have the desired # of indices
|
||||
repeat(index - bindArgsCache.size + 1) {
|
||||
bindArgsCache.add(null)
|
||||
}
|
||||
}
|
||||
bindArgsCache[index] = value
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
/**
|
||||
* Utility class for Room.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class Room {
|
||||
static final String LOG_TAG = "ROOM";
|
||||
/**
|
||||
* The master table where room keeps its metadata information.
|
||||
*/
|
||||
public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
|
||||
private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
|
||||
|
||||
/**
|
||||
* Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
|
||||
* should keep a reference to it and re-use it.
|
||||
*
|
||||
* @param context The context for the database. This is usually the Application context.
|
||||
* @param klass The abstract class which is annotated with {@link Database} and extends
|
||||
* {@link RoomDatabase}.
|
||||
* @param name The name of the database file.
|
||||
* @param <T> The type of the database class.
|
||||
* @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@NonNull
|
||||
public static <T extends RoomDatabase> RoomDatabase.Builder<T> databaseBuilder(
|
||||
@NonNull Context context, @NonNull Class<T> klass, @NonNull String name) {
|
||||
//noinspection ConstantConditions
|
||||
if (name == null || name.trim().length() == 0) {
|
||||
throw new IllegalArgumentException("Cannot build a database with null or empty name."
|
||||
+ " If you are trying to create an in memory database, use Room"
|
||||
+ ".inMemoryDatabaseBuilder");
|
||||
}
|
||||
return new RoomDatabase.Builder<>(context, klass, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
|
||||
* database disappears when the process is killed.
|
||||
* Once a database is built, you should keep a reference to it and re-use it.
|
||||
*
|
||||
* @param context The context for the database. This is usually the Application context.
|
||||
* @param klass The abstract class which is annotated with {@link Database} and extends
|
||||
* {@link RoomDatabase}.
|
||||
* @param <T> The type of the database class.
|
||||
* @return A {@code RoomDatabaseBuilder<T>} which you can use to create the database.
|
||||
*/
|
||||
@NonNull
|
||||
public static <T extends RoomDatabase> RoomDatabase.Builder<T> inMemoryDatabaseBuilder(
|
||||
@NonNull Context context, @NonNull Class<T> klass) {
|
||||
return new RoomDatabase.Builder<>(context, klass, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
|
||||
@NonNull
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
||||
public static <T, C> T getGeneratedImplementation(@NonNull Class<C> klass,
|
||||
@NonNull String suffix) {
|
||||
final String fullPackage = klass.getPackage().getName();
|
||||
String name = klass.getCanonicalName();
|
||||
final String postPackageName = fullPackage.isEmpty()
|
||||
? name
|
||||
: name.substring(fullPackage.length() + 1);
|
||||
final String implName = postPackageName.replace('.', '_') + suffix;
|
||||
//noinspection TryWithIdenticalCatches
|
||||
try {
|
||||
|
||||
final String fullClassName = fullPackage.isEmpty()
|
||||
? implName
|
||||
: fullPackage + "." + implName;
|
||||
@SuppressWarnings("unchecked")
|
||||
final Class<T> aClass = (Class<T>) Class.forName(
|
||||
fullClassName, true, klass.getClassLoader());
|
||||
return aClass.newInstance();
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("cannot find implementation for "
|
||||
+ klass.getCanonicalName() + ". " + implName + " does not exist");
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException("Cannot access the constructor"
|
||||
+ klass.getCanonicalName());
|
||||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException("Failed to create an instance of "
|
||||
+ klass.getCanonicalName());
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated This type should not be instantiated as it contains only static methods. */
|
||||
@Deprecated
|
||||
@SuppressWarnings("PrivateConstructorForUtilityClass")
|
||||
public Room() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.RestrictTo
|
||||
|
||||
/**
|
||||
* Utility functions for Room.
|
||||
*/
|
||||
object Room {
|
||||
|
||||
internal const val LOG_TAG = "ROOM"
|
||||
|
||||
/**
|
||||
* The master table where room keeps its metadata information.
|
||||
*/
|
||||
const val MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME
|
||||
|
||||
private const val CURSOR_CONV_SUFFIX = "_CursorConverter"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
||||
@JvmStatic
|
||||
fun <T, C> getGeneratedImplementation(
|
||||
klass: Class<C>,
|
||||
suffix: String
|
||||
): T {
|
||||
val fullPackage = klass.getPackage()!!.name
|
||||
val name: String = klass.canonicalName!!
|
||||
val postPackageName =
|
||||
if (fullPackage.isEmpty()) name else name.substring(fullPackage.length + 1)
|
||||
val implName = postPackageName.replace('.', '_') + suffix
|
||||
return try {
|
||||
val fullClassName = if (fullPackage.isEmpty()) {
|
||||
implName
|
||||
} else {
|
||||
"$fullPackage.$implName"
|
||||
}
|
||||
val aClass = Class.forName(
|
||||
fullClassName, true, klass.classLoader
|
||||
) as Class<T>
|
||||
aClass.getDeclaredConstructor().newInstance()
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw RuntimeException(
|
||||
"Cannot find implementation for ${klass.canonicalName}. $implName does not " +
|
||||
"exist"
|
||||
)
|
||||
} catch (e: IllegalAccessException) {
|
||||
throw RuntimeException(
|
||||
"Cannot access the constructor ${klass.canonicalName}"
|
||||
)
|
||||
} catch (e: InstantiationException) {
|
||||
throw RuntimeException(
|
||||
"Failed to create an instance of ${klass.canonicalName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
|
||||
* database disappears when the process is killed.
|
||||
* Once a database is built, you should keep a reference to it and re-use it.
|
||||
*
|
||||
* @param context The context for the database. This is usually the Application context.
|
||||
* @param klass The abstract class which is annotated with [Database] and extends
|
||||
* [RoomDatabase].
|
||||
* @param T The type of the database class.
|
||||
* @return A `RoomDatabaseBuilder<T>` which you can use to create the database.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : RoomDatabase> inMemoryDatabaseBuilder(
|
||||
context: Context,
|
||||
klass: Class<T>
|
||||
): RoomDatabase.Builder<T> {
|
||||
return RoomDatabase.Builder(context, klass, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
|
||||
* should keep a reference to it and re-use it.
|
||||
*
|
||||
* @param context The context for the database. This is usually the Application context.
|
||||
* @param klass The abstract class which is annotated with [Database] and extends
|
||||
* [RoomDatabase].
|
||||
* @param name The name of the database file.
|
||||
* @param T The type of the database class.
|
||||
* @return A `RoomDatabaseBuilder<T>` which you can use to create the database.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : RoomDatabase> databaseBuilder(
|
||||
context: Context,
|
||||
klass: Class<T>,
|
||||
name: String?
|
||||
): RoomDatabase.Builder<T> {
|
||||
require(!name.isNullOrBlank()) {
|
||||
"Cannot build a database with null or empty name." +
|
||||
" If you are trying to create an in memory database, use Room" +
|
||||
".inMemoryDatabaseBuilder"
|
||||
}
|
||||
return RoomDatabase.Builder(context, klass, name)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,277 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An open helper that holds a reference to the configuration until the database is opened.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
|
||||
@Nullable
|
||||
private DatabaseConfiguration mConfiguration;
|
||||
@NonNull
|
||||
private final Delegate mDelegate;
|
||||
@NonNull
|
||||
private final String mIdentityHash;
|
||||
/**
|
||||
* Room v1 had a bug where the hash was not consistent if fields are reordered.
|
||||
* The new has fixes it but we still need to accept the legacy hash.
|
||||
*/
|
||||
@NonNull // b/64290754
|
||||
private final String mLegacyHash;
|
||||
|
||||
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
|
||||
@NonNull String identityHash, @NonNull String legacyHash) {
|
||||
super(delegate.version);
|
||||
mConfiguration = configuration;
|
||||
mDelegate = delegate;
|
||||
mIdentityHash = identityHash;
|
||||
mLegacyHash = legacyHash;
|
||||
}
|
||||
|
||||
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
|
||||
@NonNull String legacyHash) {
|
||||
this(configuration, delegate, "", legacyHash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigure(SupportSQLiteDatabase db) {
|
||||
super.onConfigure(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SupportSQLiteDatabase db) {
|
||||
boolean isEmptyDatabase = hasEmptySchema(db);
|
||||
mDelegate.createAllTables(db);
|
||||
if (!isEmptyDatabase) {
|
||||
// A 0 version pre-populated database goes through the create path because the
|
||||
// framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
|
||||
// find the database not to be empty, then it is a pre-populated, we must validate it to
|
||||
// see if its suitable for usage.
|
||||
ValidationResult result = mDelegate.onValidateSchema(db);
|
||||
if (!result.isValid) {
|
||||
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
|
||||
+ result.expectedFoundMsg);
|
||||
}
|
||||
}
|
||||
updateIdentity(db);
|
||||
mDelegate.onCreate(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
boolean migrated = false;
|
||||
if (mConfiguration != null) {
|
||||
List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
|
||||
oldVersion, newVersion);
|
||||
if (migrations != null) {
|
||||
mDelegate.onPreMigrate(db);
|
||||
for (Migration migration : migrations) {
|
||||
migration.migrate(db);
|
||||
}
|
||||
ValidationResult result = mDelegate.onValidateSchema(db);
|
||||
if (!result.isValid) {
|
||||
throw new IllegalStateException("Migration didn't properly handle: "
|
||||
+ result.expectedFoundMsg);
|
||||
}
|
||||
mDelegate.onPostMigrate(db);
|
||||
updateIdentity(db);
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
if (!migrated) {
|
||||
if (mConfiguration != null
|
||||
&& !mConfiguration.isMigrationRequired(oldVersion, newVersion)) {
|
||||
mDelegate.dropAllTables(db);
|
||||
mDelegate.createAllTables(db);
|
||||
} else {
|
||||
throw new IllegalStateException("A migration from " + oldVersion + " to "
|
||||
+ newVersion + " was required but not found. Please provide the "
|
||||
+ "necessary Migration path via "
|
||||
+ "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
|
||||
+ "destructive migrations via one of the "
|
||||
+ "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, oldVersion, newVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(SupportSQLiteDatabase db) {
|
||||
super.onOpen(db);
|
||||
checkIdentity(db);
|
||||
mDelegate.onOpen(db);
|
||||
// there might be too many configurations etc, just clear it.
|
||||
mConfiguration = null;
|
||||
}
|
||||
|
||||
private void checkIdentity(SupportSQLiteDatabase db) {
|
||||
if (hasRoomMasterTable(db)) {
|
||||
String identityHash = null;
|
||||
Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
identityHash = cursor.getString(0);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
|
||||
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
|
||||
+ " you've changed schema but forgot to update the version number. You can"
|
||||
+ " simply fix this by increasing the version number.");
|
||||
}
|
||||
} else {
|
||||
// No room_master_table, this might an a pre-populated DB, we must validate to see if
|
||||
// its suitable for usage.
|
||||
ValidationResult result = mDelegate.onValidateSchema(db);
|
||||
if (!result.isValid) {
|
||||
throw new IllegalStateException("Pre-packaged database has an invalid schema: "
|
||||
+ result.expectedFoundMsg);
|
||||
}
|
||||
mDelegate.onPostMigrate(db);
|
||||
updateIdentity(db);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateIdentity(SupportSQLiteDatabase db) {
|
||||
createMasterTableIfNotExists(db);
|
||||
db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
|
||||
}
|
||||
|
||||
private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
|
||||
db.execSQL(RoomMasterTable.CREATE_QUERY);
|
||||
}
|
||||
|
||||
private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) {
|
||||
Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='"
|
||||
+ RoomMasterTable.TABLE_NAME + "'");
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
return cursor.moveToFirst() && cursor.getInt(0) != 0;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasEmptySchema(SupportSQLiteDatabase db) {
|
||||
Cursor cursor = db.query(
|
||||
"SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'");
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
return cursor.moveToFirst() && cursor.getInt(0) == 0;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public abstract static class Delegate {
|
||||
public final int version;
|
||||
|
||||
public Delegate(int version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
protected abstract void dropAllTables(SupportSQLiteDatabase database);
|
||||
|
||||
protected abstract void createAllTables(SupportSQLiteDatabase database);
|
||||
|
||||
protected abstract void onOpen(SupportSQLiteDatabase database);
|
||||
|
||||
protected abstract void onCreate(SupportSQLiteDatabase database);
|
||||
|
||||
/**
|
||||
* Called after a migration run to validate database integrity.
|
||||
*
|
||||
* @param db The SQLite database.
|
||||
*
|
||||
* @deprecated Use {@link #onValidateSchema(SupportSQLiteDatabase)}
|
||||
*/
|
||||
@Deprecated
|
||||
protected void validateMigration(SupportSQLiteDatabase db) {
|
||||
throw new UnsupportedOperationException("validateMigration is deprecated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a migration run or pre-package database copy to validate database integrity.
|
||||
*
|
||||
* @param db The SQLite database.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@NonNull
|
||||
protected ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) {
|
||||
validateMigration(db);
|
||||
return new ValidationResult(true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before migrations execute to perform preliminary work.
|
||||
* @param database The SQLite database.
|
||||
*/
|
||||
protected void onPreMigrate(SupportSQLiteDatabase database) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after migrations execute to perform additional work.
|
||||
* @param database The SQLite database.
|
||||
*/
|
||||
protected void onPostMigrate(SupportSQLiteDatabase database) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public static class ValidationResult {
|
||||
|
||||
public final boolean isValid;
|
||||
@Nullable
|
||||
public final String expectedFoundMsg;
|
||||
|
||||
public ValidationResult(boolean isValid, @Nullable String expectedFoundMsg) {
|
||||
this.isValid = isValid;
|
||||
this.expectedFoundMsg = expectedFoundMsg;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.room.util.useCursor
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
|
||||
/**
|
||||
* An open helper that holds a reference to the configuration until the database is opened.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
open class RoomOpenHelper(
|
||||
configuration: DatabaseConfiguration,
|
||||
delegate: Delegate,
|
||||
identityHash: String,
|
||||
legacyHash: String
|
||||
) : SupportSQLiteOpenHelper.Callback(delegate.version) {
|
||||
private var configuration: DatabaseConfiguration?
|
||||
private val delegate: Delegate
|
||||
private val identityHash: String
|
||||
|
||||
/**
|
||||
* Room v1 had a bug where the hash was not consistent if fields are reordered.
|
||||
* The new has fixes it but we still need to accept the legacy hash.
|
||||
*/
|
||||
// b/64290754
|
||||
private val legacyHash: String
|
||||
|
||||
init {
|
||||
this.configuration = configuration
|
||||
this.delegate = delegate
|
||||
this.identityHash = identityHash
|
||||
this.legacyHash = legacyHash
|
||||
}
|
||||
|
||||
constructor(
|
||||
configuration: DatabaseConfiguration,
|
||||
delegate: Delegate,
|
||||
legacyHash: String
|
||||
) : this(configuration, delegate, "", legacyHash)
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
super.onConfigure(db)
|
||||
}
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
val isEmptyDatabase = hasEmptySchema(db)
|
||||
delegate.createAllTables(db)
|
||||
if (!isEmptyDatabase) {
|
||||
// A 0 version pre-populated database goes through the create path because the
|
||||
// framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
|
||||
// find the database not to be empty, then it is a pre-populated, we must validate it to
|
||||
// see if its suitable for usage.
|
||||
val result = delegate.onValidateSchema(db)
|
||||
if (!result.isValid) {
|
||||
throw IllegalStateException(
|
||||
"Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}"
|
||||
)
|
||||
}
|
||||
}
|
||||
updateIdentity(db)
|
||||
delegate.onCreate(db)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
var migrated = false
|
||||
configuration?.let { config ->
|
||||
val migrations = config.migrationContainer.findMigrationPath(
|
||||
oldVersion, newVersion
|
||||
)
|
||||
if (migrations != null) {
|
||||
delegate.onPreMigrate(db)
|
||||
migrations.forEach { it.migrate(db) }
|
||||
val result = delegate.onValidateSchema(db)
|
||||
if (!result.isValid) {
|
||||
throw IllegalStateException(
|
||||
("Migration didn't properly handle: " +
|
||||
result.expectedFoundMsg)
|
||||
)
|
||||
}
|
||||
delegate.onPostMigrate(db)
|
||||
updateIdentity(db)
|
||||
migrated = true
|
||||
}
|
||||
}
|
||||
if (!migrated) {
|
||||
val config = this.configuration
|
||||
if (config != null && !config.isMigrationRequired(oldVersion, newVersion)) {
|
||||
delegate.dropAllTables(db)
|
||||
delegate.createAllTables(db)
|
||||
} else {
|
||||
throw IllegalStateException(
|
||||
"A migration from $oldVersion to $newVersion was required but not found. " +
|
||||
"Please provide the " +
|
||||
"necessary Migration path via " +
|
||||
"RoomDatabase.Builder.addMigration(Migration ...) or allow for " +
|
||||
"destructive migrations via one of the " +
|
||||
"RoomDatabase.Builder.fallbackToDestructiveMigration* methods."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
onUpgrade(db, oldVersion, newVersion)
|
||||
}
|
||||
|
||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||
super.onOpen(db)
|
||||
checkIdentity(db)
|
||||
delegate.onOpen(db)
|
||||
// there might be too many configurations etc, just clear it.
|
||||
configuration = null
|
||||
}
|
||||
|
||||
private fun checkIdentity(db: SupportSQLiteDatabase) {
|
||||
if (hasRoomMasterTable(db)) {
|
||||
val identityHash: String? = db.query(
|
||||
SimpleSQLiteQuery(RoomMasterTable.READ_QUERY)
|
||||
).useCursor { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.getString(0)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (this.identityHash != identityHash && this.legacyHash != identityHash) {
|
||||
throw IllegalStateException(
|
||||
"Room cannot verify the data integrity. Looks like" +
|
||||
" you've changed schema but forgot to update the version number. You can" +
|
||||
" simply fix this by increasing the version number. Expected identity" +
|
||||
" hash: ${ this.identityHash }, found: $identityHash"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// No room_master_table, this might an a pre-populated DB, we must validate to see if
|
||||
// its suitable for usage.
|
||||
val result = delegate.onValidateSchema(db)
|
||||
if (!result.isValid) {
|
||||
throw IllegalStateException(
|
||||
"Pre-packaged database has an invalid schema: ${result.expectedFoundMsg}"
|
||||
)
|
||||
}
|
||||
delegate.onPostMigrate(db)
|
||||
updateIdentity(db)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIdentity(db: SupportSQLiteDatabase) {
|
||||
createMasterTableIfNotExists(db)
|
||||
db.execSQL(RoomMasterTable.createInsertQuery(identityHash))
|
||||
}
|
||||
|
||||
private fun createMasterTableIfNotExists(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(RoomMasterTable.CREATE_QUERY)
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
abstract class Delegate(@JvmField val version: Int) {
|
||||
abstract fun dropAllTables(db: SupportSQLiteDatabase)
|
||||
abstract fun createAllTables(db: SupportSQLiteDatabase)
|
||||
abstract fun onOpen(db: SupportSQLiteDatabase)
|
||||
abstract fun onCreate(db: SupportSQLiteDatabase)
|
||||
|
||||
/**
|
||||
* Called after a migration run to validate database integrity.
|
||||
*
|
||||
* @param db The SQLite database.
|
||||
*/
|
||||
@Deprecated("Use [onValidateSchema(SupportSQLiteDatabase)]")
|
||||
protected open fun validateMigration(db: SupportSQLiteDatabase) {
|
||||
throw UnsupportedOperationException("validateMigration is deprecated")
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a migration run or pre-package database copy to validate database integrity.
|
||||
*
|
||||
* @param db The SQLite database.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
open fun onValidateSchema(db: SupportSQLiteDatabase): ValidationResult {
|
||||
validateMigration(db)
|
||||
return ValidationResult(true, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before migrations execute to perform preliminary work.
|
||||
* @param database The SQLite database.
|
||||
*/
|
||||
open fun onPreMigrate(db: SupportSQLiteDatabase) {}
|
||||
|
||||
/**
|
||||
* Called after migrations execute to perform additional work.
|
||||
* @param database The SQLite database.
|
||||
*/
|
||||
open fun onPostMigrate(db: SupportSQLiteDatabase) {}
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
open class ValidationResult(
|
||||
@JvmField val isValid: Boolean,
|
||||
@JvmField val expectedFoundMsg: String?
|
||||
)
|
||||
companion object {
|
||||
internal fun hasRoomMasterTable(db: SupportSQLiteDatabase): Boolean {
|
||||
db.query(
|
||||
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND " +
|
||||
"name='${ RoomMasterTable.TABLE_NAME }'"
|
||||
).useCursor { cursor ->
|
||||
return cursor.moveToFirst() && cursor.getInt(0) != 0
|
||||
}
|
||||
}
|
||||
|
||||
internal fun hasEmptySchema(db: SupportSQLiteDatabase): Boolean {
|
||||
db.query(
|
||||
"SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'"
|
||||
).useCursor { cursor ->
|
||||
return cursor.moveToFirst() && cursor.getInt(0) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,299 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.sqlite.db.SupportSQLiteProgram;
|
||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* This class is used as an intermediate place to keep binding arguments so that we can run
|
||||
* Cursor queries with correct types rather than passing everything as a string.
|
||||
* <p>
|
||||
* Because it is relatively a big object, they are pooled and must be released after each use.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
// Maximum number of queries we'll keep cached.
|
||||
static final int POOL_LIMIT = 15;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
// Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
|
||||
// clear the bigger queries (# of arguments).
|
||||
static final int DESIRED_POOL_SIZE = 10;
|
||||
private volatile String mQuery;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
final long[] mLongBindings;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
final double[] mDoubleBindings;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
final String[] mStringBindings;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
final byte[][] mBlobBindings;
|
||||
|
||||
@Binding
|
||||
private final int[] mBindingTypes;
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
final int mCapacity;
|
||||
// number of arguments in the query
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
int mArgCount;
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();
|
||||
|
||||
/**
|
||||
* Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
|
||||
*
|
||||
* @param supportSQLiteQuery The query to copy from
|
||||
* @return A new query copied from the provided one.
|
||||
*/
|
||||
public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
|
||||
final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
|
||||
supportSQLiteQuery.getSql(),
|
||||
supportSQLiteQuery.getArgCount());
|
||||
supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
|
||||
@Override
|
||||
public void bindNull(int index) {
|
||||
query.bindNull(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindLong(int index, long value) {
|
||||
query.bindLong(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindDouble(int index, double value) {
|
||||
query.bindDouble(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindString(int index, String value) {
|
||||
query.bindString(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindBlob(int index, byte[] value) {
|
||||
query.bindBlob(index, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearBindings() {
|
||||
query.clearBindings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// ignored.
|
||||
}
|
||||
});
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
|
||||
* given query.
|
||||
*
|
||||
* @param query The query to prepare
|
||||
* @param argumentCount The number of query arguments
|
||||
* @return A RoomSQLiteQuery that holds the given query and has space for the given number of
|
||||
* arguments.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static RoomSQLiteQuery acquire(String query, int argumentCount) {
|
||||
synchronized (sQueryPool) {
|
||||
final Map.Entry<Integer, RoomSQLiteQuery> entry =
|
||||
sQueryPool.ceilingEntry(argumentCount);
|
||||
if (entry != null) {
|
||||
sQueryPool.remove(entry.getKey());
|
||||
final RoomSQLiteQuery sqliteQuery = entry.getValue();
|
||||
sqliteQuery.init(query, argumentCount);
|
||||
return sqliteQuery;
|
||||
}
|
||||
}
|
||||
RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
|
||||
sqLiteQuery.init(query, argumentCount);
|
||||
return sqLiteQuery;
|
||||
}
|
||||
|
||||
private RoomSQLiteQuery(int capacity) {
|
||||
mCapacity = capacity;
|
||||
// because, 1 based indices... we don't want to offsets everything with 1 all the time.
|
||||
int limit = capacity + 1;
|
||||
//noinspection WrongConstant
|
||||
mBindingTypes = new int[limit];
|
||||
mLongBindings = new long[limit];
|
||||
mDoubleBindings = new double[limit];
|
||||
mStringBindings = new String[limit];
|
||||
mBlobBindings = new byte[limit][];
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
void init(String query, int argCount) {
|
||||
mQuery = query;
|
||||
mArgCount = argCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the query back to the pool.
|
||||
* <p>
|
||||
* After released, the statement might be returned when {@link #acquire(String, int)} is called
|
||||
* so you should never re-use it after releasing.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public void release() {
|
||||
synchronized (sQueryPool) {
|
||||
sQueryPool.put(mCapacity, this);
|
||||
prunePoolLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static void prunePoolLocked() {
|
||||
if (sQueryPool.size() > POOL_LIMIT) {
|
||||
int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
|
||||
final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator();
|
||||
while (toBeRemoved-- > 0) {
|
||||
iterator.next();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSql() {
|
||||
return mQuery;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getArgCount() {
|
||||
return mArgCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindTo(SupportSQLiteProgram program) {
|
||||
for (int index = 1; index <= mArgCount; index++) {
|
||||
switch (mBindingTypes[index]) {
|
||||
case NULL:
|
||||
program.bindNull(index);
|
||||
break;
|
||||
case LONG:
|
||||
program.bindLong(index, mLongBindings[index]);
|
||||
break;
|
||||
case DOUBLE:
|
||||
program.bindDouble(index, mDoubleBindings[index]);
|
||||
break;
|
||||
case STRING:
|
||||
program.bindString(index, mStringBindings[index]);
|
||||
break;
|
||||
case BLOB:
|
||||
program.bindBlob(index, mBlobBindings[index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindNull(int index) {
|
||||
mBindingTypes[index] = NULL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindLong(int index, long value) {
|
||||
mBindingTypes[index] = LONG;
|
||||
mLongBindings[index] = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindDouble(int index, double value) {
|
||||
mBindingTypes[index] = DOUBLE;
|
||||
mDoubleBindings[index] = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindString(int index, String value) {
|
||||
mBindingTypes[index] = STRING;
|
||||
mStringBindings[index] = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindBlob(int index, byte[] value) {
|
||||
mBindingTypes[index] = BLOB;
|
||||
mBlobBindings[index] = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op. not calling release because it is internal API.
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies arguments from another RoomSQLiteQuery into this query.
|
||||
*
|
||||
* @param other The other query, which holds the arguments to be copied.
|
||||
*/
|
||||
public void copyArgumentsFrom(RoomSQLiteQuery other) {
|
||||
int argCount = other.getArgCount() + 1; // +1 for the binding offsets
|
||||
System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
|
||||
System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
|
||||
System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
|
||||
System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
|
||||
System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearBindings() {
|
||||
Arrays.fill(mBindingTypes, NULL);
|
||||
Arrays.fill(mStringBindings, null);
|
||||
Arrays.fill(mBlobBindings, null);
|
||||
mQuery = null;
|
||||
// no need to clear others
|
||||
}
|
||||
|
||||
private static final int NULL = 1;
|
||||
private static final int LONG = 2;
|
||||
private static final int DOUBLE = 3;
|
||||
private static final int STRING = 4;
|
||||
private static final int BLOB = 5;
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
|
||||
@interface Binding {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.sqlite.db.SupportSQLiteProgram
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import java.util.Arrays
|
||||
import java.util.TreeMap
|
||||
|
||||
/**
|
||||
* This class is used as an intermediate place to keep binding arguments so that we can run
|
||||
* Cursor queries with correct types rather than passing everything as a string.
|
||||
*
|
||||
* Because it is relatively a big object, they are pooled and must be released after each use.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
class RoomSQLiteQuery private constructor(
|
||||
@field:VisibleForTesting val capacity: Int
|
||||
) : SupportSQLiteQuery, SupportSQLiteProgram {
|
||||
@Volatile
|
||||
private var query: String? = null
|
||||
|
||||
@JvmField
|
||||
@VisibleForTesting
|
||||
val longBindings: LongArray
|
||||
|
||||
@JvmField
|
||||
@VisibleForTesting
|
||||
val doubleBindings: DoubleArray
|
||||
|
||||
@JvmField
|
||||
@VisibleForTesting
|
||||
val stringBindings: Array<String?>
|
||||
|
||||
@JvmField
|
||||
@VisibleForTesting
|
||||
val blobBindings: Array<ByteArray?>
|
||||
|
||||
@Binding
|
||||
private val bindingTypes: IntArray
|
||||
|
||||
// number of arguments in the query
|
||||
override var argCount = 0
|
||||
private set
|
||||
|
||||
fun init(query: String, initArgCount: Int) {
|
||||
this.query = query
|
||||
argCount = initArgCount
|
||||
}
|
||||
|
||||
init {
|
||||
// because, 1 based indices... we don't want to offsets everything with 1 all the time.
|
||||
val limit = capacity + 1
|
||||
bindingTypes = IntArray(limit)
|
||||
longBindings = LongArray(limit)
|
||||
doubleBindings = DoubleArray(limit)
|
||||
stringBindings = arrayOfNulls(limit)
|
||||
blobBindings = arrayOfNulls(limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the query back to the pool.
|
||||
*
|
||||
* After released, the statement might be returned when [.acquire] is called
|
||||
* so you should never re-use it after releasing.
|
||||
*/
|
||||
fun release() {
|
||||
synchronized(queryPool) {
|
||||
queryPool[capacity] = this
|
||||
prunePoolLocked()
|
||||
}
|
||||
}
|
||||
|
||||
override val sql: String
|
||||
get() = checkNotNull(this.query)
|
||||
|
||||
override fun bindTo(statement: SupportSQLiteProgram) {
|
||||
for (index in 1..argCount) {
|
||||
when (bindingTypes[index]) {
|
||||
NULL -> statement.bindNull(index)
|
||||
LONG -> statement.bindLong(index, longBindings[index])
|
||||
DOUBLE -> statement.bindDouble(index, doubleBindings[index])
|
||||
STRING -> statement.bindString(index, requireNotNull(stringBindings[index]))
|
||||
BLOB -> statement.bindBlob(index, requireNotNull(blobBindings[index]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindNull(index: Int) {
|
||||
bindingTypes[index] = NULL
|
||||
}
|
||||
|
||||
override fun bindLong(index: Int, value: Long) {
|
||||
bindingTypes[index] = LONG
|
||||
longBindings[index] = value
|
||||
}
|
||||
|
||||
override fun bindDouble(index: Int, value: Double) {
|
||||
bindingTypes[index] = DOUBLE
|
||||
doubleBindings[index] = value
|
||||
}
|
||||
|
||||
override fun bindString(index: Int, value: String) {
|
||||
bindingTypes[index] = STRING
|
||||
stringBindings[index] = value
|
||||
}
|
||||
|
||||
override fun bindBlob(index: Int, value: ByteArray) {
|
||||
bindingTypes[index] = BLOB
|
||||
blobBindings[index] = value
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// no-op. not calling release because it is internal API.
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies arguments from another RoomSQLiteQuery into this query.
|
||||
*
|
||||
* @param other The other query, which holds the arguments to be copied.
|
||||
*/
|
||||
fun copyArgumentsFrom(other: RoomSQLiteQuery) {
|
||||
val argCount = other.argCount + 1 // +1 for the binding offsets
|
||||
System.arraycopy(other.bindingTypes, 0, bindingTypes, 0, argCount)
|
||||
System.arraycopy(other.longBindings, 0, longBindings, 0, argCount)
|
||||
System.arraycopy(other.stringBindings, 0, stringBindings, 0, argCount)
|
||||
System.arraycopy(other.blobBindings, 0, blobBindings, 0, argCount)
|
||||
System.arraycopy(other.doubleBindings, 0, doubleBindings, 0, argCount)
|
||||
}
|
||||
|
||||
override fun clearBindings() {
|
||||
Arrays.fill(bindingTypes, NULL)
|
||||
Arrays.fill(stringBindings, null)
|
||||
Arrays.fill(blobBindings, null)
|
||||
query = null
|
||||
// no need to clear others
|
||||
}
|
||||
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@IntDef(NULL, LONG, DOUBLE, STRING, BLOB)
|
||||
internal annotation class Binding
|
||||
|
||||
companion object {
|
||||
// Maximum number of queries we'll keep cached.
|
||||
@VisibleForTesting
|
||||
const val POOL_LIMIT = 15
|
||||
|
||||
// Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
|
||||
// clear the bigger queries (# of arguments).
|
||||
@VisibleForTesting
|
||||
const val DESIRED_POOL_SIZE = 10
|
||||
|
||||
@JvmField
|
||||
@VisibleForTesting
|
||||
val queryPool = TreeMap<Int, RoomSQLiteQuery>()
|
||||
|
||||
/**
|
||||
* Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
|
||||
*
|
||||
* @param supportSQLiteQuery The query to copy from
|
||||
* @return A new query copied from the provided one.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun copyFrom(supportSQLiteQuery: SupportSQLiteQuery): RoomSQLiteQuery {
|
||||
val query = acquire(
|
||||
supportSQLiteQuery.sql,
|
||||
supportSQLiteQuery.argCount
|
||||
)
|
||||
|
||||
supportSQLiteQuery.bindTo(object : SupportSQLiteProgram by query {})
|
||||
return query
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
|
||||
* given query.
|
||||
*
|
||||
* @param query The query to prepare
|
||||
* @param argumentCount The number of query arguments
|
||||
* @return A RoomSQLiteQuery that holds the given query and has space for the given number
|
||||
* of arguments.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun acquire(query: String, argumentCount: Int): RoomSQLiteQuery {
|
||||
synchronized(queryPool) {
|
||||
val entry = queryPool.ceilingEntry(argumentCount)
|
||||
if (entry != null) {
|
||||
queryPool.remove(entry.key)
|
||||
val sqliteQuery = entry.value
|
||||
sqliteQuery.init(query, argumentCount)
|
||||
return sqliteQuery
|
||||
}
|
||||
}
|
||||
val sqLiteQuery = RoomSQLiteQuery(argumentCount)
|
||||
sqLiteQuery.init(query, argumentCount)
|
||||
return sqLiteQuery
|
||||
}
|
||||
|
||||
internal fun prunePoolLocked() {
|
||||
if (queryPool.size > POOL_LIMIT) {
|
||||
var toBeRemoved = queryPool.size - DESIRED_POOL_SIZE
|
||||
val iterator = queryPool.descendingKeySet().iterator()
|
||||
while (toBeRemoved-- > 0) {
|
||||
iterator.next()
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val NULL = 1
|
||||
private const val LONG = 2
|
||||
private const val DOUBLE = 3
|
||||
private const val STRING = 4
|
||||
private const val BLOB = 5
|
||||
}
|
||||
}
|
|
@ -1,173 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.arch.core.executor.ArchTaskExecutor;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* A LiveData implementation that closely works with {@link InvalidationTracker} to implement
|
||||
* database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long
|
||||
* as they are active.
|
||||
* <p>
|
||||
* We need this extra handling for {@link androidx.lifecycle.LiveData} because when they are
|
||||
* observed forever, there is no {@link androidx.lifecycle.Lifecycle} that will keep them in
|
||||
* memory but they should stay. We cannot add-remove observer in {@link LiveData#onActive()},
|
||||
* {@link LiveData#onInactive()} because that would mean missing changes in between or doing an
|
||||
* extra query on every UI rotation.
|
||||
* <p>
|
||||
* This {@link LiveData} keeps a weak observer to the {@link InvalidationTracker} but it is hold
|
||||
* strongly by the {@link InvalidationTracker} as long as it is active.
|
||||
*/
|
||||
class RoomTrackingLiveData<T> extends LiveData<T> {
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final RoomDatabase mDatabase;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final boolean mInTransaction;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final Callable<T> mComputeFunction;
|
||||
|
||||
private final InvalidationLiveDataContainer mContainer;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final InvalidationTracker.Observer mObserver;
|
||||
|
||||
private int queued = 0;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final AtomicBoolean mRegisteredObserver = new AtomicBoolean(false);
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final Runnable mRefreshRunnable = new Runnable() {
|
||||
@WorkerThread
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (lock) {
|
||||
queued--;
|
||||
if (queued < 0) {
|
||||
eu.faircode.email.Log.e(mComputeFunction + " queued=" + queued);
|
||||
queued = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (mRegisteredObserver.compareAndSet(false, true)) {
|
||||
mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
|
||||
}
|
||||
|
||||
T value = null;
|
||||
boolean computed = false;
|
||||
synchronized (mComputeFunction) {
|
||||
int retry = 0;
|
||||
while (!computed) {
|
||||
try {
|
||||
value = mComputeFunction.call();
|
||||
computed = true;
|
||||
} catch (Throwable e) {
|
||||
if (++retry > 5) {
|
||||
eu.faircode.email.Log.e(e);
|
||||
break;
|
||||
}
|
||||
eu.faircode.email.Log.w(e);
|
||||
try {
|
||||
Thread.sleep(2000L);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (computed) {
|
||||
postValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
final Runnable mInvalidationRunnable = new Runnable() {
|
||||
@MainThread
|
||||
@Override
|
||||
public void run() {
|
||||
boolean isActive = hasActiveObservers();
|
||||
if (isActive)
|
||||
synchronized (lock) {
|
||||
if (queued > 0)
|
||||
eu.faircode.email.Log.persist(eu.faircode.email.EntityLog.Type.Debug,
|
||||
mComputeFunction + " queued=" + queued);
|
||||
else {
|
||||
queued++;
|
||||
getQueryExecutor().execute(mRefreshRunnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
RoomTrackingLiveData(
|
||||
RoomDatabase database,
|
||||
InvalidationLiveDataContainer container,
|
||||
boolean inTransaction,
|
||||
Callable<T> computeFunction,
|
||||
String[] tableNames) {
|
||||
mDatabase = database;
|
||||
mInTransaction = inTransaction;
|
||||
mComputeFunction = computeFunction;
|
||||
mContainer = container;
|
||||
mObserver = new InvalidationTracker.Observer(tableNames) {
|
||||
@Override
|
||||
public void onInvalidated(@NonNull Set<String> tables) {
|
||||
ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActive() {
|
||||
super.onActive();
|
||||
mContainer.onActive(this);
|
||||
synchronized (lock) {
|
||||
queued++;
|
||||
getQueryExecutor().execute(mRefreshRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInactive() {
|
||||
super.onInactive();
|
||||
mContainer.onInactive(this);
|
||||
}
|
||||
|
||||
Executor getQueryExecutor() {
|
||||
if (mInTransaction) {
|
||||
return mDatabase.getTransactionExecutor();
|
||||
} else {
|
||||
return mDatabase.getQueryExecutor();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.arch.core.executor.ArchTaskExecutor
|
||||
import androidx.lifecycle.LiveData
|
||||
import java.lang.Exception
|
||||
import java.lang.RuntimeException
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* A LiveData implementation that closely works with [InvalidationTracker] to implement
|
||||
* database drive [androidx.lifecycle.LiveData] queries that are strongly hold as long
|
||||
* as they are active.
|
||||
*
|
||||
* We need this extra handling for [androidx.lifecycle.LiveData] because when they are
|
||||
* observed forever, there is no [androidx.lifecycle.Lifecycle] that will keep them in
|
||||
* memory but they should stay. We cannot add-remove observer in [LiveData.onActive],
|
||||
* [LiveData.onInactive] because that would mean missing changes in between or doing an
|
||||
* extra query on every UI rotation.
|
||||
*
|
||||
* This [LiveData] keeps a weak observer to the [InvalidationTracker] but it is hold
|
||||
* strongly by the [InvalidationTracker] as long as it is active.
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
internal class RoomTrackingLiveData<T> (
|
||||
val database: RoomDatabase,
|
||||
private val container: InvalidationLiveDataContainer,
|
||||
val inTransaction: Boolean,
|
||||
val computeFunction: Callable<T?>,
|
||||
tableNames: Array<out String>
|
||||
) : LiveData<T>() {
|
||||
val observer: InvalidationTracker.Observer = object : InvalidationTracker.Observer(tableNames) {
|
||||
override fun onInvalidated(tables: Set<String>) {
|
||||
ArchTaskExecutor.getInstance().executeOnMainThread(invalidationRunnable)
|
||||
}
|
||||
}
|
||||
val invalid = AtomicBoolean(true)
|
||||
val computing = AtomicBoolean(false)
|
||||
val registeredObserver = AtomicBoolean(false)
|
||||
val refreshRunnable = Runnable {
|
||||
if (registeredObserver.compareAndSet(false, true)) {
|
||||
database.invalidationTracker.addWeakObserver(observer)
|
||||
}
|
||||
var computed: Boolean
|
||||
do {
|
||||
computed = false
|
||||
// compute can happen only in 1 thread but no reason to lock others.
|
||||
if (computing.compareAndSet(false, true)) {
|
||||
// as long as it is invalid, keep computing.
|
||||
try {
|
||||
var value: T? = null
|
||||
while (invalid.compareAndSet(true, false)) {
|
||||
computed = true
|
||||
try {
|
||||
value = computeFunction.call()
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(
|
||||
"Exception while computing database live data.",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
if (computed) {
|
||||
postValue(value)
|
||||
}
|
||||
} finally {
|
||||
// release compute lock
|
||||
computing.set(false)
|
||||
}
|
||||
}
|
||||
// check invalid after releasing compute lock to avoid the following scenario.
|
||||
// Thread A runs compute()
|
||||
// Thread A checks invalid, it is false
|
||||
// Main thread sets invalid to true
|
||||
// Thread B runs, fails to acquire compute lock and skips
|
||||
// Thread A releases compute lock
|
||||
// We've left invalid in set state. The check below recovers.
|
||||
} while (computed && invalid.get())
|
||||
}
|
||||
|
||||
val invalidationRunnable = Runnable {
|
||||
val isActive = hasActiveObservers()
|
||||
if (invalid.compareAndSet(false, true)) {
|
||||
if (isActive) {
|
||||
queryExecutor.execute(refreshRunnable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
container.onActive(this as LiveData<Any>)
|
||||
queryExecutor.execute(refreshRunnable)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
container.onInactive(this as LiveData<Any>)
|
||||
}
|
||||
|
||||
val queryExecutor: Executor
|
||||
get() = if (inTransaction) {
|
||||
database.transactionExecutor
|
||||
} else {
|
||||
database.queryExecutor
|
||||
}
|
||||
}
|
|
@ -1,289 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.room.util.CopyLock;
|
||||
import androidx.room.util.DBUtil;
|
||||
import androidx.room.util.FileUtil;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
|
||||
* storage.
|
||||
*/
|
||||
class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
|
||||
|
||||
@NonNull
|
||||
private final Context mContext;
|
||||
@Nullable
|
||||
private final String mCopyFromAssetPath;
|
||||
@Nullable
|
||||
private final File mCopyFromFile;
|
||||
@Nullable
|
||||
private final Callable<InputStream> mCopyFromInputStream;
|
||||
private final int mDatabaseVersion;
|
||||
@NonNull
|
||||
private final SupportSQLiteOpenHelper mDelegate;
|
||||
@Nullable
|
||||
private DatabaseConfiguration mDatabaseConfiguration;
|
||||
|
||||
private boolean mVerified;
|
||||
|
||||
SQLiteCopyOpenHelper(
|
||||
@NonNull Context context,
|
||||
@Nullable String copyFromAssetPath,
|
||||
@Nullable File copyFromFile,
|
||||
@Nullable Callable<InputStream> copyFromInputStream,
|
||||
int databaseVersion,
|
||||
@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) {
|
||||
mContext = context;
|
||||
mCopyFromAssetPath = copyFromAssetPath;
|
||||
mCopyFromFile = copyFromFile;
|
||||
mCopyFromInputStream = copyFromInputStream;
|
||||
mDatabaseVersion = databaseVersion;
|
||||
mDelegate = supportSQLiteOpenHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDatabaseName() {
|
||||
return mDelegate.getDatabaseName();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
public void setWriteAheadLoggingEnabled(boolean enabled) {
|
||||
mDelegate.setWriteAheadLoggingEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized SupportSQLiteDatabase getWritableDatabase() {
|
||||
if (!mVerified) {
|
||||
verifyDatabaseFile(true);
|
||||
mVerified = true;
|
||||
}
|
||||
return mDelegate.getWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized SupportSQLiteDatabase getReadableDatabase() {
|
||||
if (!mVerified) {
|
||||
verifyDatabaseFile(false);
|
||||
mVerified = true;
|
||||
}
|
||||
return mDelegate.getReadableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
mDelegate.close();
|
||||
mVerified = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public SupportSQLiteOpenHelper getDelegate() {
|
||||
return mDelegate;
|
||||
}
|
||||
|
||||
// Can't be constructor param because the factory is needed by the database builder which in
|
||||
// turn is the one that actually builds the configuration.
|
||||
void setDatabaseConfiguration(@Nullable DatabaseConfiguration databaseConfiguration) {
|
||||
mDatabaseConfiguration = databaseConfiguration;
|
||||
}
|
||||
|
||||
private void verifyDatabaseFile(boolean writable) {
|
||||
String databaseName = getDatabaseName();
|
||||
File databaseFile = mContext.getDatabasePath(databaseName);
|
||||
boolean processLevelLock = mDatabaseConfiguration == null
|
||||
|| mDatabaseConfiguration.multiInstanceInvalidation;
|
||||
CopyLock copyLock = new CopyLock(databaseName, mContext.getFilesDir(), processLevelLock);
|
||||
try {
|
||||
// Acquire a copy lock, this lock works across threads and processes, preventing
|
||||
// concurrent copy attempts from occurring.
|
||||
copyLock.lock();
|
||||
|
||||
if (!databaseFile.exists()) {
|
||||
try {
|
||||
// No database file found, copy and be done.
|
||||
copyDatabaseFile(databaseFile, writable);
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to copy database file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (mDatabaseConfiguration == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A database file is present, check if we need to re-copy it.
|
||||
int currentVersion;
|
||||
try {
|
||||
currentVersion = DBUtil.readVersion(databaseFile);
|
||||
} catch (IOException e) {
|
||||
Log.w(Room.LOG_TAG, "Unable to read database version.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentVersion == mDatabaseVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDatabaseConfiguration.isMigrationRequired(currentVersion, mDatabaseVersion)) {
|
||||
// From the current version to the desired version a migration is required, i.e.
|
||||
// we won't be performing a copy destructive migration.
|
||||
return;
|
||||
}
|
||||
|
||||
if (mContext.deleteDatabase(databaseName)) {
|
||||
try {
|
||||
copyDatabaseFile(databaseFile, writable);
|
||||
} catch (IOException e) {
|
||||
// We are more forgiving copying a database on a destructive migration since
|
||||
// there is already a database file that can be opened.
|
||||
Log.w(Room.LOG_TAG, "Unable to copy database file.", e);
|
||||
}
|
||||
} else {
|
||||
Log.w(Room.LOG_TAG, "Failed to delete database file ("
|
||||
+ databaseName + ") for a copy destructive migration.");
|
||||
}
|
||||
} finally {
|
||||
copyLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDatabaseFile(File destinationFile, boolean writable) throws IOException {
|
||||
ReadableByteChannel input;
|
||||
if (mCopyFromAssetPath != null) {
|
||||
input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath));
|
||||
} else if (mCopyFromFile != null) {
|
||||
input = new FileInputStream(mCopyFromFile).getChannel();
|
||||
} else if (mCopyFromInputStream != null) {
|
||||
final InputStream inputStream;
|
||||
try {
|
||||
inputStream = mCopyFromInputStream.call();
|
||||
} catch (Exception e) {
|
||||
throw new IOException("inputStreamCallable exception on call", e);
|
||||
}
|
||||
input = Channels.newChannel(inputStream);
|
||||
} else {
|
||||
throw new IllegalStateException("copyFromAssetPath, copyFromFile and "
|
||||
+ "copyFromInputStream are all null!");
|
||||
}
|
||||
|
||||
// An intermediate file is used so that we never end up with a half-copied database file
|
||||
// in the internal directory.
|
||||
//File intermediateFile = File.createTempFile(
|
||||
// "room-copy-helper", ".tmp", mContext.getCacheDir());
|
||||
File dir = new File(mContext.getFilesDir(), "room");
|
||||
if (!dir.exists() && !dir.mkdirs())
|
||||
throw new IOException("Failed to create=" + dir);
|
||||
File intermediateFile = new File(dir, "room-copy-helper.tmp");
|
||||
intermediateFile.deleteOnExit();
|
||||
FileChannel output = new FileOutputStream(intermediateFile).getChannel();
|
||||
FileUtil.copy(input, output);
|
||||
|
||||
File parent = destinationFile.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
throw new IOException("Failed to create directories for "
|
||||
+ destinationFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
// Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch
|
||||
// the open pre-packaged callback. If it fails then intermediate file won't be copied making
|
||||
// invoking pre-packaged callback a transactional operation.
|
||||
dispatchOnOpenPrepackagedDatabase(intermediateFile, writable);
|
||||
|
||||
if (!intermediateFile.renameTo(destinationFile)) {
|
||||
throw new IOException("Failed to move intermediate file ("
|
||||
+ intermediateFile.getAbsolutePath() + ") to destination ("
|
||||
+ destinationFile.getAbsolutePath() + ").");
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchOnOpenPrepackagedDatabase(File databaseFile, boolean writable) {
|
||||
if (mDatabaseConfiguration == null
|
||||
|| mDatabaseConfiguration.prepackagedDatabaseCallback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SupportSQLiteOpenHelper helper = createFrameworkOpenHelper(databaseFile);
|
||||
try {
|
||||
SupportSQLiteDatabase db = writable ? helper.getWritableDatabase() :
|
||||
helper.getReadableDatabase();
|
||||
mDatabaseConfiguration.prepackagedDatabaseCallback.onOpenPrepackagedDatabase(db);
|
||||
} finally {
|
||||
// Close the db and let Room re-open it through a normal path
|
||||
helper.close();
|
||||
}
|
||||
}
|
||||
|
||||
private SupportSQLiteOpenHelper createFrameworkOpenHelper(File databaseFile) {
|
||||
final int version;
|
||||
try {
|
||||
version = DBUtil.readVersion(databaseFile);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Malformed database file, unable to read version.", e);
|
||||
}
|
||||
FrameworkSQLiteOpenHelperFactory factory = new FrameworkSQLiteOpenHelperFactory();
|
||||
Configuration configuration = Configuration.builder(mContext)
|
||||
.name(databaseFile.getAbsolutePath())
|
||||
.callback(new Callback(Math.max(version, 1)) {
|
||||
@Override
|
||||
public void onCreate(@NonNull SupportSQLiteDatabase db) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion,
|
||||
int newVersion) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(@NonNull SupportSQLiteDatabase db) {
|
||||
// If pre-packaged database has a version < 1 we will open it as if it was
|
||||
// version 1 because the framework open helper does not allow version < 1.
|
||||
// The database will be considered as newly created and onCreate() will be
|
||||
// invoked, but we do nothing and reset the version back so Room later runs
|
||||
// migrations as usual.
|
||||
if (version < 1) {
|
||||
db.setVersion(version);
|
||||
}
|
||||
}
|
||||
})
|
||||
.build();
|
||||
return factory.create(configuration);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.room.Room.LOG_TAG
|
||||
import androidx.room.util.copy
|
||||
import androidx.room.util.readVersion
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.sqlite.util.ProcessLock
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.lang.Exception
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.RuntimeException
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.ReadableByteChannel
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
/**
|
||||
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
|
||||
* storage.
|
||||
*/
|
||||
@Suppress("BanSynchronizedMethods")
|
||||
internal class SQLiteCopyOpenHelper(
|
||||
private val context: Context,
|
||||
private val copyFromAssetPath: String?,
|
||||
private val copyFromFile: File?,
|
||||
private val copyFromInputStream: Callable<InputStream>?,
|
||||
private val databaseVersion: Int,
|
||||
override val delegate: SupportSQLiteOpenHelper
|
||||
) : SupportSQLiteOpenHelper, DelegatingOpenHelper {
|
||||
private lateinit var databaseConfiguration: DatabaseConfiguration
|
||||
private var verified = false
|
||||
|
||||
override val databaseName: String?
|
||||
get() = delegate.databaseName
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||
override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
|
||||
delegate.setWriteAheadLoggingEnabled(enabled)
|
||||
}
|
||||
|
||||
override val writableDatabase: SupportSQLiteDatabase
|
||||
get() {
|
||||
if (!verified) {
|
||||
verifyDatabaseFile(true)
|
||||
verified = true
|
||||
}
|
||||
return delegate.writableDatabase
|
||||
}
|
||||
|
||||
override val readableDatabase: SupportSQLiteDatabase
|
||||
get() {
|
||||
if (!verified) {
|
||||
verifyDatabaseFile(false)
|
||||
verified = true
|
||||
}
|
||||
return delegate.readableDatabase
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun close() {
|
||||
delegate.close()
|
||||
verified = false
|
||||
}
|
||||
|
||||
// Can't be constructor param because the factory is needed by the database builder which in
|
||||
// turn is the one that actually builds the configuration.
|
||||
fun setDatabaseConfiguration(databaseConfiguration: DatabaseConfiguration) {
|
||||
this.databaseConfiguration = databaseConfiguration
|
||||
}
|
||||
|
||||
private fun verifyDatabaseFile(writable: Boolean) {
|
||||
val name = checkNotNull(databaseName)
|
||||
val databaseFile = context.getDatabasePath(name)
|
||||
val processLevelLock = (databaseConfiguration.multiInstanceInvalidation)
|
||||
val copyLock = ProcessLock(
|
||||
name,
|
||||
context.filesDir,
|
||||
processLevelLock
|
||||
)
|
||||
try {
|
||||
// Acquire a copy lock, this lock works across threads and processes, preventing
|
||||
// concurrent copy attempts from occurring.
|
||||
copyLock.lock()
|
||||
if (!databaseFile.exists()) {
|
||||
try {
|
||||
// No database file found, copy and be done.
|
||||
copyDatabaseFile(databaseFile, writable)
|
||||
return
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException("Unable to copy database file.", e)
|
||||
}
|
||||
}
|
||||
|
||||
// A database file is present, check if we need to re-copy it.
|
||||
val currentVersion = try {
|
||||
readVersion(databaseFile)
|
||||
} catch (e: IOException) {
|
||||
Log.w(LOG_TAG, "Unable to read database version.", e)
|
||||
return
|
||||
}
|
||||
if (currentVersion == databaseVersion) {
|
||||
return
|
||||
}
|
||||
if (databaseConfiguration.isMigrationRequired(currentVersion, databaseVersion)) {
|
||||
// From the current version to the desired version a migration is required, i.e.
|
||||
// we won't be performing a copy destructive migration.
|
||||
return
|
||||
}
|
||||
if (context.deleteDatabase(name)) {
|
||||
try {
|
||||
copyDatabaseFile(databaseFile, writable)
|
||||
} catch (e: IOException) {
|
||||
// We are more forgiving copying a database on a destructive migration since
|
||||
// there is already a database file that can be opened.
|
||||
Log.w(LOG_TAG, "Unable to copy database file.", e)
|
||||
}
|
||||
} else {
|
||||
Log.w(
|
||||
LOG_TAG, "Failed to delete database file ($name) for " +
|
||||
"a copy destructive migration."
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
copyLock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun copyDatabaseFile(destinationFile: File, writable: Boolean) {
|
||||
val input: ReadableByteChannel
|
||||
if (copyFromAssetPath != null) {
|
||||
input = Channels.newChannel(context.assets.open(copyFromAssetPath))
|
||||
} else if (copyFromFile != null) {
|
||||
input = FileInputStream(copyFromFile).channel
|
||||
} else if (copyFromInputStream != null) {
|
||||
val inputStream = try {
|
||||
copyFromInputStream.call()
|
||||
} catch (e: Exception) {
|
||||
throw IOException("inputStreamCallable exception on call", e)
|
||||
}
|
||||
input = Channels.newChannel(inputStream)
|
||||
} else {
|
||||
throw IllegalStateException(
|
||||
"copyFromAssetPath, copyFromFile and copyFromInputStream are all null!"
|
||||
)
|
||||
}
|
||||
|
||||
// An intermediate file is used so that we never end up with a half-copied database file
|
||||
// in the internal directory.
|
||||
val intermediateFile = File.createTempFile(
|
||||
"room-copy-helper", ".tmp", context.cacheDir
|
||||
)
|
||||
intermediateFile.deleteOnExit()
|
||||
val output = FileOutputStream(intermediateFile).channel
|
||||
copy(input, output)
|
||||
val parent = destinationFile.parentFile
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
throw IOException(
|
||||
"Failed to create directories for ${destinationFile.absolutePath}"
|
||||
)
|
||||
}
|
||||
|
||||
// Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch
|
||||
// the open pre-packaged callback. If it fails then intermediate file won't be copied making
|
||||
// invoking pre-packaged callback a transactional operation.
|
||||
dispatchOnOpenPrepackagedDatabase(intermediateFile, writable)
|
||||
if (!intermediateFile.renameTo(destinationFile)) {
|
||||
throw IOException(
|
||||
"Failed to move intermediate file (${intermediateFile.absolutePath}) to " +
|
||||
"destination (${destinationFile.absolutePath})."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchOnOpenPrepackagedDatabase(databaseFile: File, writable: Boolean) {
|
||||
if (databaseConfiguration.prepackagedDatabaseCallback == null
|
||||
) {
|
||||
return
|
||||
}
|
||||
createFrameworkOpenHelper(databaseFile).use { helper ->
|
||||
val db = if (writable) helper.writableDatabase else helper.readableDatabase
|
||||
databaseConfiguration.prepackagedDatabaseCallback!!.onOpenPrepackagedDatabase(db)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFrameworkOpenHelper(databaseFile: File): SupportSQLiteOpenHelper {
|
||||
val version = try {
|
||||
readVersion(databaseFile)
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException("Malformed database file, unable to read version.", e)
|
||||
}
|
||||
val factory = FrameworkSQLiteOpenHelperFactory()
|
||||
val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||
.name(databaseFile.absolutePath)
|
||||
.callback(object : SupportSQLiteOpenHelper.Callback(version.coerceAtLeast(1)) {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {}
|
||||
override fun onUpgrade(
|
||||
db: SupportSQLiteDatabase,
|
||||
oldVersion: Int,
|
||||
newVersion: Int
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||
// If pre-packaged database has a version < 1 we will open it as if it was
|
||||
// version 1 because the framework open helper does not allow version < 1.
|
||||
// The database will be considered as newly created and onCreate() will be
|
||||
// invoked, but we do nothing and reset the version back so Room later runs
|
||||
// migrations as usual.
|
||||
if (version < 1) {
|
||||
db.version = version
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
return factory.create(configuration)
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
|
||||
* {@link SQLiteCopyOpenHelper}.
|
||||
*/
|
||||
class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
|
||||
|
||||
@Nullable
|
||||
private final String mCopyFromAssetPath;
|
||||
@Nullable
|
||||
private final File mCopyFromFile;
|
||||
@Nullable
|
||||
private final Callable<InputStream> mCopyFromInputStream;
|
||||
@NonNull
|
||||
private final SupportSQLiteOpenHelper.Factory mDelegate;
|
||||
|
||||
SQLiteCopyOpenHelperFactory(
|
||||
@Nullable String copyFromAssetPath,
|
||||
@Nullable File copyFromFile,
|
||||
@Nullable Callable<InputStream> copyFromInputStream,
|
||||
@NonNull SupportSQLiteOpenHelper.Factory factory) {
|
||||
mCopyFromAssetPath = copyFromAssetPath;
|
||||
mCopyFromFile = copyFromFile;
|
||||
mCopyFromInputStream = copyFromInputStream;
|
||||
mDelegate = factory;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
|
||||
return new SQLiteCopyOpenHelper(
|
||||
configuration.context,
|
||||
mCopyFromAssetPath,
|
||||
mCopyFromFile,
|
||||
mCopyFromInputStream,
|
||||
configuration.callback.version,
|
||||
mDelegate.create(configuration));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
/**
|
||||
* Implementation of [SupportSQLiteOpenHelper.Factory] that creates
|
||||
* [SQLiteCopyOpenHelper].
|
||||
*/
|
||||
internal class SQLiteCopyOpenHelperFactory(
|
||||
private val mCopyFromAssetPath: String?,
|
||||
private val mCopyFromFile: File?,
|
||||
private val mCopyFromInputStream: Callable<InputStream>?,
|
||||
private val mDelegate: SupportSQLiteOpenHelper.Factory
|
||||
) : SupportSQLiteOpenHelper.Factory {
|
||||
override fun create(
|
||||
configuration: SupportSQLiteOpenHelper.Configuration
|
||||
): SupportSQLiteOpenHelper {
|
||||
return SQLiteCopyOpenHelper(
|
||||
configuration.context,
|
||||
mCopyFromAssetPath,
|
||||
mCopyFromFile,
|
||||
mCopyFromInputStream,
|
||||
configuration.callback.version,
|
||||
mDelegate.create(configuration)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.sqlite.db.SupportSQLiteStatement;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Represents a prepared SQLite state that can be re-used multiple times.
|
||||
* <p>
|
||||
* This class is used by generated code. After it is used, {@code release} must be called so that
|
||||
* it can be used by other threads.
|
||||
* <p>
|
||||
* To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
|
||||
* statement until it is released.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public abstract class SharedSQLiteStatement {
|
||||
private final AtomicBoolean mLock = new AtomicBoolean(false);
|
||||
|
||||
private final RoomDatabase mDatabase;
|
||||
private volatile SupportSQLiteStatement mStmt;
|
||||
|
||||
/**
|
||||
* Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
|
||||
* it automatically creates a new one.
|
||||
*
|
||||
* @param database The database to create the statement in.
|
||||
*/
|
||||
public SharedSQLiteStatement(RoomDatabase database) {
|
||||
mDatabase = database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the query.
|
||||
*
|
||||
* @return The SQL query to prepare.
|
||||
*/
|
||||
protected abstract String createQuery();
|
||||
|
||||
protected void assertNotMainThread() {
|
||||
mDatabase.assertNotMainThread();
|
||||
}
|
||||
|
||||
private SupportSQLiteStatement createNewStatement() {
|
||||
String query = createQuery();
|
||||
return mDatabase.compileStatement(query);
|
||||
}
|
||||
|
||||
private SupportSQLiteStatement getStmt(boolean canUseCached) {
|
||||
final SupportSQLiteStatement stmt;
|
||||
if (canUseCached) {
|
||||
if (mStmt == null) {
|
||||
mStmt = createNewStatement();
|
||||
}
|
||||
stmt = mStmt;
|
||||
} else {
|
||||
// it is in use, create a one off statement
|
||||
stmt = createNewStatement();
|
||||
}
|
||||
return stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done.
|
||||
*/
|
||||
public SupportSQLiteStatement acquire() {
|
||||
assertNotMainThread();
|
||||
return getStmt(mLock.compareAndSet(false, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Must call this when statement will not be used anymore.
|
||||
*
|
||||
* @param statement The statement that was returned from acquire.
|
||||
*/
|
||||
public void release(SupportSQLiteStatement statement) {
|
||||
if (statement == mStmt) {
|
||||
mLock.set(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.sqlite.db.SupportSQLiteStatement
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Represents a prepared SQLite state that can be re-used multiple times.
|
||||
*
|
||||
* This class is used by generated code. After it is used, `release` must be called so that
|
||||
* it can be used by other threads.
|
||||
*
|
||||
* To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
|
||||
* statement until it is released.
|
||||
*
|
||||
* @constructor Creates an SQLite prepared statement that can be re-used across threads. If it is
|
||||
* in use, it automatically creates a new one.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
abstract class SharedSQLiteStatement(private val database: RoomDatabase) {
|
||||
private val lock = AtomicBoolean(false)
|
||||
|
||||
private val stmt: SupportSQLiteStatement by lazy {
|
||||
createNewStatement()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the query.
|
||||
*
|
||||
* @return The SQL query to prepare.
|
||||
*/
|
||||
protected abstract fun createQuery(): String
|
||||
|
||||
protected open fun assertNotMainThread() {
|
||||
database.assertNotMainThread()
|
||||
}
|
||||
|
||||
private fun createNewStatement(): SupportSQLiteStatement {
|
||||
val query = createQuery()
|
||||
return database.compileStatement(query)
|
||||
}
|
||||
|
||||
private fun getStmt(canUseCached: Boolean): SupportSQLiteStatement {
|
||||
val stmt = if (canUseCached) {
|
||||
stmt
|
||||
} else {
|
||||
// it is in use, create a one off statement
|
||||
createNewStatement()
|
||||
}
|
||||
return stmt
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this to get the statement. Must call [.release] once done.
|
||||
*/
|
||||
open fun acquire(): SupportSQLiteStatement {
|
||||
assertNotMainThread()
|
||||
return getStmt(lock.compareAndSet(false, true))
|
||||
}
|
||||
|
||||
/**
|
||||
* Must call this when statement will not be used anymore.
|
||||
*
|
||||
* @param statement The statement that was returned from acquire.
|
||||
*/
|
||||
open fun release(statement: SupportSQLiteStatement) {
|
||||
if (statement === stmt) {
|
||||
lock.set(false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
/**
|
||||
* Java 8 Sneaky Throw technique.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
||||
public class SneakyThrow {
|
||||
|
||||
/**
|
||||
* Re-throws a checked exception as if it was a runtime exception without wrapping it.
|
||||
*
|
||||
* @param e the exception to re-throw.
|
||||
*/
|
||||
public static void reThrow(@NonNull Exception e) {
|
||||
sneakyThrow(e);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <E extends Throwable> void sneakyThrow(@NonNull Throwable e) throws E {
|
||||
throw (E) e;
|
||||
}
|
||||
|
||||
private SneakyThrow() {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*
|
||||
* String utilities for Room
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public class StringUtil {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||
/**
|
||||
* Returns a new StringBuilder to be used while producing SQL queries.
|
||||
*
|
||||
* @return A new or recycled StringBuilder
|
||||
*/
|
||||
public static StringBuilder newStringBuilder() {
|
||||
// TODO pool:
|
||||
return new StringBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bind variable placeholders (?) to the given string. Each placeholder is separated
|
||||
* by a comma.
|
||||
*
|
||||
* @param builder The StringBuilder for the query
|
||||
* @param count Number of placeholders
|
||||
*/
|
||||
public static void appendPlaceholders(StringBuilder builder, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
builder.append("?");
|
||||
if (i < count - 1) {
|
||||
builder.append(",");
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Splits a comma separated list of integers to integer list.
|
||||
* <p>
|
||||
* If an input is malformed, it is omitted from the result.
|
||||
*
|
||||
* @param input Comma separated list of integers.
|
||||
* @return A List containing the integers or null if the input is null.
|
||||
*/
|
||||
@Nullable
|
||||
public static List<Integer> splitToIntList(@Nullable String input) {
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
List<Integer> result = new ArrayList<>();
|
||||
StringTokenizer tokenizer = new StringTokenizer(input, ",");
|
||||
while (tokenizer.hasMoreElements()) {
|
||||
final String item = tokenizer.nextToken();
|
||||
try {
|
||||
result.add(Integer.parseInt(item));
|
||||
} catch (NumberFormatException ex) {
|
||||
Log.e("ROOM", "Malformed integer list", ex);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the given list of integers into a comma separated list.
|
||||
*
|
||||
* @param input The list of integers.
|
||||
* @return Comma separated string composed of integers in the list. If the list is null, return
|
||||
* value is null.
|
||||
*/
|
||||
@Nullable
|
||||
public static String joinIntoString(@Nullable List<Integer> input) {
|
||||
if (input == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int size = input.size();
|
||||
if (size == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < size; i++) {
|
||||
sb.append(Integer.toString(input.get(i)));
|
||||
if (i < size - 1) {
|
||||
sb.append(",");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private StringUtil() {
|
||||
}
|
||||
}
|
|
@ -1,743 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* A data class that holds the information about a table.
|
||||
* <p>
|
||||
* It directly maps to the result of {@code PRAGMA table_info(<table_name>)}. Check the
|
||||
* <a href="http://www.sqlite.org/pragma.html#pragma_table_info">PRAGMA table_info</a>
|
||||
* documentation for more details.
|
||||
* <p>
|
||||
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
|
||||
"SimplifiableIfStatement"})
|
||||
// if you change this class, you must change TableInfoValidationWriter.kt
|
||||
public final class TableInfo {
|
||||
|
||||
/**
|
||||
* Identifies from where the info object was created.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(value = {CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE})
|
||||
@interface CreatedFrom {
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier for when the info is created from an unknown source.
|
||||
*/
|
||||
public static final int CREATED_FROM_UNKNOWN = 0;
|
||||
|
||||
/**
|
||||
* Identifier for when the info is created from an entity definition, such as generated code
|
||||
* by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
|
||||
*/
|
||||
public static final int CREATED_FROM_ENTITY = 1;
|
||||
|
||||
/**
|
||||
* Identifier for when the info is created from the database itself, reading information from a
|
||||
* PRAGMA, such as table_info.
|
||||
*/
|
||||
public static final int CREATED_FROM_DATABASE = 2;
|
||||
|
||||
/**
|
||||
* The table name.
|
||||
*/
|
||||
public final String name;
|
||||
/**
|
||||
* Unmodifiable map of columns keyed by column name.
|
||||
*/
|
||||
public final Map<String, Column> columns;
|
||||
|
||||
public final Set<ForeignKey> foreignKeys;
|
||||
|
||||
/**
|
||||
* Sometimes, Index information is not available (older versions). If so, we skip their
|
||||
* verification.
|
||||
*/
|
||||
@Nullable
|
||||
public final Set<Index> indices;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys,
|
||||
Set<Index> indices) {
|
||||
this.name = name;
|
||||
this.columns = Collections.unmodifiableMap(columns);
|
||||
this.foreignKeys = Collections.unmodifiableSet(foreignKeys);
|
||||
this.indices = indices == null ? null : Collections.unmodifiableSet(indices);
|
||||
}
|
||||
|
||||
/**
|
||||
* For backward compatibility with dbs created with older versions.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public TableInfo(String name, Map<String, Column> columns, Set<ForeignKey> foreignKeys) {
|
||||
this(name, columns, foreignKeys, Collections.<Index>emptySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof TableInfo)) return false;
|
||||
|
||||
TableInfo tableInfo = (TableInfo) o;
|
||||
|
||||
if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
|
||||
if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) {
|
||||
return false;
|
||||
}
|
||||
if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys)
|
||||
: tableInfo.foreignKeys != null) {
|
||||
return false;
|
||||
}
|
||||
if (indices == null || tableInfo.indices == null) {
|
||||
// if one us is missing index information, seems like we couldn't acquire the
|
||||
// information so we better skip.
|
||||
return true;
|
||||
}
|
||||
return indices.equals(tableInfo.indices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = name != null ? name.hashCode() : 0;
|
||||
result = 31 * result + (columns != null ? columns.hashCode() : 0);
|
||||
result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0);
|
||||
// skip index, it is not reliable for comparison.
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TableInfo{"
|
||||
+ "name='" + name + '\''
|
||||
+ ", columns=" + columns
|
||||
+ ", foreignKeys=" + foreignKeys
|
||||
+ ", indices=" + indices
|
||||
+ '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the table information from the given database.
|
||||
*
|
||||
* @param database The database to read the information from.
|
||||
* @param tableName The table name.
|
||||
* @return A TableInfo containing the schema information for the provided table name.
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
|
||||
Map<String, Column> columns = readColumns(database, tableName);
|
||||
Set<ForeignKey> foreignKeys = readForeignKeys(database, tableName);
|
||||
Set<Index> indices = readIndices(database, tableName);
|
||||
return new TableInfo(tableName, columns, foreignKeys, indices);
|
||||
}
|
||||
|
||||
private static Set<ForeignKey> readForeignKeys(SupportSQLiteDatabase database,
|
||||
String tableName) {
|
||||
Set<ForeignKey> foreignKeys = new HashSet<>();
|
||||
// this seems to return everything in order but it is not documented so better be safe
|
||||
Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)");
|
||||
try {
|
||||
final int idColumnIndex = cursor.getColumnIndex("id");
|
||||
final int seqColumnIndex = cursor.getColumnIndex("seq");
|
||||
final int tableColumnIndex = cursor.getColumnIndex("table");
|
||||
final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete");
|
||||
final int onUpdateColumnIndex = cursor.getColumnIndex("on_update");
|
||||
|
||||
final List<ForeignKeyWithSequence> ordered = readForeignKeyFieldMappings(cursor);
|
||||
final int count = cursor.getCount();
|
||||
for (int position = 0; position < count; position++) {
|
||||
cursor.moveToPosition(position);
|
||||
final int seq = cursor.getInt(seqColumnIndex);
|
||||
if (seq != 0) {
|
||||
continue;
|
||||
}
|
||||
final int id = cursor.getInt(idColumnIndex);
|
||||
List<String> myColumns = new ArrayList<>();
|
||||
List<String> refColumns = new ArrayList<>();
|
||||
for (ForeignKeyWithSequence key : ordered) {
|
||||
if (key.mId == id) {
|
||||
myColumns.add(key.mFrom);
|
||||
refColumns.add(key.mTo);
|
||||
}
|
||||
}
|
||||
foreignKeys.add(new ForeignKey(
|
||||
cursor.getString(tableColumnIndex),
|
||||
cursor.getString(onDeleteColumnIndex),
|
||||
cursor.getString(onUpdateColumnIndex),
|
||||
myColumns,
|
||||
refColumns
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
return foreignKeys;
|
||||
}
|
||||
|
||||
private static List<ForeignKeyWithSequence> readForeignKeyFieldMappings(Cursor cursor) {
|
||||
final int idColumnIndex = cursor.getColumnIndex("id");
|
||||
final int seqColumnIndex = cursor.getColumnIndex("seq");
|
||||
final int fromColumnIndex = cursor.getColumnIndex("from");
|
||||
final int toColumnIndex = cursor.getColumnIndex("to");
|
||||
final int count = cursor.getCount();
|
||||
List<ForeignKeyWithSequence> result = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
cursor.moveToPosition(i);
|
||||
result.add(new ForeignKeyWithSequence(
|
||||
cursor.getInt(idColumnIndex),
|
||||
cursor.getInt(seqColumnIndex),
|
||||
cursor.getString(fromColumnIndex),
|
||||
cursor.getString(toColumnIndex)
|
||||
));
|
||||
}
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, Column> readColumns(SupportSQLiteDatabase database,
|
||||
String tableName) {
|
||||
|
||||
Cursor cursor = database
|
||||
.query("PRAGMA table_info(`" + tableName + "`)");
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
Map<String, Column> columns = new HashMap<>();
|
||||
try {
|
||||
if (cursor.getColumnCount() > 0) {
|
||||
int nameIndex = cursor.getColumnIndex("name");
|
||||
int typeIndex = cursor.getColumnIndex("type");
|
||||
int notNullIndex = cursor.getColumnIndex("notnull");
|
||||
int pkIndex = cursor.getColumnIndex("pk");
|
||||
int defaultValueIndex = cursor.getColumnIndex("dflt_value");
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
final String name = cursor.getString(nameIndex);
|
||||
final String type = cursor.getString(typeIndex);
|
||||
final boolean notNull = 0 != cursor.getInt(notNullIndex);
|
||||
final int primaryKeyPosition = cursor.getInt(pkIndex);
|
||||
final String defaultValue = cursor.getString(defaultValueIndex);
|
||||
columns.put(name,
|
||||
new Column(name, type, notNull, primaryKeyPosition, defaultValue,
|
||||
CREATED_FROM_DATABASE));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null if we cannot read the indices due to older sqlite implementations.
|
||||
*/
|
||||
@Nullable
|
||||
private static Set<Index> readIndices(SupportSQLiteDatabase database, String tableName) {
|
||||
Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)");
|
||||
try {
|
||||
final int nameColumnIndex = cursor.getColumnIndex("name");
|
||||
final int originColumnIndex = cursor.getColumnIndex("origin");
|
||||
final int uniqueIndex = cursor.getColumnIndex("unique");
|
||||
if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
|
||||
// we cannot read them so better not validate any index.
|
||||
return null;
|
||||
}
|
||||
HashSet<Index> indices = new HashSet<>();
|
||||
while (cursor.moveToNext()) {
|
||||
String origin = cursor.getString(originColumnIndex);
|
||||
if (!"c".equals(origin)) {
|
||||
// Ignore auto-created indices
|
||||
continue;
|
||||
}
|
||||
String name = cursor.getString(nameColumnIndex);
|
||||
boolean unique = cursor.getInt(uniqueIndex) == 1;
|
||||
Index index = readIndex(database, name, unique);
|
||||
if (index == null) {
|
||||
// we cannot read it properly so better not read it
|
||||
return null;
|
||||
}
|
||||
indices.add(index);
|
||||
}
|
||||
return indices;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null if we cannot read the index due to older sqlite implementations.
|
||||
*/
|
||||
@Nullable
|
||||
private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) {
|
||||
Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)");
|
||||
try {
|
||||
final int seqnoColumnIndex = cursor.getColumnIndex("seqno");
|
||||
final int cidColumnIndex = cursor.getColumnIndex("cid");
|
||||
final int nameColumnIndex = cursor.getColumnIndex("name");
|
||||
final int descColumnIndex = cursor.getColumnIndex("desc");
|
||||
if (seqnoColumnIndex == -1 || cidColumnIndex == -1
|
||||
|| nameColumnIndex == -1 || descColumnIndex == -1) {
|
||||
// we cannot read them so better not validate any index.
|
||||
return null;
|
||||
}
|
||||
final TreeMap<Integer, String> columnsMap = new TreeMap<>();
|
||||
final TreeMap<Integer, String> ordersMap = new TreeMap<>();
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
int cid = cursor.getInt(cidColumnIndex);
|
||||
if (cid < 0) {
|
||||
// Ignore SQLite row ID
|
||||
continue;
|
||||
}
|
||||
int seq = cursor.getInt(seqnoColumnIndex);
|
||||
String columnName = cursor.getString(nameColumnIndex);
|
||||
String order = cursor.getInt(descColumnIndex) > 0 ? "DESC" : "ASC";
|
||||
|
||||
columnsMap.put(seq, columnName);
|
||||
ordersMap.put(seq, order);
|
||||
}
|
||||
final List<String> columns = new ArrayList<>(columnsMap.size());
|
||||
columns.addAll(columnsMap.values());
|
||||
final List<String> orders = new ArrayList<>(ordersMap.size());
|
||||
orders.addAll(ordersMap.values());
|
||||
return new Index(name, unique, columns, orders);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the information about a database column.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static final class Column {
|
||||
/**
|
||||
* The column name.
|
||||
*/
|
||||
public final String name;
|
||||
/**
|
||||
* The column type affinity.
|
||||
*/
|
||||
public final String type;
|
||||
/**
|
||||
* The column type after it is normalized to one of the basic types according to
|
||||
* https://www.sqlite.org/datatype3.html Section 3.1.
|
||||
* <p>
|
||||
* This is the value Room uses for equality check.
|
||||
*/
|
||||
@ColumnInfo.SQLiteTypeAffinity
|
||||
public final int affinity;
|
||||
/**
|
||||
* Whether or not the column can be NULL.
|
||||
*/
|
||||
public final boolean notNull;
|
||||
/**
|
||||
* The position of the column in the list of primary keys, 0 if the column is not part
|
||||
* of the primary key.
|
||||
* <p>
|
||||
* This information is only available in API 20+.
|
||||
* <a href="https://www.sqlite.org/releaselog/3_7_16_2.html">(SQLite version 3.7.16.2)</a>
|
||||
* On older platforms, it will be 1 if the column is part of the primary key and 0
|
||||
* otherwise.
|
||||
* <p>
|
||||
* The {@link #equals(Object)} implementation handles this inconsistency based on
|
||||
* API levels os if you are using a custom SQLite deployment, it may return false
|
||||
* positives.
|
||||
*/
|
||||
public final int primaryKeyPosition;
|
||||
/**
|
||||
* The default value of this column.
|
||||
*/
|
||||
public final String defaultValue;
|
||||
|
||||
@CreatedFrom
|
||||
private final int mCreatedFrom;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Column#Column(String, String, boolean, int, String, int)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
|
||||
this(name, type, notNull, primaryKeyPosition, null, CREATED_FROM_UNKNOWN);
|
||||
}
|
||||
|
||||
// if you change this constructor, you must change TableInfoWriter.kt
|
||||
public Column(String name, String type, boolean notNull, int primaryKeyPosition,
|
||||
String defaultValue, @CreatedFrom int createdFrom) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.notNull = notNull;
|
||||
this.primaryKeyPosition = primaryKeyPosition;
|
||||
this.affinity = findAffinity(type);
|
||||
this.defaultValue = defaultValue;
|
||||
this.mCreatedFrom = createdFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements https://www.sqlite.org/datatype3.html section 3.1
|
||||
*
|
||||
* @param type The type that was given to the sqlite
|
||||
* @return The normalized type which is one of the 5 known affinities
|
||||
*/
|
||||
@ColumnInfo.SQLiteTypeAffinity
|
||||
private static int findAffinity(@Nullable String type) {
|
||||
if (type == null) {
|
||||
return ColumnInfo.BLOB;
|
||||
}
|
||||
String uppercaseType = type.toUpperCase(Locale.US);
|
||||
if (uppercaseType.contains("INT")) {
|
||||
return ColumnInfo.INTEGER;
|
||||
}
|
||||
if (uppercaseType.contains("CHAR")
|
||||
|| uppercaseType.contains("CLOB")
|
||||
|| uppercaseType.contains("TEXT")) {
|
||||
return ColumnInfo.TEXT;
|
||||
}
|
||||
if (uppercaseType.contains("BLOB")) {
|
||||
return ColumnInfo.BLOB;
|
||||
}
|
||||
if (uppercaseType.contains("REAL")
|
||||
|| uppercaseType.contains("FLOA")
|
||||
|| uppercaseType.contains("DOUB")) {
|
||||
return ColumnInfo.REAL;
|
||||
}
|
||||
// sqlite returns NUMERIC here but it is like a catch all. We already
|
||||
// have UNDEFINED so it is better to use UNDEFINED for consistency.
|
||||
return ColumnInfo.UNDEFINED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Column)) return false;
|
||||
|
||||
Column column = (Column) o;
|
||||
if (Build.VERSION.SDK_INT >= 20) {
|
||||
if (primaryKeyPosition != column.primaryKeyPosition) return false;
|
||||
} else {
|
||||
if (isPrimaryKey() != column.isPrimaryKey()) return false;
|
||||
}
|
||||
|
||||
if (!name.equals(column.name)) return false;
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (notNull != column.notNull) return false;
|
||||
|
||||
// Only validate default value if it was defined in an entity, i.e. if the info
|
||||
// from the compiler itself has it. b/136019383
|
||||
if (mCreatedFrom == CREATED_FROM_ENTITY
|
||||
&& column.mCreatedFrom == CREATED_FROM_DATABASE
|
||||
&& (defaultValue != null && !defaultValueEquals(defaultValue,
|
||||
column.defaultValue))) {
|
||||
return false;
|
||||
} else if (mCreatedFrom == CREATED_FROM_DATABASE
|
||||
&& column.mCreatedFrom == CREATED_FROM_ENTITY
|
||||
&& (column.defaultValue != null && !defaultValueEquals(
|
||||
column.defaultValue, defaultValue))) {
|
||||
return false;
|
||||
} else if (mCreatedFrom != CREATED_FROM_UNKNOWN
|
||||
&& mCreatedFrom == column.mCreatedFrom
|
||||
&& (defaultValue != null ? !defaultValueEquals(defaultValue,
|
||||
column.defaultValue)
|
||||
: column.defaultValue != null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return affinity == column.affinity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the default values provided match. Handles the special case in which the
|
||||
* default value is surrounded by parenthesis (e.g. encountered in b/182284899).
|
||||
*
|
||||
* Surrounding parenthesis are removed by SQLite when reading from the database, hence
|
||||
* this function will check if they are present in the actual value, if so, it will
|
||||
* compare the two values by ignoring the surrounding parenthesis.
|
||||
*
|
||||
*/
|
||||
public static boolean defaultValueEquals(@NonNull String actual, @Nullable String other) {
|
||||
if (other == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (actual.equals(other)) {
|
||||
return true;
|
||||
} else if (containsSurroundingParenthesis(actual)) {
|
||||
return actual.substring(1, actual.length() - 1).trim().equals(other);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for potential surrounding parenthesis, if found, removes them and checks if
|
||||
* remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant,
|
||||
* and returns true.
|
||||
*/
|
||||
private static boolean containsSurroundingParenthesis(@NonNull String actual) {
|
||||
if (actual.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
int surroundingParenthesis = 0;
|
||||
for (int i = 0; i < actual.length(); i++) {
|
||||
char c = actual.charAt(i);
|
||||
if (i == 0 && c != '(') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (c == '(') {
|
||||
surroundingParenthesis++;
|
||||
} else if (c == ')') {
|
||||
surroundingParenthesis--;
|
||||
if (surroundingParenthesis == 0 && i != actual.length() - 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return surroundingParenthesis == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this column is part of the primary key or not.
|
||||
*
|
||||
* @return True if this column is part of the primary key, false otherwise.
|
||||
*/
|
||||
public boolean isPrimaryKey() {
|
||||
return primaryKeyPosition > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = name.hashCode();
|
||||
result = 31 * result + affinity;
|
||||
result = 31 * result + (notNull ? 1231 : 1237);
|
||||
result = 31 * result + primaryKeyPosition;
|
||||
// Default value is not part of the hashcode since we conditionally check it for
|
||||
// equality which would break the equals + hashcode contract.
|
||||
// result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Column{"
|
||||
+ "name='" + name + '\''
|
||||
+ ", type='" + type + '\''
|
||||
+ ", affinity='" + affinity + '\''
|
||||
+ ", notNull=" + notNull
|
||||
+ ", primaryKeyPosition=" + primaryKeyPosition
|
||||
+ ", defaultValue='" + defaultValue + '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the information about an SQLite foreign key
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public static final class ForeignKey {
|
||||
@NonNull
|
||||
public final String referenceTable;
|
||||
@NonNull
|
||||
public final String onDelete;
|
||||
@NonNull
|
||||
public final String onUpdate;
|
||||
@NonNull
|
||||
public final List<String> columnNames;
|
||||
@NonNull
|
||||
public final List<String> referenceColumnNames;
|
||||
|
||||
public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete,
|
||||
@NonNull String onUpdate,
|
||||
@NonNull List<String> columnNames, @NonNull List<String> referenceColumnNames) {
|
||||
this.referenceTable = referenceTable;
|
||||
this.onDelete = onDelete;
|
||||
this.onUpdate = onUpdate;
|
||||
this.columnNames = Collections.unmodifiableList(columnNames);
|
||||
this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof ForeignKey)) return false;
|
||||
|
||||
ForeignKey that = (ForeignKey) o;
|
||||
|
||||
if (!referenceTable.equals(that.referenceTable)) return false;
|
||||
if (!onDelete.equals(that.onDelete)) return false;
|
||||
if (!onUpdate.equals(that.onUpdate)) return false;
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (!columnNames.equals(that.columnNames)) return false;
|
||||
return referenceColumnNames.equals(that.referenceColumnNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = referenceTable.hashCode();
|
||||
result = 31 * result + onDelete.hashCode();
|
||||
result = 31 * result + onUpdate.hashCode();
|
||||
result = 31 * result + columnNames.hashCode();
|
||||
result = 31 * result + referenceColumnNames.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ForeignKey{"
|
||||
+ "referenceTable='" + referenceTable + '\''
|
||||
+ ", onDelete='" + onDelete + '\''
|
||||
+ ", onUpdate='" + onUpdate + '\''
|
||||
+ ", columnNames=" + columnNames
|
||||
+ ", referenceColumnNames=" + referenceColumnNames
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary data holder for a foreign key row in the pragma result. We need this to ensure
|
||||
* sorting in the generated foreign key object.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
static class ForeignKeyWithSequence implements Comparable<ForeignKeyWithSequence> {
|
||||
final int mId;
|
||||
final int mSequence;
|
||||
final String mFrom;
|
||||
final String mTo;
|
||||
|
||||
ForeignKeyWithSequence(int id, int sequence, String from, String to) {
|
||||
mId = id;
|
||||
mSequence = sequence;
|
||||
mFrom = from;
|
||||
mTo = to;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull ForeignKeyWithSequence o) {
|
||||
final int idCmp = mId - o.mId;
|
||||
if (idCmp == 0) {
|
||||
return mSequence - o.mSequence;
|
||||
} else {
|
||||
return idCmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the information about an SQLite index
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public static final class Index {
|
||||
// should match the value in Index.kt
|
||||
public static final String DEFAULT_PREFIX = "index_";
|
||||
public final String name;
|
||||
public final boolean unique;
|
||||
public final List<String> columns;
|
||||
public final List<String> orders;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #Index(String, boolean, List, List)}
|
||||
*/
|
||||
public Index(String name, boolean unique, List<String> columns) {
|
||||
this(name, unique, columns, null);
|
||||
}
|
||||
|
||||
public Index(String name, boolean unique, List<String> columns, List<String> orders) {
|
||||
this.name = name;
|
||||
this.unique = unique;
|
||||
this.columns = columns;
|
||||
this.orders = orders == null || orders.size() == 0
|
||||
? Collections.nCopies(columns.size(), androidx.room.Index.Order.ASC.name())
|
||||
: orders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Index)) return false;
|
||||
|
||||
Index index = (Index) o;
|
||||
if (unique != index.unique) {
|
||||
return false;
|
||||
}
|
||||
if (!columns.equals(index.columns)) {
|
||||
return false;
|
||||
}
|
||||
if (!orders.equals(index.orders)) {
|
||||
return false;
|
||||
}
|
||||
if (name.startsWith(Index.DEFAULT_PREFIX)) {
|
||||
return index.name.startsWith(Index.DEFAULT_PREFIX);
|
||||
} else {
|
||||
return name.equals(index.name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result;
|
||||
if (name.startsWith(DEFAULT_PREFIX)) {
|
||||
result = DEFAULT_PREFIX.hashCode();
|
||||
} else {
|
||||
result = name.hashCode();
|
||||
}
|
||||
result = 31 * result + (unique ? 1 : 0);
|
||||
result = 31 * result + columns.hashCode();
|
||||
result = 31 * result + orders.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Index{"
|
||||
+ "name='" + name + '\''
|
||||
+ ", unique=" + unique
|
||||
+ ", columns=" + columns
|
||||
+ ", orders=" + orders
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,52 +13,42 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room
|
||||
|
||||
package androidx.room;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.ArrayDeque
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* Executor wrapper for performing database transactions serially.
|
||||
* <p>
|
||||
*
|
||||
* Since database transactions are exclusive, this executor ensures that transactions are performed
|
||||
* in-order and one at a time, preventing threads from blocking each other when multiple concurrent
|
||||
* transactions are attempted.
|
||||
*/
|
||||
class TransactionExecutor implements Executor {
|
||||
|
||||
private final Executor mExecutor;
|
||||
private final ArrayDeque<Runnable> mTasks = new ArrayDeque<>();
|
||||
private Runnable mActive;
|
||||
|
||||
TransactionExecutor(@NonNull Executor executor) {
|
||||
mExecutor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void execute(final Runnable command) {
|
||||
mTasks.offer(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
internal class TransactionExecutor(private val executor: Executor) : Executor {
|
||||
private val tasks = ArrayDeque<Runnable>()
|
||||
private var active: Runnable? = null
|
||||
private val syncLock = Any()
|
||||
override fun execute(command: Runnable) {
|
||||
synchronized(syncLock) {
|
||||
tasks.offer(Runnable {
|
||||
try {
|
||||
command.run();
|
||||
command.run()
|
||||
} finally {
|
||||
scheduleNext();
|
||||
scheduleNext()
|
||||
}
|
||||
})
|
||||
if (active == null) {
|
||||
scheduleNext()
|
||||
}
|
||||
});
|
||||
if (mActive == null) {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
synchronized void scheduleNext() {
|
||||
if ((mActive = mTasks.poll()) != null) {
|
||||
mExecutor.execute(mActive);
|
||||
fun scheduleNext() {
|
||||
synchronized(syncLock) {
|
||||
if (tasks.poll().also { active = it } != null) {
|
||||
executor.execute(active)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* UUID / byte[] two-way conversion utility for Room
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public final class UUIDUtil {
|
||||
|
||||
// private constructor to prevent instantiation
|
||||
private UUIDUtil() {}
|
||||
|
||||
/**
|
||||
* Converts a 16-bytes array BLOB into a UUID pojo
|
||||
*
|
||||
* @param bytes byte array stored in database as BLOB
|
||||
* @return a UUID object created based on the provided byte array
|
||||
*/
|
||||
@NonNull
|
||||
public static UUID convertByteToUUID(@NonNull byte[] bytes) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
long firstLong = buffer.getLong();
|
||||
long secondLong = buffer.getLong();
|
||||
return new UUID(firstLong, secondLong);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UUID pojo into a 16-bytes array to store into database as BLOB
|
||||
*
|
||||
* @param uuid the UUID pojo
|
||||
* @return a byte array to store into database
|
||||
*/
|
||||
@NonNull
|
||||
public static byte[] convertUUIDToByte(@NonNull UUID uuid) {
|
||||
byte[] bytes = new byte[16];
|
||||
ByteBuffer buffer = ByteBuffer.wrap(bytes);
|
||||
buffer.putLong(uuid.getMostSignificantBits());
|
||||
buffer.putLong(uuid.getLeastSignificantBits());
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.room.util;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
/**
|
||||
* A data class that holds the information about a view.
|
||||
* <p>
|
||||
* This derives information from sqlite_master.
|
||||
* <p>
|
||||
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
public final class ViewInfo {
|
||||
|
||||
/**
|
||||
* The view name
|
||||
*/
|
||||
public final String name;
|
||||
|
||||
/**
|
||||
* The SQL of CREATE VIEW.
|
||||
*/
|
||||
public final String sql;
|
||||
|
||||
public ViewInfo(String name, String sql) {
|
||||
this.name = name;
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof ViewInfo)) return false;
|
||||
ViewInfo viewInfo = (ViewInfo) o;
|
||||
return (name != null ? name.equals(viewInfo.name) : viewInfo.name == null)
|
||||
&& (sql != null ? sql.equals(viewInfo.sql) : viewInfo.sql == null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = name != null ? name.hashCode() : 0;
|
||||
result = 31 * result + (sql != null ? sql.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ViewInfo{"
|
||||
+ "name='" + name + '\''
|
||||
+ ", sql='" + sql + '\''
|
||||
+ '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the view information from the given database.
|
||||
*
|
||||
* @param database The database to read the information from.
|
||||
* @param viewName The view name.
|
||||
* @return A ViewInfo containing the schema information for the provided view name.
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
public static ViewInfo read(SupportSQLiteDatabase database, String viewName) {
|
||||
Cursor cursor = database.query("SELECT name, sql FROM sqlite_master "
|
||||
+ "WHERE type = 'view' AND name = '" + viewName + "'");
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
return new ViewInfo(cursor.getString(0), cursor.getString(1));
|
||||
} else {
|
||||
return new ViewInfo(viewName, null);
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,26 +13,23 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room.migration
|
||||
|
||||
package androidx.room.migration;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.AutoMigration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
/**
|
||||
* Interface for defining an automatic migration specification for Room databases.
|
||||
* <p>
|
||||
*
|
||||
* The methods defined in this interface will be called on a background thread from the executor
|
||||
* set in Room's builder. It is important to note that the methods are all in a transaction when
|
||||
* it is called.
|
||||
*
|
||||
* @see AutoMigration
|
||||
* For details, see [androidx.room.AutoMigration]
|
||||
*/
|
||||
public interface AutoMigrationSpec {
|
||||
|
||||
interface AutoMigrationSpec {
|
||||
/**
|
||||
* Invoked after the migration is completed.
|
||||
* @param db The SQLite database.
|
||||
*/
|
||||
default void onPostMigrate(@NonNull SupportSQLiteDatabase db) {}
|
||||
fun onPostMigrate(db: SupportSQLiteDatabase) {}
|
||||
}
|
|
@ -13,51 +13,42 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room.migration
|
||||
|
||||
package androidx.room.migration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
/**
|
||||
* Base class for a database migration.
|
||||
* <p>
|
||||
* Each migration can move between 2 versions that are defined by {@link #startVersion} and
|
||||
* {@link #endVersion}.
|
||||
* <p>
|
||||
*
|
||||
* Creates a new migration between [startVersion] and [endVersion].
|
||||
*
|
||||
* Each migration can move between 2 versions that are defined by [startVersion] and
|
||||
* [endVersion].
|
||||
*
|
||||
* A migration can handle more than 1 version (e.g. if you have a faster path to choose when
|
||||
* going version 3 to 5 without going to version 4). If Room opens a database at version
|
||||
* 3 and latest version is >= 5, Room will use the migration object that can migrate from
|
||||
* 3 and latest version is 5, Room will use the migration object that can migrate from
|
||||
* 3 to 5 instead of 3 to 4 and 4 to 5.
|
||||
* <p>
|
||||
*
|
||||
* If there are not enough migrations provided to move from the current version to the latest
|
||||
* version, Room will clear the database and recreate so even if you have no changes between 2
|
||||
* versions, you should still provide a Migration object to the builder.
|
||||
*/
|
||||
public abstract class Migration {
|
||||
public final int startVersion;
|
||||
public final int endVersion;
|
||||
|
||||
/**
|
||||
* Creates a new migration between {@code startVersion} and {@code endVersion}.
|
||||
*
|
||||
* @param startVersion The start version of the database.
|
||||
* @param endVersion The end version of the database after this migration is applied.
|
||||
*/
|
||||
public Migration(int startVersion, int endVersion) {
|
||||
this.startVersion = startVersion;
|
||||
this.endVersion = endVersion;
|
||||
}
|
||||
|
||||
abstract class Migration(
|
||||
@JvmField
|
||||
val startVersion: Int,
|
||||
@JvmField
|
||||
val endVersion: Int
|
||||
) {
|
||||
/**
|
||||
* Should run the necessary migrations.
|
||||
* <p>
|
||||
* This class cannot access any generated Dao in this method.
|
||||
* <p>
|
||||
* This method is already called inside a transaction and that transaction might actually be a
|
||||
* composite transaction of all necessary {@code Migration}s.
|
||||
*
|
||||
* @param database The database instance
|
||||
* The Migration class cannot access any generated Dao in this method.
|
||||
*
|
||||
* This method is already called inside a transaction and that transaction might actually be a
|
||||
* composite transaction of all necessary `Migration`s.
|
||||
*
|
||||
* @param db The database instance
|
||||
*/
|
||||
public abstract void migrate(@NonNull SupportSQLiteDatabase database);
|
||||
abstract fun migrate(db: SupportSQLiteDatabase)
|
||||
}
|
|
@ -46,7 +46,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
*
|
||||
* @param <T> Data type returned by the data source.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
|
@ -117,8 +116,8 @@ public abstract class LimitOffsetDataSource<T> extends androidx.paging.Positiona
|
|||
/**
|
||||
* Count number of rows query can return
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public int countItems() {
|
||||
registerObserverIfNecessary();
|
||||
|
@ -154,7 +153,7 @@ public abstract class LimitOffsetDataSource<T> extends androidx.paging.Positiona
|
|||
@NonNull LoadInitialCallback<T> callback) {
|
||||
registerObserverIfNecessary();
|
||||
List<T> list = Collections.emptyList();
|
||||
int totalCount = 0;
|
||||
int totalCount;
|
||||
int firstLoadPosition = 0;
|
||||
RoomSQLiteQuery sqLiteQuery = null;
|
||||
Cursor cursor = null;
|
||||
|
@ -172,8 +171,6 @@ public abstract class LimitOffsetDataSource<T> extends androidx.paging.Positiona
|
|||
mDb.setTransactionSuccessful();
|
||||
list = rows;
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
eu.faircode.email.Log.w(ex);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
|
@ -196,8 +193,8 @@ public abstract class LimitOffsetDataSource<T> extends androidx.paging.Positiona
|
|||
/**
|
||||
* Return the rows from startPos to startPos + loadCount
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
@SuppressWarnings("deprecation")
|
||||
@NonNull
|
||||
public List<T> loadRange(int startPosition, int loadCount) {
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:JvmName("CursorUtil")
|
||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
|
||||
package androidx.room.util
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.CursorWrapper
|
||||
import android.database.MatrixCursor
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.VisibleForTesting
|
||||
|
||||
/**
|
||||
* Copies the given cursor into a in-memory cursor and then closes it.
|
||||
*
|
||||
*
|
||||
* This is useful for iterating over a cursor multiple times without the cost of JNI while
|
||||
* reading or IO while filling the window at the expense of memory consumption.
|
||||
*
|
||||
* @param c the cursor to copy.
|
||||
* @return a new cursor containing the same data as the given cursor.
|
||||
*/
|
||||
fun copyAndClose(c: Cursor): Cursor = c.useCursor { cursor ->
|
||||
val matrixCursor = MatrixCursor(cursor.columnNames, cursor.count)
|
||||
while (cursor.moveToNext()) {
|
||||
val row = arrayOfNulls<Any>(cursor.columnCount)
|
||||
for (i in 0 until c.columnCount) {
|
||||
when (cursor.getType(i)) {
|
||||
Cursor.FIELD_TYPE_NULL -> row[i] = null
|
||||
Cursor.FIELD_TYPE_INTEGER -> row[i] = cursor.getLong(i)
|
||||
Cursor.FIELD_TYPE_FLOAT -> row[i] = cursor.getDouble(i)
|
||||
Cursor.FIELD_TYPE_STRING -> row[i] = cursor.getString(i)
|
||||
Cursor.FIELD_TYPE_BLOB -> row[i] = cursor.getBlob(i)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
matrixCursor.addRow(row)
|
||||
}
|
||||
matrixCursor
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches [Cursor.getColumnIndex] to work around issues on older devices.
|
||||
* If the column is not found, it retries with the specified name surrounded by backticks.
|
||||
*
|
||||
* @param c The cursor.
|
||||
* @param name The name of the target column.
|
||||
* @return The index of the column, or -1 if not found.
|
||||
*/
|
||||
fun getColumnIndex(c: Cursor, name: String): Int {
|
||||
var index = c.getColumnIndex(name)
|
||||
if (index >= 0) {
|
||||
return index
|
||||
}
|
||||
index = c.getColumnIndex("`$name`")
|
||||
return if (index >= 0) {
|
||||
index
|
||||
} else {
|
||||
findColumnIndexBySuffix(c, name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches [Cursor.getColumnIndexOrThrow] to work around issues on older devices.
|
||||
* If the column is not found, it retries with the specified name surrounded by backticks.
|
||||
*
|
||||
* @param c The cursor.
|
||||
* @param name The name of the target column.
|
||||
* @return The index of the column.
|
||||
* @throws IllegalArgumentException if the column does not exist.
|
||||
*/
|
||||
fun getColumnIndexOrThrow(c: Cursor, name: String): Int {
|
||||
val index: Int = getColumnIndex(c, name)
|
||||
if (index >= 0) {
|
||||
return index
|
||||
}
|
||||
val availableColumns = try {
|
||||
c.columnNames.joinToString()
|
||||
} catch (e: Exception) {
|
||||
Log.d("RoomCursorUtil", "Cannot collect column names for debug purposes", e)
|
||||
"unknown"
|
||||
}
|
||||
throw IllegalArgumentException(
|
||||
"column '$name' does not exist. Available columns: $availableColumns"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a column by name by appending `.` in front of it and checking by suffix match.
|
||||
* Also checks for the version wrapped with `` (backticks).
|
||||
* workaround for b/157261134 for API levels 25 and below
|
||||
*
|
||||
* e.g. "foo" will match "any.foo" and "`any.foo`"
|
||||
*/
|
||||
private fun findColumnIndexBySuffix(cursor: Cursor, name: String): Int {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
|
||||
// we need this workaround only on APIs < 26. So just return not found on newer APIs
|
||||
return -1
|
||||
}
|
||||
if (name.isEmpty()) {
|
||||
return -1
|
||||
}
|
||||
val columnNames = cursor.columnNames
|
||||
return findColumnIndexBySuffix(columnNames, name)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun findColumnIndexBySuffix(columnNames: Array<String>, name: String): Int {
|
||||
val dotSuffix = ".$name"
|
||||
val backtickSuffix = ".$name`"
|
||||
columnNames.forEachIndexed { index, columnName ->
|
||||
// do not check if column name is not long enough. 1 char for table name, 1 char for '.'
|
||||
if (columnName.length >= name.length + 2) {
|
||||
if (columnName.endsWith(dotSuffix)) {
|
||||
return index
|
||||
} else if (columnName[0] == '`' && columnName.endsWith(backtickSuffix)) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Backwards compatible function that executes the given block function on this Cursor and then
|
||||
* closes the Cursor.
|
||||
*/
|
||||
inline fun <R> Cursor.useCursor(block: (Cursor) -> R): R {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
|
||||
return this.use(block)
|
||||
} else {
|
||||
try {
|
||||
return block(this)
|
||||
} finally {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given cursor such that `getColumnIndex()` will utilize the provided
|
||||
* `mapping` when getting the index of a column in `columnNames`.
|
||||
*
|
||||
* This is useful when the original cursor contains duplicate columns. Instead of letting the
|
||||
* cursor return the first matching column with a name, we can resolve the ambiguous column
|
||||
* indices and wrap the cursor such that for a set of desired column indices, the returned
|
||||
* value will be that from the pre-computation.
|
||||
*
|
||||
* @param cursor the cursor to wrap.
|
||||
* @param columnNames the column names whose index are known. The result column index of the
|
||||
* column name at i will be at `mapping[i]`.
|
||||
* @param mapping the cursor column indices of the columns at `columnNames`.
|
||||
* @return the wrapped Cursor.
|
||||
*/
|
||||
fun wrapMappedColumns(cursor: Cursor, columnNames: Array<String>, mapping: IntArray): Cursor {
|
||||
check(columnNames.size == mapping.size) { "Expected columnNames.length == mapping.length" }
|
||||
return object : CursorWrapper(cursor) {
|
||||
override fun getColumnIndex(columnName: String): Int {
|
||||
columnNames.forEachIndexed { i, mappedColumnName ->
|
||||
if (mappedColumnName.equals(columnName, ignoreCase = true)) {
|
||||
return mapping[i]
|
||||
}
|
||||
}
|
||||
return super.getColumnIndex(columnName)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:JvmName("DBUtil")
|
||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
|
||||
package androidx.room.util
|
||||
|
||||
import android.database.AbstractWindowedCursor
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteCompat
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Performs the SQLiteQuery on the given database.
|
||||
*
|
||||
* This util method encapsulates copying the cursor if the `maybeCopy` parameter is
|
||||
* `true` and either the api level is below a certain threshold or the full result of the
|
||||
* query does not fit in a single window.
|
||||
*
|
||||
* @param db The database to perform the query on.
|
||||
* @param sqLiteQuery The query to perform.
|
||||
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
|
||||
* @return Result of the query.
|
||||
*
|
||||
*/
|
||||
@Deprecated(
|
||||
"This is only used in the generated code and shouldn't be called directly."
|
||||
)
|
||||
fun query(db: RoomDatabase, sqLiteQuery: SupportSQLiteQuery, maybeCopy: Boolean): Cursor {
|
||||
return query(db, sqLiteQuery, maybeCopy, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the SQLiteQuery on the given database.
|
||||
*
|
||||
* This util method encapsulates copying the cursor if the `maybeCopy` parameter is
|
||||
* `true` and either the api level is below a certain threshold or the full result of the
|
||||
* query does not fit in a single window.
|
||||
*
|
||||
* @param db The database to perform the query on.
|
||||
* @param sqLiteQuery The query to perform.
|
||||
* @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
|
||||
* @param signal The cancellation signal to be attached to the query.
|
||||
* @return Result of the query.
|
||||
*/
|
||||
fun query(
|
||||
db: RoomDatabase,
|
||||
sqLiteQuery: SupportSQLiteQuery,
|
||||
maybeCopy: Boolean,
|
||||
signal: CancellationSignal?
|
||||
): Cursor {
|
||||
val cursor = db.query(sqLiteQuery, signal)
|
||||
if (maybeCopy && cursor is AbstractWindowedCursor) {
|
||||
val rowsInCursor = cursor.count // Should fill the window.
|
||||
val rowsInWindow = if (cursor.hasWindow()) {
|
||||
cursor.window.numRows
|
||||
} else {
|
||||
rowsInCursor
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
|
||||
return copyAndClose(cursor)
|
||||
}
|
||||
}
|
||||
return cursor
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops all FTS content sync triggers created by Room.
|
||||
*
|
||||
* FTS content sync triggers created by Room are those that are found in the sqlite_master table
|
||||
* who's names start with 'room_fts_content_sync_'.
|
||||
*
|
||||
* @param db The database.
|
||||
*/
|
||||
fun dropFtsSyncTriggers(db: SupportSQLiteDatabase) {
|
||||
val existingTriggers = buildList {
|
||||
db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'").useCursor { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
add(cursor.getString(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existingTriggers.forEach { triggerName ->
|
||||
if (triggerName.startsWith("room_fts_content_sync_")) {
|
||||
db.execSQL("DROP TRIGGER IF EXISTS $triggerName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for foreign key violations by executing a PRAGMA foreign_key_check.
|
||||
*/
|
||||
fun foreignKeyCheck(
|
||||
db: SupportSQLiteDatabase,
|
||||
tableName: String
|
||||
) {
|
||||
db.query("PRAGMA foreign_key_check(`$tableName`)").useCursor { cursor ->
|
||||
if (cursor.count > 0) {
|
||||
val errorMsg = processForeignKeyCheckFailure(cursor)
|
||||
throw SQLiteConstraintException(errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user version number out of the database header from the given file.
|
||||
*
|
||||
* @param databaseFile the database file.
|
||||
* @return the database version
|
||||
* @throws IOException if something goes wrong reading the file, such as bad database header or
|
||||
* missing permissions.
|
||||
*
|
||||
* @see [User Version
|
||||
* Number](https://www.sqlite.org/fileformat.html.user_version_number).
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun readVersion(databaseFile: File): Int {
|
||||
FileInputStream(databaseFile).channel.use { input ->
|
||||
val buffer = ByteBuffer.allocate(4)
|
||||
input.tryLock(60, 4, true)
|
||||
input.position(60)
|
||||
val read = input.read(buffer)
|
||||
if (read != 4) {
|
||||
throw IOException("Bad database header, unable to read 4 bytes at offset 60")
|
||||
}
|
||||
buffer.rewind()
|
||||
return buffer.int // ByteBuffer is big-endian by default
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CancellationSignal is only available from API 16 on. This function will create a new
|
||||
* instance of the Cancellation signal only if the current API > 16.
|
||||
*
|
||||
* @return A new instance of CancellationSignal or null.
|
||||
*/
|
||||
fun createCancellationSignal(): CancellationSignal? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
SupportSQLiteCompat.Api16Impl.createCancellationSignal()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the [Cursor] returned in case of a foreign key violation into a detailed
|
||||
* error message for debugging.
|
||||
*
|
||||
* The foreign_key_check pragma returns one row output for each foreign key violation.
|
||||
*
|
||||
* The cursor received has four columns for each row output. The first column is the name of
|
||||
* the child table. The second column is the rowId of the row that contains the foreign key
|
||||
* violation (or NULL if the child table is a WITHOUT ROWID table). The third column is the
|
||||
* name of the parent table. The fourth column is the index of the specific foreign key
|
||||
* constraint that failed.
|
||||
*
|
||||
* @param cursor Cursor containing information regarding the FK violation
|
||||
* @return Error message generated containing debugging information
|
||||
*/
|
||||
private fun processForeignKeyCheckFailure(cursor: Cursor): String {
|
||||
return buildString {
|
||||
val rowCount = cursor.count
|
||||
val fkParentTables = mutableMapOf<String, String>()
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
if (cursor.isFirst) {
|
||||
append("Foreign key violation(s) detected in '")
|
||||
append(cursor.getString(0)).append("'.\n")
|
||||
}
|
||||
val constraintIndex = cursor.getString(3)
|
||||
if (!fkParentTables.containsKey(constraintIndex)) {
|
||||
fkParentTables[constraintIndex] = cursor.getString(2)
|
||||
}
|
||||
}
|
||||
|
||||
append("Number of different violations discovered: ")
|
||||
append(fkParentTables.keys.size).append("\n")
|
||||
append("Number of rows in violation: ")
|
||||
append(rowCount).append("\n")
|
||||
append("Violation(s) detected in the following constraint(s):\n")
|
||||
|
||||
for ((key, value) in fkParentTables) {
|
||||
append("\tParent Table = ")
|
||||
append(value)
|
||||
append(", Foreign Key Constraint Index = ")
|
||||
append(key).append("\n")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:JvmName("FileUtil")
|
||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
|
||||
package androidx.room.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.annotation.RestrictTo
|
||||
import java.io.IOException
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.channels.ReadableByteChannel
|
||||
|
||||
/**
|
||||
* Copies data from the input channel to the output file channel.
|
||||
*
|
||||
* @param input the input channel to copy.
|
||||
* @param output the output channel to copy.
|
||||
* @throws IOException if there is an I/O error.
|
||||
*/
|
||||
@SuppressLint("LambdaLast")
|
||||
@Throws(IOException::class)
|
||||
fun copy(input: ReadableByteChannel, output: FileChannel) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
||||
output.transferFrom(input, 0, Long.MAX_VALUE)
|
||||
} else {
|
||||
val inputStream = Channels.newInputStream(input)
|
||||
val outputStream = Channels.newOutputStream(output)
|
||||
var length: Int
|
||||
val buffer = ByteArray(1024 * 4)
|
||||
|
||||
// TODO: Use Kotlin stdlib IO APIs
|
||||
while (inputStream.read(buffer).also { length = it } > 0) {
|
||||
outputStream.write(buffer, 0, length)
|
||||
}
|
||||
}
|
||||
output.force(false)
|
||||
} finally {
|
||||
input.close()
|
||||
output.close()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room.util
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import java.util.ArrayDeque
|
||||
|
||||
/**
|
||||
* A data class that holds the information about an FTS table.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
class FtsTableInfo(
|
||||
/**
|
||||
* The table name
|
||||
*/
|
||||
@JvmField
|
||||
val name: String,
|
||||
|
||||
/**
|
||||
* The column names
|
||||
*/
|
||||
@JvmField
|
||||
val columns: Set<String>,
|
||||
|
||||
/**
|
||||
* The set of options. Each value in the set contains the option in the following format:
|
||||
* <key, value>.
|
||||
*/
|
||||
@JvmField
|
||||
val options: Set<String>
|
||||
) {
|
||||
constructor(name: String, columns: Set<String>, createSql: String) :
|
||||
this(name, columns, parseOptions(createSql))
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is FtsTableInfo) return false
|
||||
val that = other
|
||||
if (name != that.name) return false
|
||||
if (columns != that.columns) return false
|
||||
return options == that.options
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + (columns.hashCode())
|
||||
result = 31 * result + (options.hashCode())
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("FtsTableInfo{name='$name', columns=$columns, options=$options'}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
// A set of valid FTS Options
|
||||
private val FTS_OPTIONS = arrayOf(
|
||||
"tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
|
||||
"order=", "prefix=", "uncompress="
|
||||
)
|
||||
|
||||
/**
|
||||
* Reads the table information from the given database.
|
||||
*
|
||||
* @param database The database to read the information from.
|
||||
* @param tableName The table name.
|
||||
* @return A FtsTableInfo containing the columns and options for the provided table name.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun read(database: SupportSQLiteDatabase, tableName: String): FtsTableInfo {
|
||||
val columns = readColumns(database, tableName)
|
||||
val options = readOptions(database, tableName)
|
||||
return FtsTableInfo(tableName, columns, options)
|
||||
}
|
||||
|
||||
private fun readColumns(database: SupportSQLiteDatabase, tableName: String): Set<String> {
|
||||
return buildSet {
|
||||
database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor ->
|
||||
if (cursor.columnCount > 0) {
|
||||
val nameIndex = cursor.getColumnIndex("name")
|
||||
while (cursor.moveToNext()) {
|
||||
add(cursor.getString(nameIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readOptions(database: SupportSQLiteDatabase, tableName: String): Set<String> {
|
||||
val sql = database.query(
|
||||
"SELECT * FROM sqlite_master WHERE `name` = '$tableName'"
|
||||
).useCursor { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
cursor.getString(cursor.getColumnIndexOrThrow("sql"))
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
return parseOptions(sql)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses FTS options from the create statement of an FTS table.
|
||||
*
|
||||
* This method assumes the given create statement is a valid well-formed SQLite statement as
|
||||
* defined in the [CREATE VIRTUAL TABLE
|
||||
* syntax diagram](https://www.sqlite.org/lang_createvtab.html).
|
||||
*
|
||||
* @param createStatement the "CREATE VIRTUAL TABLE" statement.
|
||||
* @return the set of FTS option key and values in the create statement.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@JvmStatic
|
||||
fun parseOptions(createStatement: String): Set<String> {
|
||||
if (createStatement.isEmpty()) {
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
// Module arguments are within the parenthesis followed by the module name.
|
||||
val argsString = createStatement.substring(
|
||||
createStatement.indexOf('(') + 1,
|
||||
createStatement.lastIndexOf(')')
|
||||
)
|
||||
|
||||
// Split the module argument string by the comma delimiter, keeping track of quotation
|
||||
// so that if the delimiter is found within a string literal we don't substring at the
|
||||
// wrong index. SQLite supports four ways of quoting keywords, see:
|
||||
// https://www.sqlite.org/lang_keywords.html
|
||||
val args = mutableListOf<String>()
|
||||
val quoteStack = ArrayDeque<Char>()
|
||||
var lastDelimiterIndex = -1
|
||||
argsString.forEachIndexed { i, value ->
|
||||
when (value) {
|
||||
'\'', '"', '`' ->
|
||||
if (quoteStack.isEmpty()) {
|
||||
quoteStack.push(value)
|
||||
} else if (quoteStack.peek() == value) {
|
||||
quoteStack.pop()
|
||||
}
|
||||
'[' -> if (quoteStack.isEmpty()) {
|
||||
quoteStack.push(value)
|
||||
}
|
||||
']' -> if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
|
||||
quoteStack.pop()
|
||||
}
|
||||
',' -> if (quoteStack.isEmpty()) {
|
||||
args.add(argsString.substring(lastDelimiterIndex + 1, i).trim { it <= ' ' })
|
||||
lastDelimiterIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add final argument.
|
||||
args.add(argsString.substring(lastDelimiterIndex + 1).trim())
|
||||
|
||||
// Match args against valid options, otherwise they are column definitions.
|
||||
val options = args.filter { arg ->
|
||||
FTS_OPTIONS.any { validOption ->
|
||||
arg.startsWith(validOption)
|
||||
}
|
||||
}.toSet()
|
||||
return options
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:JvmName("RelationUtil")
|
||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
|
||||
package androidx.room.util
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.collection.ArrayMap
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
/**
|
||||
* Utility function used in generated code to recursively fetch relationships when the amount of
|
||||
* keys exceed [RoomDatabase.MAX_BIND_PARAMETER_CNT].
|
||||
*
|
||||
* @param map - The map containing the relationship keys to fill-in.
|
||||
* @param isRelationCollection - True if [V] is a [Collection] which means it is non null.
|
||||
* @param fetchBlock - A lambda for calling the generated _fetchRelationship function.
|
||||
*/
|
||||
fun <K : Any, V> recursiveFetchHashMap(
|
||||
map: HashMap<K, V>,
|
||||
isRelationCollection: Boolean,
|
||||
fetchBlock: (HashMap<K, V>) -> Unit
|
||||
) {
|
||||
val tmpMap = HashMap<K, V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
|
||||
var count = 0
|
||||
for (key in map.keys) {
|
||||
// Safe because `V` is a nullable type arg when isRelationCollection == false and vice versa
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (isRelationCollection) {
|
||||
tmpMap[key] = map[key] as V
|
||||
} else {
|
||||
tmpMap[key] = null as V
|
||||
}
|
||||
count++
|
||||
if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
|
||||
// recursively load that batch
|
||||
fetchBlock(tmpMap)
|
||||
// for non collection relation, put the loaded batch in the original map,
|
||||
// not needed when dealing with collections since references are passed
|
||||
if (!isRelationCollection) {
|
||||
map.putAll(tmpMap)
|
||||
}
|
||||
tmpMap.clear()
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
// load the last batch
|
||||
fetchBlock(tmpMap)
|
||||
// for non collection relation, put the last batch in the original map
|
||||
if (!isRelationCollection) {
|
||||
map.putAll(tmpMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [recursiveFetchHashMap] but for [LongSparseArray].
|
||||
*/
|
||||
fun <V> recursiveFetchLongSparseArray(
|
||||
map: LongSparseArray<V>,
|
||||
isRelationCollection: Boolean,
|
||||
fetchBlock: (LongSparseArray<V>) -> Unit
|
||||
) {
|
||||
val tmpMap = LongSparseArray<V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
|
||||
var count = 0
|
||||
var mapIndex = 0
|
||||
val limit = map.size()
|
||||
while (mapIndex < limit) {
|
||||
if (isRelationCollection) {
|
||||
tmpMap.put(map.keyAt(mapIndex), map.valueAt(mapIndex))
|
||||
} else {
|
||||
// Safe because `V` is a nullable type arg when isRelationCollection == false
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
tmpMap.put(map.keyAt(mapIndex), null as V)
|
||||
}
|
||||
mapIndex++
|
||||
count++
|
||||
if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
|
||||
fetchBlock(tmpMap)
|
||||
if (!isRelationCollection) {
|
||||
map.putAll(tmpMap)
|
||||
}
|
||||
tmpMap.clear()
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
fetchBlock(tmpMap)
|
||||
if (!isRelationCollection) {
|
||||
map.putAll(tmpMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [recursiveFetchHashMap] but for [ArrayMap].
|
||||
*/
|
||||
fun <K : Any, V> recursiveFetchArrayMap(
|
||||
map: ArrayMap<K, V>,
|
||||
isRelationCollection: Boolean,
|
||||
fetchBlock: (ArrayMap<K, V>) -> Unit
|
||||
) {
|
||||
val tmpMap = ArrayMap<K, V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
|
||||
var count = 0
|
||||
var mapIndex = 0
|
||||
val limit = map.size
|
||||
while (mapIndex < limit) {
|
||||
if (isRelationCollection) {
|
||||
tmpMap[map.keyAt(mapIndex)] = map.valueAt(mapIndex)
|
||||
} else {
|
||||
tmpMap[map.keyAt(mapIndex)] = null
|
||||
}
|
||||
mapIndex++
|
||||
count++
|
||||
if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
|
||||
fetchBlock(tmpMap)
|
||||
if (!isRelationCollection) {
|
||||
// Cast needed to disambiguate from putAll(SimpleArrayMap)
|
||||
map.putAll(tmpMap as Map<K, V>)
|
||||
}
|
||||
tmpMap.clear()
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
fetchBlock(tmpMap)
|
||||
if (!isRelationCollection) {
|
||||
// Cast needed to disambiguate from putAll(SimpleArrayMap)
|
||||
map.putAll(tmpMap as Map<K, V>)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:JvmName("StringUtil")
|
||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
|
||||
package androidx.room.util
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.RestrictTo
|
||||
import java.lang.NumberFormatException
|
||||
import java.lang.StringBuilder
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmField
|
||||
val EMPTY_STRING_ARRAY = arrayOfNulls<String>(0)
|
||||
|
||||
/**
|
||||
* Returns a new StringBuilder to be used while producing SQL queries.
|
||||
*
|
||||
* @return A new or recycled StringBuilder
|
||||
*/
|
||||
fun newStringBuilder(): StringBuilder {
|
||||
// TODO pool:
|
||||
return StringBuilder()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bind variable placeholders (?) to the given string. Each placeholder is separated
|
||||
* by a comma.
|
||||
*
|
||||
* @param builder The StringBuilder for the query
|
||||
* @param count Number of placeholders
|
||||
*/
|
||||
fun appendPlaceholders(builder: StringBuilder, count: Int) {
|
||||
for (i in 0 until count) {
|
||||
builder.append("?")
|
||||
if (i < count - 1) {
|
||||
builder.append(",")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a comma separated list of integers to integer list.
|
||||
*
|
||||
*
|
||||
* If an input is malformed, it is omitted from the result.
|
||||
*
|
||||
* @param input Comma separated list of integers.
|
||||
* @return A List containing the integers or null if the input is null.
|
||||
*/
|
||||
fun splitToIntList(input: String?): List<Int>? {
|
||||
return input?.split(',')?.mapNotNull { item ->
|
||||
try {
|
||||
item.toInt()
|
||||
} catch (ex: NumberFormatException) {
|
||||
Log.e("ROOM", "Malformed integer list", ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the given list of integers into a comma separated list.
|
||||
*
|
||||
* @param input The list of integers.
|
||||
* @return Comma separated string composed of integers in the list. If the list is null, return
|
||||
* value is null.
|
||||
*/
|
||||
fun joinIntoString(input: List<Int>?): String? {
|
||||
return input?.joinToString(",")
|
||||
}
|
|
@ -0,0 +1,645 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room.util
|
||||
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.ColumnInfo.SQLiteTypeAffinity
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import java.util.Locale
|
||||
import java.util.TreeMap
|
||||
|
||||
/**
|
||||
* A data class that holds the information about a table.
|
||||
*
|
||||
* It directly maps to the result of `PRAGMA table_info(<table_name>)`. Check the
|
||||
* [PRAGMA table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
|
||||
* documentation for more details.
|
||||
*
|
||||
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
// if you change this class, you must change TableInfoValidationWriter.kt
|
||||
class TableInfo(
|
||||
/**
|
||||
* The table name.
|
||||
*/
|
||||
@JvmField
|
||||
val name: String,
|
||||
@JvmField
|
||||
val columns: Map<String, Column>,
|
||||
@JvmField
|
||||
val foreignKeys: Set<ForeignKey>,
|
||||
@JvmField
|
||||
val indices: Set<Index>? = null
|
||||
) {
|
||||
/**
|
||||
* Identifies from where the info object was created.
|
||||
*/
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@IntDef(value = [CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE])
|
||||
internal annotation class CreatedFrom()
|
||||
|
||||
/**
|
||||
* For backward compatibility with dbs created with older versions.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
constructor(
|
||||
name: String,
|
||||
columns: Map<String, Column>,
|
||||
foreignKeys: Set<ForeignKey>
|
||||
) : this(name, columns, foreignKeys, emptySet<Index>())
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is TableInfo) return false
|
||||
if (name != other.name) return false
|
||||
if (columns != other.columns) {
|
||||
return false
|
||||
}
|
||||
if (foreignKeys != other.foreignKeys) {
|
||||
return false
|
||||
}
|
||||
return if (indices == null || other.indices == null) {
|
||||
// if one us is missing index information, seems like we couldn't acquire the
|
||||
// information so we better skip.
|
||||
true
|
||||
} else indices == other.indices
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + columns.hashCode()
|
||||
result = 31 * result + foreignKeys.hashCode()
|
||||
// skip index, it is not reliable for comparison.
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("TableInfo{name='$name', columns=$columns, foreignKeys=$foreignKeys, " +
|
||||
"indices=$indices}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Identifier for when the info is created from an unknown source.
|
||||
*/
|
||||
const val CREATED_FROM_UNKNOWN = 0
|
||||
|
||||
/**
|
||||
* Identifier for when the info is created from an entity definition, such as generated code
|
||||
* by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
|
||||
*/
|
||||
const val CREATED_FROM_ENTITY = 1
|
||||
|
||||
/**
|
||||
* Identifier for when the info is created from the database itself, reading information
|
||||
* from a PRAGMA, such as table_info.
|
||||
*/
|
||||
const val CREATED_FROM_DATABASE = 2
|
||||
|
||||
/**
|
||||
* Reads the table information from the given database.
|
||||
*
|
||||
* @param database The database to read the information from.
|
||||
* @param tableName The table name.
|
||||
* @return A TableInfo containing the schema information for the provided table name.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun read(database: SupportSQLiteDatabase, tableName: String): TableInfo {
|
||||
return readTableInfo(
|
||||
database = database,
|
||||
tableName = tableName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the information about a database column.
|
||||
*/
|
||||
class Column(
|
||||
/**
|
||||
* The column name.
|
||||
*/
|
||||
@JvmField
|
||||
val name: String,
|
||||
/**
|
||||
* The column type affinity.
|
||||
*/
|
||||
@JvmField
|
||||
val type: String,
|
||||
/**
|
||||
* Whether or not the column can be NULL.
|
||||
*/
|
||||
@JvmField
|
||||
val notNull: Boolean,
|
||||
@JvmField
|
||||
val primaryKeyPosition: Int,
|
||||
@JvmField
|
||||
val defaultValue: String?,
|
||||
@CreatedFrom
|
||||
@JvmField
|
||||
val createdFrom: Int
|
||||
) {
|
||||
/**
|
||||
* The column type after it is normalized to one of the basic types according to
|
||||
* https://www.sqlite.org/datatype3.html Section 3.1.
|
||||
*
|
||||
*
|
||||
* This is the value Room uses for equality check.
|
||||
*/
|
||||
@SQLiteTypeAffinity
|
||||
@JvmField
|
||||
val affinity: Int = findAffinity(type)
|
||||
|
||||
@Deprecated("Use {@link Column#Column(String, String, boolean, int, String, int)} instead.")
|
||||
constructor(name: String, type: String, notNull: Boolean, primaryKeyPosition: Int) : this(
|
||||
name,
|
||||
type,
|
||||
notNull,
|
||||
primaryKeyPosition,
|
||||
null,
|
||||
CREATED_FROM_UNKNOWN
|
||||
)
|
||||
|
||||
/**
|
||||
* Implements https://www.sqlite.org/datatype3.html section 3.1
|
||||
*
|
||||
* @param type The type that was given to the sqlite
|
||||
* @return The normalized type which is one of the 5 known affinities
|
||||
*/
|
||||
@SQLiteTypeAffinity
|
||||
private fun findAffinity(type: String?): Int {
|
||||
if (type == null) {
|
||||
return ColumnInfo.BLOB
|
||||
}
|
||||
val uppercaseType = type.uppercase(Locale.US)
|
||||
if (uppercaseType.contains("INT")) {
|
||||
return ColumnInfo.INTEGER
|
||||
}
|
||||
if (uppercaseType.contains("CHAR") ||
|
||||
uppercaseType.contains("CLOB") ||
|
||||
uppercaseType.contains("TEXT")
|
||||
) {
|
||||
return ColumnInfo.TEXT
|
||||
}
|
||||
if (uppercaseType.contains("BLOB")) {
|
||||
return ColumnInfo.BLOB
|
||||
}
|
||||
if (uppercaseType.contains("REAL") ||
|
||||
uppercaseType.contains("FLOA") ||
|
||||
uppercaseType.contains("DOUB")
|
||||
) {
|
||||
return ColumnInfo.REAL
|
||||
}
|
||||
// sqlite returns NUMERIC here but it is like a catch all. We already
|
||||
// have UNDEFINED so it is better to use UNDEFINED for consistency.
|
||||
return ColumnInfo.UNDEFINED
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Checks if the default values provided match. Handles the special case in which the
|
||||
* default value is surrounded by parenthesis (e.g. encountered in b/182284899).
|
||||
*
|
||||
* Surrounding parenthesis are removed by SQLite when reading from the database, hence
|
||||
* this function will check if they are present in the actual value, if so, it will
|
||||
* compare the two values by ignoring the surrounding parenthesis.
|
||||
*
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@JvmStatic
|
||||
fun defaultValueEquals(current: String, other: String?): Boolean {
|
||||
if (current == other) {
|
||||
return true
|
||||
} else if (containsSurroundingParenthesis(current)) {
|
||||
return current.substring(1, current.length - 1).trim() == other
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for potential surrounding parenthesis, if found, removes them and checks if
|
||||
* remaining paranthesis are balanced. If so, the surrounding parenthesis are redundant,
|
||||
* and returns true.
|
||||
*/
|
||||
private fun containsSurroundingParenthesis(current: String): Boolean {
|
||||
if (current.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
var surroundingParenthesis = 0
|
||||
current.forEachIndexed { i, c ->
|
||||
if (i == 0 && c != '(') {
|
||||
return false
|
||||
}
|
||||
if (c == '(') {
|
||||
surroundingParenthesis++
|
||||
} else if (c == ')') {
|
||||
surroundingParenthesis--
|
||||
if (surroundingParenthesis == 0 && i != current.length - 1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return surroundingParenthesis == 0
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: problem probably here
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Column) return false
|
||||
if (Build.VERSION.SDK_INT >= 20) {
|
||||
if (primaryKeyPosition != other.primaryKeyPosition) return false
|
||||
} else {
|
||||
if (isPrimaryKey != other.isPrimaryKey) return false
|
||||
}
|
||||
if (name != other.name) return false
|
||||
if (notNull != other.notNull) return false
|
||||
// Only validate default value if it was defined in an entity, i.e. if the info
|
||||
// from the compiler itself has it. b/136019383
|
||||
if (
|
||||
createdFrom == CREATED_FROM_ENTITY &&
|
||||
other.createdFrom == CREATED_FROM_DATABASE &&
|
||||
defaultValue != null &&
|
||||
!defaultValueEquals(defaultValue, other.defaultValue)
|
||||
) {
|
||||
return false
|
||||
} else if (
|
||||
createdFrom == CREATED_FROM_DATABASE &&
|
||||
other.createdFrom == CREATED_FROM_ENTITY &&
|
||||
other.defaultValue != null &&
|
||||
!defaultValueEquals(other.defaultValue, defaultValue)
|
||||
) {
|
||||
return false
|
||||
} else if (
|
||||
createdFrom != CREATED_FROM_UNKNOWN &&
|
||||
createdFrom == other.createdFrom &&
|
||||
(if (defaultValue != null)
|
||||
!defaultValueEquals(defaultValue, other.defaultValue)
|
||||
else other.defaultValue != null)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return affinity == other.affinity
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this column is part of the primary key or not.
|
||||
*
|
||||
* @return True if this column is part of the primary key, false otherwise.
|
||||
*/
|
||||
val isPrimaryKey: Boolean
|
||||
get() = primaryKeyPosition > 0
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + affinity
|
||||
result = 31 * result + if (notNull) 1231 else 1237
|
||||
result = 31 * result + primaryKeyPosition
|
||||
// Default value is not part of the hashcode since we conditionally check it for
|
||||
// equality which would break the equals + hashcode contract.
|
||||
// result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("Column{name='$name', type='$type', affinity='$affinity', " +
|
||||
"notNull=$notNull, primaryKeyPosition=$primaryKeyPosition, " +
|
||||
"defaultValue='${defaultValue ?: "undefined"}'}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the information about an SQLite foreign key
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
class ForeignKey(
|
||||
@JvmField
|
||||
val referenceTable: String,
|
||||
@JvmField
|
||||
val onDelete: String,
|
||||
@JvmField
|
||||
val onUpdate: String,
|
||||
@JvmField
|
||||
val columnNames: List<String>,
|
||||
@JvmField
|
||||
val referenceColumnNames: List<String>
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ForeignKey) return false
|
||||
if (referenceTable != other.referenceTable) return false
|
||||
if (onDelete != other.onDelete) return false
|
||||
if (onUpdate != other.onUpdate) return false
|
||||
return if (columnNames != other.columnNames) false else referenceColumnNames ==
|
||||
other.referenceColumnNames
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = referenceTable.hashCode()
|
||||
result = 31 * result + onDelete.hashCode()
|
||||
result = 31 * result + onUpdate.hashCode()
|
||||
result = 31 * result + columnNames.hashCode()
|
||||
result = 31 * result + referenceColumnNames.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("ForeignKey{referenceTable='$referenceTable', onDelete='$onDelete +', " +
|
||||
"onUpdate='$onUpdate', columnNames=$columnNames, " +
|
||||
"referenceColumnNames=$referenceColumnNames}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary data holder for a foreign key row in the pragma result. We need this to ensure
|
||||
* sorting in the generated foreign key object.
|
||||
*/
|
||||
internal class ForeignKeyWithSequence(
|
||||
val id: Int,
|
||||
val sequence: Int,
|
||||
val from: String,
|
||||
val to: String
|
||||
) : Comparable<ForeignKeyWithSequence> {
|
||||
override fun compareTo(other: ForeignKeyWithSequence): Int {
|
||||
val idCmp = id - other.id
|
||||
return if (idCmp == 0) {
|
||||
sequence - other.sequence
|
||||
} else {
|
||||
idCmp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the information about an SQLite index
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
class Index(
|
||||
@JvmField
|
||||
val name: String,
|
||||
@JvmField
|
||||
val unique: Boolean,
|
||||
@JvmField
|
||||
val columns: List<String>,
|
||||
@JvmField
|
||||
var orders: List<String>
|
||||
) {
|
||||
init {
|
||||
orders = orders.ifEmpty {
|
||||
List(columns.size) { androidx.room.Index.Order.ASC.name }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// should match the value in Index.kt
|
||||
const val DEFAULT_PREFIX = "index_"
|
||||
}
|
||||
|
||||
@Deprecated("Use {@link #Index(String, boolean, List, List)}")
|
||||
constructor(name: String, unique: Boolean, columns: List<String>) : this(
|
||||
name,
|
||||
unique,
|
||||
columns,
|
||||
List<String>(columns.size) { androidx.room.Index.Order.ASC.name }
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Index) return false
|
||||
if (unique != other.unique) {
|
||||
return false
|
||||
}
|
||||
if (columns != other.columns) {
|
||||
return false
|
||||
}
|
||||
if (orders != other.orders) {
|
||||
return false
|
||||
}
|
||||
return if (name.startsWith(DEFAULT_PREFIX)) {
|
||||
other.name.startsWith(DEFAULT_PREFIX)
|
||||
} else {
|
||||
name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = if (name.startsWith(DEFAULT_PREFIX)) {
|
||||
DEFAULT_PREFIX.hashCode()
|
||||
} else {
|
||||
name.hashCode()
|
||||
}
|
||||
result = 31 * result + if (unique) 1 else 0
|
||||
result = 31 * result + columns.hashCode()
|
||||
result = 31 * result + orders.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("Index{name='$name', unique=$unique, columns=$columns, orders=$orders'}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun readTableInfo(database: SupportSQLiteDatabase, tableName: String): TableInfo {
|
||||
val columns = readColumns(database, tableName)
|
||||
val foreignKeys = readForeignKeys(database, tableName)
|
||||
val indices = readIndices(database, tableName)
|
||||
return TableInfo(tableName, columns, foreignKeys, indices)
|
||||
}
|
||||
|
||||
private fun readForeignKeys(
|
||||
database: SupportSQLiteDatabase,
|
||||
tableName: String
|
||||
): Set<TableInfo.ForeignKey> {
|
||||
// this seems to return everything in order but it is not documented so better be safe
|
||||
database.query("PRAGMA foreign_key_list(`$tableName`)").useCursor { cursor ->
|
||||
val idColumnIndex = cursor.getColumnIndex("id")
|
||||
val seqColumnIndex = cursor.getColumnIndex("seq")
|
||||
val tableColumnIndex = cursor.getColumnIndex("table")
|
||||
val onDeleteColumnIndex = cursor.getColumnIndex("on_delete")
|
||||
val onUpdateColumnIndex = cursor.getColumnIndex("on_update")
|
||||
val ordered = readForeignKeyFieldMappings(cursor)
|
||||
|
||||
// Reset cursor as readForeignKeyFieldMappings has moved it
|
||||
cursor.moveToPosition(-1)
|
||||
return buildSet {
|
||||
while (cursor.moveToNext()) {
|
||||
val seq = cursor.getInt(seqColumnIndex)
|
||||
if (seq != 0) {
|
||||
continue
|
||||
}
|
||||
val id = cursor.getInt(idColumnIndex)
|
||||
val myColumns = mutableListOf<String>()
|
||||
val refColumns = mutableListOf<String>()
|
||||
|
||||
ordered.filter {
|
||||
it.id == id
|
||||
}.forEach { key ->
|
||||
myColumns.add(key.from)
|
||||
refColumns.add(key.to)
|
||||
}
|
||||
|
||||
add(
|
||||
TableInfo.ForeignKey(
|
||||
referenceTable = cursor.getString(tableColumnIndex),
|
||||
onDelete = cursor.getString(onDeleteColumnIndex),
|
||||
onUpdate = cursor.getString(onUpdateColumnIndex),
|
||||
columnNames = myColumns,
|
||||
referenceColumnNames = refColumns
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readForeignKeyFieldMappings(cursor: Cursor): List<TableInfo.ForeignKeyWithSequence> {
|
||||
val idColumnIndex = cursor.getColumnIndex("id")
|
||||
val seqColumnIndex = cursor.getColumnIndex("seq")
|
||||
val fromColumnIndex = cursor.getColumnIndex("from")
|
||||
val toColumnIndex = cursor.getColumnIndex("to")
|
||||
|
||||
return buildList {
|
||||
while (cursor.moveToNext()) {
|
||||
add(
|
||||
TableInfo.ForeignKeyWithSequence(
|
||||
id = cursor.getInt(idColumnIndex),
|
||||
sequence = cursor.getInt(seqColumnIndex),
|
||||
from = cursor.getString(fromColumnIndex),
|
||||
to = cursor.getString(toColumnIndex)
|
||||
)
|
||||
)
|
||||
}
|
||||
}.sorted()
|
||||
}
|
||||
|
||||
private fun readColumns(
|
||||
database: SupportSQLiteDatabase,
|
||||
tableName: String
|
||||
): Map<String, TableInfo.Column> {
|
||||
database.query("PRAGMA table_info(`$tableName`)").useCursor { cursor ->
|
||||
if (cursor.columnCount <= 0) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val nameIndex = cursor.getColumnIndex("name")
|
||||
val typeIndex = cursor.getColumnIndex("type")
|
||||
val notNullIndex = cursor.getColumnIndex("notnull")
|
||||
val pkIndex = cursor.getColumnIndex("pk")
|
||||
val defaultValueIndex = cursor.getColumnIndex("dflt_value")
|
||||
|
||||
return buildMap {
|
||||
while (cursor.moveToNext()) {
|
||||
val name = cursor.getString(nameIndex)
|
||||
val type = cursor.getString(typeIndex)
|
||||
val notNull = 0 != cursor.getInt(notNullIndex)
|
||||
val primaryKeyPosition = cursor.getInt(pkIndex)
|
||||
val defaultValue = cursor.getString(defaultValueIndex)
|
||||
put(
|
||||
key = name,
|
||||
value = TableInfo.Column(
|
||||
name = name,
|
||||
type = type,
|
||||
notNull = notNull,
|
||||
primaryKeyPosition = primaryKeyPosition,
|
||||
defaultValue = defaultValue,
|
||||
createdFrom = TableInfo.CREATED_FROM_DATABASE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null if we cannot read the indices due to older sqlite implementations.
|
||||
*/
|
||||
private fun readIndices(database: SupportSQLiteDatabase, tableName: String): Set<TableInfo.Index>? {
|
||||
database.query("PRAGMA index_list(`$tableName`)").useCursor { cursor ->
|
||||
val nameColumnIndex = cursor.getColumnIndex("name")
|
||||
val originColumnIndex = cursor.getColumnIndex("origin")
|
||||
val uniqueIndex = cursor.getColumnIndex("unique")
|
||||
if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
|
||||
// we cannot read them so better not validate any index.
|
||||
return null
|
||||
}
|
||||
return buildSet {
|
||||
while (cursor.moveToNext()) {
|
||||
val origin = cursor.getString(originColumnIndex)
|
||||
if ("c" != origin) {
|
||||
// Ignore auto-created indices
|
||||
continue
|
||||
}
|
||||
val name = cursor.getString(nameColumnIndex)
|
||||
val unique = cursor.getInt(uniqueIndex) == 1
|
||||
// Read index but if we cannot read it properly so better not read it
|
||||
val index = readIndex(database, name, unique) ?: return null
|
||||
add(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null if we cannot read the index due to older sqlite implementations.
|
||||
*/
|
||||
private fun readIndex(
|
||||
database: SupportSQLiteDatabase,
|
||||
name: String,
|
||||
unique: Boolean
|
||||
): TableInfo.Index? {
|
||||
return database.query("PRAGMA index_xinfo(`$name`)").useCursor { cursor ->
|
||||
val seqnoColumnIndex = cursor.getColumnIndex("seqno")
|
||||
val cidColumnIndex = cursor.getColumnIndex("cid")
|
||||
val nameColumnIndex = cursor.getColumnIndex("name")
|
||||
val descColumnIndex = cursor.getColumnIndex("desc")
|
||||
if (
|
||||
seqnoColumnIndex == -1 ||
|
||||
cidColumnIndex == -1 ||
|
||||
nameColumnIndex == -1 ||
|
||||
descColumnIndex == -1
|
||||
) {
|
||||
// we cannot read them so better not validate any index.
|
||||
return null
|
||||
}
|
||||
val columnsMap = TreeMap<Int, String>()
|
||||
val ordersMap = TreeMap<Int, String>()
|
||||
while (cursor.moveToNext()) {
|
||||
val cid = cursor.getInt(cidColumnIndex)
|
||||
if (cid < 0) {
|
||||
// Ignore SQLite row ID
|
||||
continue
|
||||
}
|
||||
val seq = cursor.getInt(seqnoColumnIndex)
|
||||
val columnName = cursor.getString(nameColumnIndex)
|
||||
val order = if (cursor.getInt(descColumnIndex) > 0) "DESC" else "ASC"
|
||||
columnsMap[seq] = columnName
|
||||
ordersMap[seq] = order
|
||||
}
|
||||
val columns = columnsMap.values.toList()
|
||||
val orders = ordersMap.values.toList()
|
||||
TableInfo.Index(name, unique, columns, orders)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:JvmName("UUIDUtil")
|
||||
@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
|
||||
package androidx.room.util
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* UUID / byte[] two-way conversion utility for Room
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts a 16-bytes array BLOB into a UUID pojo
|
||||
*
|
||||
* @param bytes byte array stored in database as BLOB
|
||||
* @return a UUID object created based on the provided byte array
|
||||
*/
|
||||
fun convertByteToUUID(bytes: ByteArray): UUID {
|
||||
val buffer = ByteBuffer.wrap(bytes)
|
||||
val firstLong = buffer.long
|
||||
val secondLong = buffer.long
|
||||
return UUID(firstLong, secondLong)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a UUID pojo into a 16-bytes array to store into database as BLOB
|
||||
*
|
||||
* @param uuid the UUID pojo
|
||||
* @return a byte array to store into database
|
||||
*/
|
||||
fun convertUUIDToByte(uuid: UUID): ByteArray {
|
||||
val bytes = ByteArray(16)
|
||||
val buffer = ByteBuffer.wrap(bytes)
|
||||
buffer.putLong(uuid.mostSignificantBits)
|
||||
buffer.putLong(uuid.leastSignificantBits)
|
||||
return buffer.array()
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.room.util
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
/**
|
||||
* A data class that holds the information about a view.
|
||||
*
|
||||
*
|
||||
* This derives information from sqlite_master.
|
||||
*
|
||||
*
|
||||
* Even though SQLite column names are case insensitive, this class uses case sensitive matching.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
|
||||
class ViewInfo(
|
||||
/**
|
||||
* The view name
|
||||
*/
|
||||
@JvmField
|
||||
val name: String,
|
||||
/**
|
||||
* The SQL of CREATE VIEW.
|
||||
*/
|
||||
@JvmField
|
||||
val sql: String?
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ViewInfo) return false
|
||||
return ((name == other.name) && if (sql != null) sql == other.sql else other.sql == null)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name.hashCode()
|
||||
result = 31 * result + (sql?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("ViewInfo{" + "name='" + name + '\'' + ", sql='" + sql + '\'' + '}')
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Reads the view information from the given database.
|
||||
*
|
||||
* @param database The database to read the information from.
|
||||
* @param viewName The view name.
|
||||
* @return A ViewInfo containing the schema information for the provided view name.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun read(database: SupportSQLiteDatabase, viewName: String): ViewInfo {
|
||||
return database.query(
|
||||
"SELECT name, sql FROM sqlite_master " +
|
||||
"WHERE type = 'view' AND name = '$viewName'"
|
||||
).useCursor { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
ViewInfo(cursor.getString(0), cursor.getString(1))
|
||||
} else {
|
||||
ViewInfo(viewName, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -563,7 +563,7 @@ public abstract class DB extends RoomDatabase {
|
|||
if (BuildConfig.DEBUG && false)
|
||||
builder.setQueryCallback(new QueryCallback() {
|
||||
@Override
|
||||
public void onQuery(@NonNull String sqlQuery, @NonNull List<Object> bindArgs) {
|
||||
public void onQuery(@NonNull String sqlQuery, @NonNull List<?> bindArgs) {
|
||||
Log.i("query=" + sqlQuery);
|
||||
}
|
||||
}, Helper.getParallelExecutor());
|
||||
|
|
Loading…
Reference in New Issue