mirror of https://github.com/M66B/FairEmail.git
919 lines
35 KiB
Java
919 lines
35 KiB
Java
/*
|
|
* 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 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;
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
} 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 {
|
|
// This method runs in a while loop because while changes are synced to db, another
|
|
// runnable may be skipped. If we cause it to skip, we need to do its work.
|
|
while (true) {
|
|
Lock closeLock = mDatabase.getCloseLock();
|
|
closeLock.lock();
|
|
try {
|
|
// there is a potential race condition where another mSyncTriggers runnable
|
|
// can start running right after we get the tables list to sync.
|
|
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();
|
|
}
|
|
mObservedTableTracker.onSyncCompleted();
|
|
} 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;
|
|
|
|
/**
|
|
* After we return non-null value from getTablesToSync, we expect a onSyncCompleted before
|
|
* returning any non-null value from getTablesToSync.
|
|
* This allows us to workaround any multi-threaded state syncing issues.
|
|
*/
|
|
boolean mPendingSync;
|
|
|
|
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, you must call onSyncCompleted.
|
|
*
|
|
* @return int[] An int array where the index for each tableId has the action for that
|
|
* table.
|
|
*/
|
|
@Nullable
|
|
int[] getTablesToSync() {
|
|
synchronized (this) {
|
|
if (!mNeedsSync || mPendingSync) {
|
|
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;
|
|
}
|
|
mPendingSync = true;
|
|
mNeedsSync = false;
|
|
return mTriggerStateChanges;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* if getTablesToSync returned non-null, the called should call onSyncCompleted once it
|
|
* is done.
|
|
*/
|
|
void onSyncCompleted() {
|
|
synchronized (this) {
|
|
mPendingSync = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
}
|
|
}
|