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)
InvalidationTracker(RoomDatabase database, String... tableNames)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      */
internalInit(SupportSQLiteDatabase database)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 
appendTriggerName(StringBuilder builder, String tableName, String triggerType)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 
stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId)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 
startTrackingTable(SupportSQLiteDatabase writableDb, int tableId)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
addObserver(@onNull Observer observer)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)
addWeakObserver(Observer observer)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
removeObserver(@onNull final Observer observer)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 
ensureInitialization()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")
refreshVersionsAsync()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
refreshVersionsSync()397     public void refreshVersionsSync() {
398         syncTriggers();
399         mRefreshRunnable.run();
400     }
401 
syncTriggers(SupportSQLiteDatabase database)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      */
syncTriggers()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 
ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames, long[] versions)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 
checkForInvalidation(long[] versions)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")
Observer(@onNull String firstTable, String... rest)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          */
Observer(@onNull String[] tables)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          */
onInvalidated(@onNull Set<String> tables)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 
ObservedTableTracker(int tableCount)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          */
onAdded(int... tableIds)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          */
onRemoved(int... tableIds)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
getTablesToSync()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          */
onSyncCompleted()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 
WeakObserver(InvalidationTracker tracker, Observer delegate)676         WeakObserver(InvalidationTracker tracker, Observer delegate) {
677             super(delegate.mTables);
678             mTracker = tracker;
679             mDelegateRef = new WeakReference<>(delegate);
680         }
681 
682         @Override
onInvalidated(@onNull Set<String> tables)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