public class

InvalidationTracker

extends java.lang.Object

 java.lang.Object

↳androidx.room.InvalidationTracker

Gradle dependencies

compile group: 'androidx.room', name: 'room-runtime', version: '2.5.0-alpha01'

  • groupId: androidx.room
  • artifactId: room-runtime
  • version: 2.5.0-alpha01

Artifact androidx.room:room-runtime:2.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.room:room-runtime android.arch.persistence.room:runtime

Androidx class mapping:

androidx.room.InvalidationTracker android.arch.persistence.room.InvalidationTracker

Overview

InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about these tables.

Summary

Constructors
publicInvalidationTracker(RoomDatabase database, java.util.Map<java.lang.String, java.lang.String> shadowTablesMap, java.util.Map<java.lang.String, java.util.Set> viewTables, java.lang.String tableNames[])

Used by the generated code.

publicInvalidationTracker(RoomDatabase database, java.lang.String tableNames[])

Used by the generated code.

Methods
public voidaddObserver(InvalidationTracker.Observer observer)

Adds the given observer to the observers list and it will be notified if any table it observes changes.

public voidaddWeakObserver(InvalidationTracker.Observer observer)

Adds an observer but keeps a weak reference back to it.

public LiveData<java.lang.Object>createLiveData(java.lang.String tableNames[], boolean inTransaction, java.util.concurrent.Callable<java.lang.Object> computeFunction)

Creates a LiveData that computes the given function once and for every other invalidation of the database.

public LiveData<java.lang.Object>createLiveData(java.lang.String tableNames[], java.util.concurrent.Callable<java.lang.Object> computeFunction)

Creates a LiveData that computes the given function once and for every other invalidation of the database.

public voidnotifyObserversByTableNames(java.lang.String tables[])

Notifies all the registered InvalidationTracker.Observers of table changes.

public voidrefreshVersionsAsync()

Enqueues a task to refresh the list of updated tables.

public voidrefreshVersionsSync()

Check versions for tables, and run observers synchronously if tables have been updated.

public voidremoveObserver(InvalidationTracker.Observer observer)

Removes the observer from the observers list.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public InvalidationTracker(RoomDatabase database, java.lang.String tableNames[])

Used by the generated code.

public InvalidationTracker(RoomDatabase database, java.util.Map<java.lang.String, java.lang.String> shadowTablesMap, java.util.Map<java.lang.String, java.util.Set> viewTables, java.lang.String tableNames[])

Used by the generated code.

Methods

public void addObserver(InvalidationTracker.Observer observer)

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 java.lang.IllegalArgumentException.

This method should be called on a background/worker thread as it performs database operations.

Parameters:

observer: The observer which listens the database for changes.

public void addWeakObserver(InvalidationTracker.Observer observer)

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.

Parameters:

observer: The observer to which InvalidationTracker will keep a weak reference.

public void removeObserver(InvalidationTracker.Observer observer)

Removes the observer from the observers list.

This method should be called on a background/worker thread as it performs database operations.

Parameters:

observer: The observer to remove.

public void refreshVersionsAsync()

Enqueues a task to refresh the list of updated tables.

This method is automatically called when RoomDatabase.endTransaction() is called but if you have another connection to the database or directly use SupportSQLiteDatabase, you may need to call this manually.

public void refreshVersionsSync()

Check versions for tables, and run observers synchronously if tables have been updated.

public void notifyObserversByTableNames(java.lang.String tables[])

Notifies all the registered InvalidationTracker.Observers of table changes.

This can be used for notifying invalidation that cannot be detected by this InvalidationTracker, for example, invalidation from another process.

Parameters:

tables: The invalidated tables.

public LiveData<java.lang.Object> createLiveData(java.lang.String tableNames[], java.util.concurrent.Callable<java.lang.Object> computeFunction)

Deprecated: Use InvalidationTracker.createLiveData(String[], boolean, Callable)

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.

Parameters:

computeFunction: The function that calculates the value
tableNames: The list of tables to observe

Returns:

A new LiveData that computes the given function when the given list of tables invalidates.

