/* * 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 mTableIdLookup; final String[] mTableNames; @NonNull private Map> 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 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(), Collections.>emptyMap(), tableNames); } /** * Used by the generated code. * * @hide */ @SuppressWarnings("WeakerAccess") @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public InvalidationTracker(RoomDatabase database, Map shadowTablesMap, Map> 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 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. *

* 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. *

* 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 * {@link 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 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 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. *

* 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. *

* 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 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 entry : mObserverMap) { entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds); } } } } private Set checkUpdatedTable() { HashSet 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. *

* 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. *

* 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 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. *

* 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. */ 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. *

* 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 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 LiveData createLiveData(String[] tableNames, Callable computeFunction) { 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 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 LiveData createLiveData(String[] tableNames, boolean inTransaction, Callable computeFunction) { return mInvalidationLiveDataContainer.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. */ @SuppressWarnings("WeakerAccess") static class ObserverWrapper { final int[] mTableIds; private final String[] mTableNames; final Observer mObserver; private final Set mSingleTableSet; ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) { mObserver = observer; mTableIds = tableIds; mTableNames = tableNames; if (tableIds.length == 1) { HashSet 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 invalidatedTablesIds) { Set 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 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 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 tables); boolean isRemote() { return 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 */ 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. *

* This class will automatically unsubscribe when the wrapped observer goes out of memory. */ static class WeakObserver extends Observer { final InvalidationTracker mTracker; final WeakReference mDelegateRef; WeakObserver(InvalidationTracker tracker, Observer delegate) { super(delegate.mTables); mTracker = tracker; mDelegateRef = new WeakReference<>(delegate); } @Override public void onInvalidated(@NonNull Set tables) { final Observer observer = mDelegateRef.get(); if (observer == null) { mTracker.removeObserver(this); } else { observer.onInvalidated(tables); } } } }