Home | History | Annotate | Download | only in room
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.room;
     18 
     19 import android.database.Cursor;
     20 import android.database.sqlite.SQLiteException;
     21 import android.util.Log;
     22 
     23 import androidx.annotation.NonNull;
     24 import androidx.annotation.Nullable;
     25 import androidx.annotation.RestrictTo;
     26 import androidx.annotation.VisibleForTesting;
     27 import androidx.annotation.WorkerThread;
     28 import androidx.arch.core.internal.SafeIterableMap;
     29 import androidx.collection.ArrayMap;
     30 import androidx.collection.ArraySet;
     31 import androidx.arch.core.executor.ArchTaskExecutor;
     32 import androidx.sqlite.db.SupportSQLiteDatabase;
     33 import androidx.sqlite.db.SupportSQLiteStatement;
     34 
     35 import java.lang.ref.WeakReference;
     36 import java.util.Arrays;
     37 import java.util.Collections;
     38 import java.util.Locale;
     39 import java.util.Map;
     40 import java.util.Set;
     41 import java.util.concurrent.atomic.AtomicBoolean;
     42 import java.util.concurrent.locks.Lock;
     43 
     44 /**
     45  * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
     46  * these tables.
     47  */
     48 // We create an in memory table with (version, table_id) where version is an auto-increment primary
     49 // key and a table_id (hardcoded int from initialization).
     50 // ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
     51 // Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
     52 // After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
     53 // tables.
     54 // Each update on one of the observed tables triggers an insertion into this table, hence a
     55 // new version.
     56 // Unfortunately, we cannot override the previous row because sqlite uses the conflict resolution
     57 // of the outer query (the thing that triggered us) so we do a cleanup as we sync instead of letting
     58 // SQLite override the rows.
     59 // https://sqlite.org/lang_createtrigger.html:  An ON CONFLICT clause may be specified as part of an
     60 // UPDATE or INSERT action within the body of the trigger. However if an ON CONFLICT clause is
     61 // specified as part of the statement causing the trigger to fire, then conflict handling policy of
     62 // the outer statement is used instead.
     63 public class InvalidationTracker {
     64 
     65     private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
     66 
     67     private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
     68 
     69     private static final String VERSION_COLUMN_NAME = "version";
     70 
     71     private static final String TABLE_ID_COLUMN_NAME = "table_id";
     72 
     73     private static final String CREATE_VERSION_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
     74             + "(" + VERSION_COLUMN_NAME
     75             + " INTEGER PRIMARY KEY AUTOINCREMENT, "
     76             + TABLE_ID_COLUMN_NAME
     77             + " INTEGER)";
     78 
     79     @VisibleForTesting
     80     static final String CLEANUP_SQL = "DELETE FROM " + UPDATE_TABLE_NAME
     81             + " WHERE " + VERSION_COLUMN_NAME + " NOT IN( SELECT MAX("
     82             + VERSION_COLUMN_NAME + ") FROM " + UPDATE_TABLE_NAME
     83             + " GROUP BY " + TABLE_ID_COLUMN_NAME + ")";
     84 
     85     @VisibleForTesting
     86     // We always clean before selecting so it is unlikely to have the same row twice and if we
     87     // do, it is not a big deal, just more data in the cursor.
     88     static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
     89             + " WHERE " + VERSION_COLUMN_NAME
     90             + "  > ? ORDER BY " + VERSION_COLUMN_NAME + " ASC;";
     91 
     92     @NonNull
     93     @VisibleForTesting
     94     ArrayMap<String, Integer> mTableIdLookup;
     95     private String[] mTableNames;
     96 
     97     @NonNull
     98     @VisibleForTesting
     99     long[] mTableVersions;
    100 
    101     private Object[] mQueryArgs = new Object[1];
    102 
    103     // max id in the last syc
    104     private long mMaxVersion = 0;
    105 
    106     private final RoomDatabase mDatabase;
    107 
    108     AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
    109 
    110     private volatile boolean mInitialized = false;
    111 
    112     private volatile SupportSQLiteStatement mCleanupStatement;
    113 
    114     private ObservedTableTracker mObservedTableTracker;
    115 
    116     // should be accessed with synchronization only.
    117     @VisibleForTesting
    118     final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();
    119 
    120     /**
    121      * Used by the generated code.
    122      *
    123      * @hide
    124      */
    125     @SuppressWarnings("WeakerAccess")
    126     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    127     public InvalidationTracker(RoomDatabase database, String... tableNames) {
    128         mDatabase = database;
    129         mObservedTableTracker = new ObservedTableTracker(tableNames.length);
    130         mTableIdLookup = new ArrayMap<>();
    131         final int size = tableNames.length;
    132         mTableNames = new String[size];
    133         for (int id = 0; id < size; id++) {
    134             final String tableName = tableNames[id].toLowerCase(Locale.US);
    135             mTableIdLookup.put(tableName, id);
    136             mTableNames[id] = tableName;
    137         }
    138         mTableVersions = new long[tableNames.length];
    139         Arrays.fill(mTableVersions, 0);
    140     }
    141 
    142     /**
    143      * Internal method to initialize table tracking.
    144      * <p>
    145      * You should never call this method, it is called by the generated code.
    146      */
    147     void internalInit(SupportSQLiteDatabase database) {
    148         synchronized (this) {
    149             if (mInitialized) {
    150                 Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
    151                 return;
    152             }
    153 
    154             database.beginTransaction();
    155             try {
    156                 database.execSQL("PRAGMA temp_store = MEMORY;");
    157                 database.execSQL("PRAGMA recursive_triggers='ON';");
    158                 database.execSQL(CREATE_VERSION_TABLE_SQL);
    159                 database.setTransactionSuccessful();
    160             } finally {
    161                 database.endTransaction();
    162             }
    163             syncTriggers(database);
    164             mCleanupStatement = database.compileStatement(CLEANUP_SQL);
    165             mInitialized = true;
    166         }
    167     }
    168 
    169     private static void appendTriggerName(StringBuilder builder, String tableName,
    170             String triggerType) {
    171         builder.append("`")
    172                 .append("room_table_modification_trigger_")
    173                 .append(tableName)
    174                 .append("_")
    175                 .append(triggerType)
    176                 .append("`");
    177     }
    178 
    179     private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
    180         final String tableName = mTableNames[tableId];
    181         StringBuilder stringBuilder = new StringBuilder();
    182         for (String trigger : TRIGGERS) {
    183             stringBuilder.setLength(0);
    184             stringBuilder.append("DROP TRIGGER IF EXISTS ");
    185             appendTriggerName(stringBuilder, tableName, trigger);
    186             writableDb.execSQL(stringBuilder.toString());
    187         }
    188     }
    189 
    190     private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
    191         final String tableName = mTableNames[tableId];
    192         StringBuilder stringBuilder = new StringBuilder();
    193         for (String trigger : TRIGGERS) {
    194             stringBuilder.setLength(0);
    195             stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
    196             appendTriggerName(stringBuilder, tableName, trigger);
    197             stringBuilder.append(" AFTER ")
    198                     .append(trigger)
    199                     .append(" ON `")
    200                     .append(tableName)
    201                     .append("` BEGIN INSERT OR REPLACE INTO ")
    202                     .append(UPDATE_TABLE_NAME)
    203                     .append(" VALUES(null, ")
    204                     .append(tableId)
    205                     .append("); END");
    206             writableDb.execSQL(stringBuilder.toString());
    207         }
    208     }
    209 
    210     /**
    211      * Adds the given observer to the observers list and it will be notified if any table it
    212      * observes changes.
    213      * <p>
    214      * Database changes are pulled on another thread so in some race conditions, the observer might
    215      * be invoked for changes that were done before it is added.
    216      * <p>
    217      * If the observer already exists, this is a no-op call.
    218      * <p>
    219      * If one of the tables in the Observer does not exist in the database, this method throws an
    220      * {@link IllegalArgumentException}.
    221      *
    222      * @param observer The observer which listens the database for changes.
    223      */
    224     @WorkerThread
    225     public void addObserver(@NonNull Observer observer) {
    226         final String[] tableNames = observer.mTables;
    227         int[] tableIds = new int[tableNames.length];
    228         final int size = tableNames.length;
    229         long[] versions = new long[tableNames.length];
    230 
    231         // TODO sync versions ?
    232         for (int i = 0; i < size; i++) {
    233             Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
    234             if (tableId == null) {
    235                 throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
    236             }
    237             tableIds[i] = tableId;
    238             versions[i] = mMaxVersion;
    239         }
    240         ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames, versions);
    241         ObserverWrapper currentObserver;
    242         synchronized (mObserverMap) {
    243             currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
    244         }
    245         if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
    246             syncTriggers();
    247         }
    248     }
    249 
    250     /**
    251      * Adds an observer but keeps a weak reference back to it.
    252      * <p>
    253      * Note that you cannot remove this observer once added. It will be automatically removed
    254      * when the observer is GC'ed.
    255      *
    256      * @param observer The observer to which InvalidationTracker will keep a weak reference.
    257      * @hide
    258      */
    259     @SuppressWarnings("unused")
    260     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    261     public void addWeakObserver(Observer observer) {
    262         addObserver(new WeakObserver(this, observer));
    263     }
    264 
    265     /**
    266      * Removes the observer from the observers list.
    267      *
    268      * @param observer The observer to remove.
    269      */
    270     @SuppressWarnings("WeakerAccess")
    271     @WorkerThread
    272     public void removeObserver(@NonNull final Observer observer) {
    273         ObserverWrapper wrapper;
    274         synchronized (mObserverMap) {
    275             wrapper = mObserverMap.remove(observer);
    276         }
    277         if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
    278             syncTriggers();
    279         }
    280     }
    281 
    282     private boolean ensureInitialization() {
    283         if (!mDatabase.isOpen()) {
    284             return false;
    285         }
    286         if (!mInitialized) {
    287             // trigger initialization
    288             mDatabase.getOpenHelper().getWritableDatabase();
    289         }
    290         if (!mInitialized) {
    291             Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
    292             return false;
    293         }
    294         return true;
    295     }
    296 
    297     @VisibleForTesting
    298     Runnable mRefreshRunnable = new Runnable() {
    299         @Override
    300         public void run() {
    301             final Lock closeLock = mDatabase.getCloseLock();
    302             boolean hasUpdatedTable = false;
    303             try {
    304                 closeLock.lock();
    305 
    306                 if (!ensureInitialization()) {
    307                     return;
    308                 }
    309 
    310                 if (!mPendingRefresh.compareAndSet(true, false)) {
    311                     // no pending refresh
    312                     return;
    313                 }
    314 
    315                 if (mDatabase.inTransaction()) {
    316                     // current thread is in a transaction. when it ends, it will invoke
    317                     // refreshRunnable again. mPendingRefresh is left as false on purpose
    318                     // so that the last transaction can flip it on again.
    319                     return;
    320                 }
    321 
    322                 mCleanupStatement.executeUpdateDelete();
    323                 mQueryArgs[0] = mMaxVersion;
    324                 if (mDatabase.mWriteAheadLoggingEnabled) {
    325                     // This transaction has to be on the underlying DB rather than the RoomDatabase
    326                     // in order to avoid a recursive loop after endTransaction.
    327                     SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
    328                     try {
    329                         db.beginTransaction();
    330                         hasUpdatedTable = checkUpdatedTable();
    331                         db.setTransactionSuccessful();
    332                     } finally {
    333                         db.endTransaction();
    334                     }
    335                 } else {
    336                     hasUpdatedTable = checkUpdatedTable();
    337                 }
    338             } catch (IllegalStateException | SQLiteException exception) {
    339                 // may happen if db is closed. just log.
    340                 Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
    341                         exception);
    342             } finally {
    343                 closeLock.unlock();
    344             }
    345             if (hasUpdatedTable) {
    346                 synchronized (mObserverMap) {
    347                     for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
    348                         entry.getValue().checkForInvalidation(mTableVersions);
    349                     }
    350                 }
    351             }
    352         }
    353 
    354         private boolean checkUpdatedTable() {
    355             boolean hasUpdatedTable = false;
    356             Cursor cursor = mDatabase.query(SELECT_UPDATED_TABLES_SQL, mQueryArgs);
    357             //noinspection TryFinallyCanBeTryWithResources
    358             try {
    359                 while (cursor.moveToNext()) {
    360                     final long version = cursor.getLong(0);
    361                     final int tableId = cursor.getInt(1);
    362 
    363                     mTableVersions[tableId] = version;
    364                     hasUpdatedTable = true;
    365                     // result is ordered so we can safely do this assignment
    366                     mMaxVersion = version;
    367                 }
    368             } finally {
    369                 cursor.close();
    370             }
    371             return hasUpdatedTable;
    372         }
    373     };
    374 
    375     /**
    376      * Enqueues a task to refresh the list of updated tables.
    377      * <p>
    378      * This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
    379      * if you have another connection to the database or directly use {@link
    380      * SupportSQLiteDatabase}, you may need to call this manually.
    381      */
    382     @SuppressWarnings("WeakerAccess")
    383     public void refreshVersionsAsync() {
    384         // TODO we should consider doing this sync instead of async.
    385         if (mPendingRefresh.compareAndSet(false, true)) {
    386             ArchTaskExecutor.getInstance().executeOnDiskIO(mRefreshRunnable);
    387         }
    388     }
    389 
    390     /**
    391      * Check versions for tables, and run observers synchronously if tables have been updated.
    392      *
    393      * @hide
    394      */
    395     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    396     @WorkerThread
    397     public void refreshVersionsSync() {
    398         syncTriggers();
    399         mRefreshRunnable.run();
    400     }
    401 
    402     void syncTriggers(SupportSQLiteDatabase database) {
    403         if (database.inTransaction()) {
    404             // we won't run this inside another transaction.
    405             return;
    406         }
    407         try {
    408             // This method runs in a while loop because while changes are synced to db, another
    409             // runnable may be skipped. If we cause it to skip, we need to do its work.
    410             while (true) {
    411                 Lock closeLock = mDatabase.getCloseLock();
    412                 closeLock.lock();
    413                 try {
    414                     // there is a potential race condition where another mSyncTriggers runnable
    415                     // can start running right after we get the tables list to sync.
    416                     final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
    417                     if (tablesToSync == null) {
    418                         return;
    419                     }
    420                     final int limit = tablesToSync.length;
    421                     try {
    422                         database.beginTransaction();
    423                         for (int tableId = 0; tableId < limit; tableId++) {
    424                             switch (tablesToSync[tableId]) {
    425                                 case ObservedTableTracker.ADD:
    426                                     startTrackingTable(database, tableId);
    427                                     break;
    428                                 case ObservedTableTracker.REMOVE:
    429                                     stopTrackingTable(database, tableId);
    430                                     break;
    431                             }
    432                         }
    433                         database.setTransactionSuccessful();
    434                     } finally {
    435                         database.endTransaction();
    436                     }
    437                     mObservedTableTracker.onSyncCompleted();
    438                 } finally {
    439                     closeLock.unlock();
    440                 }
    441             }
    442         } catch (IllegalStateException | SQLiteException exception) {
    443             // may happen if db is closed. just log.
    444             Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
    445                     exception);
    446         }
    447     }
    448 
    449     /**
    450      * Called by RoomDatabase before each beginTransaction call.
    451      * <p>
    452      * It is important that pending trigger changes are applied to the database before any query
    453      * runs. Otherwise, we may miss some changes.
    454      * <p>
    455      * This api should eventually be public.
    456      */
    457     void syncTriggers() {
    458         if (!mDatabase.isOpen()) {
    459             return;
    460         }
    461         syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
    462     }
    463 
    464     /**
    465      * Wraps an observer and keeps the table information.
    466      * <p>
    467      * Internally table ids are used which may change from database to database so the table
    468      * related information is kept here rather than in the Observer.
    469      */
    470     @SuppressWarnings("WeakerAccess")
    471     static class ObserverWrapper {
    472         final int[] mTableIds;
    473         private final String[] mTableNames;
    474         private final long[] mVersions;
    475         final Observer mObserver;
    476         private final Set<String> mSingleTableSet;
    477 
    478         ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames, long[] versions) {
    479             mObserver = observer;
    480             mTableIds = tableIds;
    481             mTableNames = tableNames;
    482             mVersions = versions;
    483             if (tableIds.length == 1) {
    484                 ArraySet<String> set = new ArraySet<>();
    485                 set.add(mTableNames[0]);
    486                 mSingleTableSet = Collections.unmodifiableSet(set);
    487             } else {
    488                 mSingleTableSet = null;
    489             }
    490         }
    491 
    492         void checkForInvalidation(long[] versions) {
    493             Set<String> invalidatedTables = null;
    494             final int size = mTableIds.length;
    495             for (int index = 0; index < size; index++) {
    496                 final int tableId = mTableIds[index];
    497                 final long newVersion = versions[tableId];
    498                 final long currentVersion = mVersions[index];
    499                 if (currentVersion < newVersion) {
    500                     mVersions[index] = newVersion;
    501                     if (size == 1) {
    502                         // Optimization for a single-table observer
    503                         invalidatedTables = mSingleTableSet;
    504                     } else {
    505                         if (invalidatedTables == null) {
    506                             invalidatedTables = new ArraySet<>(size);
    507                         }
    508                         invalidatedTables.add(mTableNames[index]);
    509                     }
    510                 }
    511             }
    512             if (invalidatedTables != null) {
    513                 mObserver.onInvalidated(invalidatedTables);
    514             }
    515         }
    516     }
    517 
    518     /**
    519      * An observer that can listen for changes in the database.
    520      */
    521     public abstract static class Observer {
    522         final String[] mTables;
    523 
    524         /**
    525          * Observes the given list of tables.
    526          *
    527          * @param firstTable The table name
    528          * @param rest       More table names
    529          */
    530         @SuppressWarnings("unused")
    531         protected Observer(@NonNull String firstTable, String... rest) {
    532             mTables = Arrays.copyOf(rest, rest.length + 1);
    533             mTables[rest.length] = firstTable;
    534         }
    535 
    536         /**
    537          * Observes the given list of tables.
    538          *
    539          * @param tables The list of tables to observe for changes.
    540          */
    541         public Observer(@NonNull String[] tables) {
    542             // copy tables in case user modifies them afterwards
    543             mTables = Arrays.copyOf(tables, tables.length);
    544         }
    545 
    546         /**
    547          * Called when one of the observed tables is invalidated in the database.
    548          *
    549          * @param tables A set of invalidated tables. This is useful when the observer targets
    550          *               multiple tables and want to know which table is invalidated.
    551          */
    552         public abstract void onInvalidated(@NonNull Set<String> tables);
    553     }
    554 
    555 
    556     /**
    557      * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
    558      * triggers in the database.
    559      * <p>
    560      * This class is thread safe
    561      */
    562     static class ObservedTableTracker {
    563         static final int NO_OP = 0; // don't change trigger state for this table
    564         static final int ADD = 1; // add triggers for this table
    565         static final int REMOVE = 2; // remove triggers for this table
    566 
    567         // number of observers per table
    568         final long[] mTableObservers;
    569         // trigger state for each table at last sync
    570         // this field is updated when syncAndGet is called.
    571         final boolean[] mTriggerStates;
    572         // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
    573         final int[] mTriggerStateChanges;
    574 
    575         boolean mNeedsSync;
    576 
    577         /**
    578          * After we return non-null value from getTablesToSync, we expect a onSyncCompleted before
    579          * returning any non-null value from getTablesToSync.
    580          * This allows us to workaround any multi-threaded state syncing issues.
    581          */
    582         boolean mPendingSync;
    583 
    584         ObservedTableTracker(int tableCount) {
    585             mTableObservers = new long[tableCount];
    586             mTriggerStates = new boolean[tableCount];
    587             mTriggerStateChanges = new int[tableCount];
    588             Arrays.fill(mTableObservers, 0);
    589             Arrays.fill(mTriggerStates, false);
    590         }
    591 
    592         /**
    593          * @return true if # of triggers is affected.
    594          */
    595         boolean onAdded(int... tableIds) {
    596             boolean needTriggerSync = false;
    597             synchronized (this) {
    598                 for (int tableId : tableIds) {
    599                     final long prevObserverCount = mTableObservers[tableId];
    600                     mTableObservers[tableId] = prevObserverCount + 1;
    601                     if (prevObserverCount == 0) {
    602                         mNeedsSync = true;
    603                         needTriggerSync = true;
    604                     }
    605                 }
    606             }
    607             return needTriggerSync;
    608         }
    609 
    610         /**
    611          * @return true if # of triggers is affected.
    612          */
    613         boolean onRemoved(int... tableIds) {
    614             boolean needTriggerSync = false;
    615             synchronized (this) {
    616                 for (int tableId : tableIds) {
    617                     final long prevObserverCount = mTableObservers[tableId];
    618                     mTableObservers[tableId] = prevObserverCount - 1;
    619                     if (prevObserverCount == 1) {
    620                         mNeedsSync = true;
    621                         needTriggerSync = true;
    622                     }
    623                 }
    624             }
    625             return needTriggerSync;
    626         }
    627 
    628         /**
    629          * If this returns non-null, you must call onSyncCompleted.
    630          *
    631          * @return int[] An int array where the index for each tableId has the action for that
    632          * table.
    633          */
    634         @Nullable
    635         int[] getTablesToSync() {
    636             synchronized (this) {
    637                 if (!mNeedsSync || mPendingSync) {
    638                     return null;
    639                 }
    640                 final int tableCount = mTableObservers.length;
    641                 for (int i = 0; i < tableCount; i++) {
    642                     final boolean newState = mTableObservers[i] > 0;
    643                     if (newState != mTriggerStates[i]) {
    644                         mTriggerStateChanges[i] = newState ? ADD : REMOVE;
    645                     } else {
    646                         mTriggerStateChanges[i] = NO_OP;
    647                     }
    648                     mTriggerStates[i] = newState;
    649                 }
    650                 mPendingSync = true;
    651                 mNeedsSync = false;
    652                 return mTriggerStateChanges;
    653             }
    654         }
    655 
    656         /**
    657          * if getTablesToSync returned non-null, the called should call onSyncCompleted once it
    658          * is done.
    659          */
    660         void onSyncCompleted() {
    661             synchronized (this) {
    662                 mPendingSync = false;
    663             }
    664         }
    665     }
    666 
    667     /**
    668      * An Observer wrapper that keeps a weak reference to the given object.
    669      * <p>
    670      * This class with automatically unsubscribe when the wrapped observer goes out of memory.
    671      */
    672     static class WeakObserver extends Observer {
    673         final InvalidationTracker mTracker;
    674         final WeakReference<Observer> mDelegateRef;
    675 
    676         WeakObserver(InvalidationTracker tracker, Observer delegate) {
    677             super(delegate.mTables);
    678             mTracker = tracker;
    679             mDelegateRef = new WeakReference<>(delegate);
    680         }
    681 
    682         @Override
    683         public void onInvalidated(@NonNull Set<String> tables) {
    684             final Observer observer = mDelegateRef.get();
    685             if (observer == null) {
    686                 mTracker.removeObserver(this);
    687             } else {
    688                 observer.onInvalidated(tables);
    689             }
    690         }
    691     }
    692 }
    693