public LiveData<java.lang.Object> createLiveData(java.lang.String tableNames[], boolean inTransaction, java.util.concurrent.Callable<java.lang.Object> 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.

Parameters:

tableNames: The list of tables to observe
inTransaction: True if the computeFunction will be done in a transaction, false otherwise.
computeFunction: The function that calculates the value

Returns:

A new LiveData that computes the given function when the given list of tables invalidates.

Source

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.room;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.arch.core.internal.SafeIterableMap;
import androidx.lifecycle.LiveData;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteStatement;

import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;

/**
 * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
 * these tables.
 */
// Some details on how the InvalidationTracker works:
// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
// tables.
// * Each update (write operation) on one of the observed tables triggers an update into the
// memory table table, flipping the invalidated flag ON.
// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
// It works as an Observer, and notifies other instances of table invalidation.
public class InvalidationTracker {

    private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};

    private static final String UPDATE_TABLE_NAME = "room_table_modification_log";

    private static final String TABLE_ID_COLUMN_NAME = "table_id";

    private static final String INVALIDATED_COLUMN_NAME = "invalidated";

    private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
            + "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, "
            + INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)";

    @VisibleForTesting
    static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
            + " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";

    @VisibleForTesting
    static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
            + " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";

    @NonNull
    final HashMap<String, Integer> mTableIdLookup;
    final String[] mTableNames;

    @NonNull
    private Map<String, Set<String>> mViewTables;

    @Nullable
    AutoCloser mAutoCloser = null;

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final RoomDatabase mDatabase;

    AtomicBoolean mPendingRefresh = new AtomicBoolean(false);

    private volatile boolean mInitialized = false;

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    volatile SupportSQLiteStatement mCleanupStatement;

    private final ObservedTableTracker mObservedTableTracker;

    private final InvalidationLiveDataContainer mInvalidationLiveDataContainer;

    // should be accessed with synchronization only.
    @VisibleForTesting
    @SuppressLint("RestrictedApi")
    final SafeIterableMap<Observer, ObserverWrapper> mObserverMap = new SafeIterableMap<>();

    private MultiInstanceInvalidationClient mMultiInstanceInvalidationClient;

    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<String, String>(), Collections.<String, Set<String>>emptyMap(),
                tableNames);
    }

    /**
     * Used by the generated code.
     *
     * @hide
     */
    @SuppressWarnings("WeakerAccess")
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    public InvalidationTracker(RoomDatabase database, Map<String, String> shadowTablesMap,
            Map<String, Set<String>> viewTables, String... tableNames) {
        mDatabase = database;
        mObservedTableTracker = new ObservedTableTracker(tableNames.length);
        mTableIdLookup = new HashMap<>();
        mViewTables = viewTables;
        mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase);
        final int size = tableNames.length;
        mTableNames = new String[size];
        for (int id = 0; id < size; id++) {
            final String tableName = tableNames[id].toLowerCase(Locale.US);
            mTableIdLookup.put(tableName, id);
            String shadowTableName = shadowTablesMap.get(tableNames[id]);
            if (shadowTableName != null) {
                mTableNames[id] = shadowTableName.toLowerCase(Locale.US);
            } else {
                mTableNames[id] = tableName;
            }
        }
        // Adjust table id lookup for those tables whose shadow table is another already mapped
        // table (e.g. external content fts tables).
        for (Map.Entry<String, String> shadowTableEntry : shadowTablesMap.entrySet()) {
            String shadowTableName = shadowTableEntry.getValue().toLowerCase(Locale.US);
            if (mTableIdLookup.containsKey(shadowTableName)) {
                String tableName = shadowTableEntry.getKey().toLowerCase(Locale.US);
                mTableIdLookup.put(tableName, mTableIdLookup.get(shadowTableName));
            }
        }
    }

    /**
     * Sets the auto closer for this invalidation tracker so that the invalidation tracker can
     * ensure that the database is not closed if there are pending invalidations that haven't yet
     * been flushed.
     *
     * This also adds a callback to the autocloser to ensure that the InvalidationTracker is in
     * an ok state once the table is invalidated.
     *
     * This must be called before the database is used.
     *
     * @param autoCloser the autocloser associated with the db
     */
    void setAutoCloser(AutoCloser autoCloser) {
        this.mAutoCloser = autoCloser;
        mAutoCloser.setAutoCloseCallback(this::onAutoCloseCallback);
    }

    /**
     * Internal method to initialize table tracking.
     * <p>
     * You should never call this method, it is called by the generated code.
     */
    void internalInit(SupportSQLiteDatabase database) {
        synchronized (this) {
            if (mInitialized) {
                Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
                return;
            }

            // These actions are not in a transaction because temp_store is not allowed to be
            // performed on a transaction, and recursive_triggers is not affected by transactions.
            database.execSQL("PRAGMA temp_store = MEMORY;");
            database.execSQL("PRAGMA recursive_triggers='ON';");
            database.execSQL(CREATE_TRACKING_TABLE_SQL);
            syncTriggers(database);
            mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);
            mInitialized = true;
        }
    }

    void onAutoCloseCallback() {
        synchronized (this) {
            mInitialized = false;
            mObservedTableTracker.resetTriggerState();
        }
    }

    void startMultiInstanceInvalidation(Context context, String name, Intent serviceIntent) {
        mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name,
                serviceIntent, this, mDatabase.getQueryExecutor());
    }

    void stopMultiInstanceInvalidation() {
        if (mMultiInstanceInvalidationClient != null) {
            mMultiInstanceInvalidationClient.stop();
            mMultiInstanceInvalidationClient = null;
        }
    }

    private static void appendTriggerName(StringBuilder builder, String tableName,
            String triggerType) {
        builder.append("`")
                .append("room_table_modification_trigger_")
                .append(tableName)
                .append("_")
                .append(triggerType)
                .append("`");
    }

    private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
        final String tableName = mTableNames[tableId];
        StringBuilder stringBuilder = new StringBuilder();
        for (String trigger : TRIGGERS) {
            stringBuilder.setLength(0);
            stringBuilder.append("DROP TRIGGER IF EXISTS ");
            appendTriggerName(stringBuilder, tableName, trigger);
            writableDb.execSQL(stringBuilder.toString());
        }
    }

    private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
        writableDb.execSQL(
                "INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
        final String tableName = mTableNames[tableId];
        StringBuilder stringBuilder = new StringBuilder();
        for (String trigger : TRIGGERS) {
            stringBuilder.setLength(0);
            stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
            appendTriggerName(stringBuilder, tableName, trigger);
            stringBuilder.append(" AFTER ")
                    .append(trigger)
                    .append(" ON `")
                    .append(tableName)
                    .append("` BEGIN UPDATE ")
                    .append(UPDATE_TABLE_NAME)
                    .append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1")
                    .append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId)
                    .append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0")
                    .append("; END");
            writableDb.execSQL(stringBuilder.toString());
        }
    }

    /**
     * Adds the given observer to the observers list and it will be notified if any table it
     * observes changes.
     * <p>
     * Database changes are pulled on another thread so in some race conditions, the observer might
     * be invoked for changes that were done before it is added.
     * <p>
     * If the observer already exists, this is a no-op call.
     * <p>
     * If one of the tables in the Observer does not exist in the database, this method throws an
     * {@link IllegalArgumentException}.
     * <p>
     * This method should be called on a background/worker thread as it performs database
     * operations.
     *
     * @param observer The observer which listens the database for changes.
     */
    @SuppressLint("RestrictedApi")
    @WorkerThread
    public void addObserver(@NonNull Observer observer) {
        final String[] tableNames = resolveViews(observer.mTables);
        int[] tableIds = new int[tableNames.length];
        final int size = tableNames.length;

        for (int i = 0; i < size; i++) {
            Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
            if (tableId == null) {
                throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
            }
            tableIds[i] = tableId;
        }
        ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);
        ObserverWrapper currentObserver;
        synchronized (mObserverMap) {
            currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
        }
        if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
            syncTriggers();
        }
    }

    private String[] validateAndResolveTableNames(String[] tableNames) {
        String[] resolved = resolveViews(tableNames);
        for (String tableName : resolved) {
            if (!mTableIdLookup.containsKey(tableName.toLowerCase(Locale.US))) {
                throw new IllegalArgumentException("There is no table with name " + tableName);
            }
        }
        return resolved;
    }

    /**
     * Resolves the list of tables and views into a list of unique tables that are underlying them.
     *
     * @param names The names of tables or views.
     * @return The names of the underlying tables.
     */
    private String[] resolveViews(String[] names) {
        Set<String> tables = new HashSet<>();
        for (String name : names) {
            final String lowercase = name.toLowerCase(Locale.US);
            if (mViewTables.containsKey(lowercase)) {
                tables.addAll(mViewTables.get(lowercase));
            } else {
                tables.add(name);
            }
        }
        return tables.toArray(new String[tables.size()]);
    }

    private static void beginTransactionInternal(SupportSQLiteDatabase database) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
                && database.isWriteAheadLoggingEnabled()) {
            database.beginTransactionNonExclusive();
        } else {
            database.beginTransaction();
        }
    }

    /**
     * Adds an observer but keeps a weak reference back to it.
     * <p>
     * Note that you cannot remove this observer once added. It will be automatically removed
     * when the observer is GC'ed.
     *
     * @param observer The observer to which InvalidationTracker will keep a weak reference.
     * @hide
     */
    @SuppressWarnings("unused")
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    public void addWeakObserver(Observer observer) {
        addObserver(new WeakObserver(this, observer));
    }

    /**
     * Removes the observer from the observers list.
     * <p>
     * This method should be called on a background/worker thread as it performs database
     * operations.
     *
     * @param observer The observer to remove.
     */
    @SuppressLint("RestrictedApi")
    @SuppressWarnings("WeakerAccess")
    @WorkerThread
    public void removeObserver(@NonNull final Observer observer) {
        ObserverWrapper wrapper;
        synchronized (mObserverMap) {
            wrapper = mObserverMap.remove(observer);
        }
        if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
            syncTriggers();
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    boolean ensureInitialization() {
        if (!mDatabase.isOpen()) {
            return false;
        }
        if (!mInitialized) {
            // trigger initialization
            mDatabase.getOpenHelper().getWritableDatabase();
        }
        if (!mInitialized) {
            Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
            return false;
        }
        return true;
    }

    @VisibleForTesting
    Runnable mRefreshRunnable = new Runnable() {
        @Override
        public void run() {
            final Lock closeLock = mDatabase.getCloseLock();
            Set<Integer> invalidatedTableIds = null;
            closeLock.lock();
            try {

                if (!ensureInitialization()) {
                    return;
                }

                if (!mPendingRefresh.compareAndSet(true, false)) {
                    // no pending refresh
                    return;
                }

                if (mDatabase.inTransaction()) {
                    // current thread is in a transaction. when it ends, it will invoke
                    // refreshRunnable again. mPendingRefresh is left as false on purpose
                    // so that the last transaction can flip it on again.
                    return;
                }

                // This transaction has to be on the underlying DB rather than the RoomDatabase
                // in order to avoid a recursive loop after endTransaction.
                SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
                db.beginTransactionNonExclusive();
                try {
                    invalidatedTableIds = checkUpdatedTable();
                    db.setTransactionSuccessful();
                } finally {
                    db.endTransaction();
                }
            } catch (IllegalStateException | SQLiteException exception) {
                // may happen if db is closed. just log.
                Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
                        exception);
            } finally {
                closeLock.unlock();

                if (mAutoCloser != null) {
                    mAutoCloser.decrementCountAndScheduleClose();
                }
            }
            if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
                synchronized (mObserverMap) {
                    for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
                        entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds);
                    }
                }
            }
        }

        private Set<Integer> checkUpdatedTable() {
            HashSet<Integer> invalidatedTableIds = new HashSet<>();
            Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));
            //noinspection TryFinallyCanBeTryWithResources
            try {
                while (cursor.moveToNext()) {
                    final int tableId = cursor.getInt(0);
                    invalidatedTableIds.add(tableId);
                }
            } finally {
                cursor.close();
            }
            if (!invalidatedTableIds.isEmpty()) {
                mCleanupStatement.executeUpdateDelete();
            }
            return invalidatedTableIds;
        }
    };

    /**
     * Enqueues a task to refresh the list of updated tables.
     * <p>
     * This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
     * if you have another connection to the database or directly use {@link
     * SupportSQLiteDatabase}, you may need to call this manually.
     */
    @SuppressWarnings("WeakerAccess")
    public void refreshVersionsAsync() {
        // TODO we should consider doing this sync instead of async.
        if (mPendingRefresh.compareAndSet(false, true)) {
            if (mAutoCloser != null) {
                // refreshVersionsAsync is called with the ref count incremented from
                // RoomDatabase, so the db can't be closed here, but we need to be sure that our
                // db isn't closed until refresh is completed. This increment call must be
                // matched with a corresponding call in mRefreshRunnable.
                mAutoCloser.incrementCountAndEnsureDbIsOpen();
            }
            mDatabase.getQueryExecutor().execute(mRefreshRunnable);
        }
    }

    /**
     * Check versions for tables, and run observers synchronously if tables have been updated.
     *
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    @WorkerThread
    public void refreshVersionsSync() {
        if (mAutoCloser != null) {
            // This increment call must be matched with a corresponding call in mRefreshRunnable.
            mAutoCloser.incrementCountAndEnsureDbIsOpen();
        }
        syncTriggers();
        mRefreshRunnable.run();
    }

    /**
     * Notifies all the registered {@link Observer}s of table changes.
     * <p>
     * This can be used for notifying invalidation that cannot be detected by this
     * {@link InvalidationTracker}, for example, invalidation from another process.
     *
     * @param tables The invalidated tables.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
    public void notifyObserversByTableNames(String... tables) {
        synchronized (mObserverMap) {
            for (Map.Entry<Observer, ObserverWrapper> entry : mObserverMap) {
                if (!entry.getKey().isRemote()) {
                    entry.getValue().notifyByTableNames(tables);
                }
            }
        }
    }

    void syncTriggers(SupportSQLiteDatabase database) {
        if (database.inTransaction()) {
            // we won't run this inside another transaction.
            return;
        }
        try {
            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.
     * <p>
     * It is important that pending trigger changes are applied to the database before any query
     * runs. Otherwise, we may miss some changes.
     * <p>
     * This api should eventually be public.
     */
    void syncTriggers() {
        if (!mDatabase.isOpen()) {
            return;
        }
        syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
    }

    /**
     * Creates a LiveData that computes the given function once and for every other invalidation
     * of the database.
     * <p>
     * Holds a strong reference to the created LiveData as long as it is active.
     *
     * @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
     *
     * @param computeFunction The function that calculates the value
     * @param tableNames      The list of tables to observe
     * @param <T>             The return type
     * @return A new LiveData that computes the given function when the given list of tables
     * invalidates.
     * @hide
     */
    @Deprecated
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    public <T> LiveData<T> createLiveData(String[] tableNames, Callable<T> computeFunction) {
        return createLiveData(tableNames, false, computeFunction);
    }

    /**
     * Creates a LiveData that computes the given function once and for every other invalidation
     * of the database.
     * <p>
     * Holds a strong reference to the created LiveData as long as it is active.
     *
     * @param tableNames      The list of tables to observe
     * @param inTransaction   True if the computeFunction will be done in a transaction, false
     *                        otherwise.
     * @param computeFunction The function that calculates the value
     * @param <T>             The return type
     * @return A new LiveData that computes the given function when the given list of tables
     * invalidates.
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
    public <T> LiveData<T> createLiveData(String[] tableNames, boolean inTransaction,
            Callable<T> computeFunction) {
        return mInvalidationLiveDataContainer.create(
                validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
    }

    /**
     * Wraps an observer and keeps the table information.
     * <p>
     * Internally table ids are used which may change from database to database so the table
     * related information is kept here rather than in the Observer.
     */
    @SuppressWarnings("WeakerAccess")
    static class ObserverWrapper {
        final int[] mTableIds;
        private final String[] mTableNames;
        final Observer mObserver;
        private final Set<String> mSingleTableSet;

        ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) {
            mObserver = observer;
            mTableIds = tableIds;
            mTableNames = tableNames;
            if (tableIds.length == 1) {
                HashSet<String> set = new HashSet<>();
                set.add(mTableNames[0]);
                mSingleTableSet = Collections.unmodifiableSet(set);
            } else {
                mSingleTableSet = null;
            }
        }

        /**
         * Notifies the underlying {@link #mObserver} if any of the observed tables are invalidated
         * based on the given invalid status set.
         *
         * @param invalidatedTablesIds The table ids of the tables that are invalidated.
         */
        void notifyByTableInvalidStatus(Set<Integer> invalidatedTablesIds) {
            Set<String> invalidatedTables = null;
            final int size = mTableIds.length;
            for (int index = 0; index < size; index++) {
                final int tableId = mTableIds[index];
                if (invalidatedTablesIds.contains(tableId)) {
                    if (size == 1) {
                        // Optimization for a single-table observer
                        invalidatedTables = mSingleTableSet;
                    } else {
                        if (invalidatedTables == null) {
                            invalidatedTables = new HashSet<>(size);
                        }
                        invalidatedTables.add(mTableNames[index]);
                    }
                }
            }
            if (invalidatedTables != null) {
                mObserver.onInvalidated(invalidatedTables);
            }
        }

        /**
         * Notifies the underlying {@link #mObserver} if it observes any of the specified
         * {@code tables}.
         *
         * @param tables The invalidated table names.
         */
        void notifyByTableNames(String[] tables) {
            Set<String> invalidatedTables = null;
            if (mTableNames.length == 1) {
                for (String table : tables) {
                    if (table.equalsIgnoreCase(mTableNames[0])) {
                        // Optimization for a single-table observer
                        invalidatedTables = mSingleTableSet;
                        break;
                    }
                }
            } else {
                HashSet<String> set = new HashSet<>();
                for (String table : tables) {
                    for (String ourTable : mTableNames) {
                        if (ourTable.equalsIgnoreCase(table)) {
                            set.add(ourTable);
                            break;
                        }
                    }
                }
                if (set.size() > 0) {
                    invalidatedTables = set;
                }
            }
            if (invalidatedTables != null) {
                mObserver.onInvalidated(invalidatedTables);
            }
        }
    }

    /**
     * An observer that can listen for changes in the database.
     */
    public abstract static class Observer {
        final String[] mTables;

        /**
         * Observes the given list of tables and views.
         *
         * @param firstTable The name of the table or view.
         * @param rest       More names of tables or views.
         */
        @SuppressWarnings("unused")
        protected Observer(@NonNull String firstTable, String... rest) {
            mTables = Arrays.copyOf(rest, rest.length + 1);
            mTables[rest.length] = firstTable;
        }

        /**
         * Observes the given list of tables and views.
         *
         * @param tables The list of tables or views to observe for changes.
         */
        public Observer(@NonNull String[] tables) {
            // copy tables in case user modifies them afterwards
            mTables = Arrays.copyOf(tables, tables.length);
        }

        /**
         * Called when one of the observed tables is invalidated in the database.
         *
         * @param tables A set of invalidated tables. This is useful when the observer targets
         *               multiple tables and you want to know which table is invalidated. This will
         *               be names of underlying tables when you are observing views.
         */
        public abstract void onInvalidated(@NonNull Set<String> tables);

        boolean isRemote() {
            return false;
        }
    }

    /**
     * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
     * triggers in the database.
     * <p>
     * This class is thread safe
     */
    static class ObservedTableTracker {
        static final int NO_OP = 0; // don't change trigger state for this table
        static final int ADD = 1; // add triggers for this table
        static final int REMOVE = 2; // remove triggers for this table

        // number of observers per table
        final long[] mTableObservers;
        // trigger state for each table at last sync
        // this field is updated when syncAndGet is called.
        final boolean[] mTriggerStates;
        // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
        final int[] mTriggerStateChanges;

        boolean mNeedsSync;

        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.
     * <p>
     * This class will automatically unsubscribe when the wrapped observer goes out of memory.
     */
    static class WeakObserver extends Observer {
        final InvalidationTracker mTracker;
        final WeakReference<Observer> mDelegateRef;

        WeakObserver(InvalidationTracker tracker, Observer delegate) {
            super(delegate.mTables);
            mTracker = tracker;
            mDelegateRef = new WeakReference<>(delegate);
        }

        @Override
        public void onInvalidated(@NonNull Set<String> tables) {
            final Observer observer = mDelegateRef.get();
            if (observer == null) {
                mTracker.removeObserver(this);
            } else {
                observer.onInvalidated(tables);
            }
        }
    }
}