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