mirror of
https://github.com/M66B/FairEmail.git
synced 2026-04-23 01:13:29 +02:00
Updated room
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user