Merge changes from topic "query-callback" into androidx-master-dev

* changes:
  Follow-up CL resolving b/174478034. Adding functionality for handling bind arguments provided for queries.
  Implementing functionality for a general callback function for SQLite queries. If possible, bind arguments are provided to the callback in addition to the SQLite query statement. This callback may be used for logging executed queries, in which case it is recommended to use an immediate executor.
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
new file mode 100644
index 0000000..a23ce6b
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/QueryInterceptorTest.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2020 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.integration.kotlintestapp.test
+
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.Update
+import androidx.sqlite.db.SimpleSQLiteQuery
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CopyOnWriteArrayList
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class QueryInterceptorTest {
+    @Rule
+    @JvmField
+    val countingTaskExecutorRule = CountingTaskExecutorRule()
+    lateinit var mDatabase: QueryInterceptorTestDatabase
+    var queryAndArgs = CopyOnWriteArrayList<Pair<String, ArrayList<Any>>>()
+
+    @Entity(tableName = "queryInterceptorTestDatabase")
+    data class QueryInterceptorEntity(@PrimaryKey val id: String, val description: String)
+
+    @Dao
+    interface QueryInterceptorDao {
+        @Query("DELETE FROM queryInterceptorTestDatabase WHERE id=:id")
+        fun delete(id: String)
+
+        @Insert
+        fun insert(item: QueryInterceptorEntity)
+
+        @Update
+        fun update(vararg item: QueryInterceptorEntity)
+    }
+
+    @Database(
+        version = 1,
+        entities = [
+            QueryInterceptorEntity::class
+        ],
+        exportSchema = false
+    )
+    abstract class QueryInterceptorTestDatabase : RoomDatabase() {
+        abstract fun queryInterceptorDao(): QueryInterceptorDao
+    }
+
+    @Before
+    fun setUp() {
+        mDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(),
+            QueryInterceptorTestDatabase::class.java
+        ).setQueryCallback(
+            RoomDatabase.QueryCallback { sqlQuery, bindArgs ->
+                val argTrace = ArrayList<Any>()
+                argTrace.addAll(bindArgs)
+                queryAndArgs.add(Pair(sqlQuery, argTrace))
+            },
+            MoreExecutors.directExecutor()
+        ).build()
+    }
+
+    @After
+    fun tearDown() {
+        mDatabase.close()
+    }
+
+    @Test
+    fun testInsert() {
+        mDatabase.queryInterceptorDao().insert(
+            QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+        )
+
+        assertQueryLogged(
+            "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                "VALUES (?,?)",
+            listOf("Insert", "Inserted a placeholder query")
+        )
+        assertTransactionQueries()
+    }
+
+    @Test
+    fun testDelete() {
+        mDatabase.queryInterceptorDao().delete("Insert")
+        assertQueryLogged(
+            "DELETE FROM queryInterceptorTestDatabase WHERE id=?",
+            listOf("Insert")
+        )
+        assertTransactionQueries()
+    }
+
+    @Test
+    fun testUpdate() {
+        mDatabase.queryInterceptorDao().insert(
+            QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+        )
+        mDatabase.queryInterceptorDao().update(
+            QueryInterceptorEntity("Insert", "Updated the placeholder query")
+        )
+
+        assertQueryLogged(
+            "UPDATE OR ABORT `queryInterceptorTestDatabase` SET `id` " +
+                "= ?,`description` = ? " +
+                "WHERE `id` = ?",
+            listOf("Insert", "Updated the placeholder query", "Insert")
+        )
+        assertTransactionQueries()
+    }
+
+    @Test
+    fun testCompileStatement() {
+        assertEquals(queryAndArgs.size, 0)
+        mDatabase.queryInterceptorDao().insert(
+            QueryInterceptorEntity("Insert", "Inserted a placeholder query")
+        )
+        mDatabase.openHelper.writableDatabase.compileStatement(
+            "DELETE FROM queryInterceptorTestDatabase WHERE id=?"
+        ).execute()
+        assertQueryLogged("DELETE FROM queryInterceptorTestDatabase WHERE id=?", emptyList())
+    }
+
+    @Test
+    fun testLoggingSupportSQLiteQuery() {
+        mDatabase.openHelper.writableDatabase.query(
+            SimpleSQLiteQuery(
+                "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                    "VALUES (?,?)",
+                arrayOf<Any>("3", "Description")
+            )
+        )
+        assertQueryLogged(
+            "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                "VALUES (?,?)",
+            listOf("3", "Description")
+        )
+    }
+
+    @Test
+    fun testNullBindArgument() {
+        mDatabase.openHelper.writableDatabase.query(
+            SimpleSQLiteQuery(
+                "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`,`description`) " +
+                    "VALUES (?,?)",
+                arrayOf("ID", null)
+            )
+        )
+        assertQueryLogged(
+            "INSERT OR ABORT INTO `queryInterceptorTestDatabase` (`id`," +
+                "`description`) VALUES (?,?)",
+            listOf("ID", null)
+        )
+    }
+
+    private fun assertQueryLogged(
+        query: String,
+        expectedArgs: List<String?>
+    ) {
+        val filteredQueries = queryAndArgs.filter {
+            it.first == query
+        }
+        assertThat(filteredQueries).hasSize(1)
+        assertThat(expectedArgs).containsExactlyElementsIn(filteredQueries[0].second)
+    }
+
+    private fun assertTransactionQueries() {
+        assertNotNull(
+            queryAndArgs.any {
+                it.equals("BEGIN TRANSACTION")
+            }
+        )
+        assertNotNull(
+            queryAndArgs.any {
+                it.equals("TRANSACTION SUCCESSFUL")
+            }
+        )
+        assertNotNull(
+            queryAndArgs.any {
+                it.equals("END TRANSACTION")
+            }
+        )
+    }
+}
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
index e77a927..061b0ad 100644
--- a/room/runtime/api/current.txt
+++ b/room/runtime/api/current.txt
@@ -87,6 +87,7 @@
     method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
     method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
     method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+    method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
   }
@@ -115,6 +116,10 @@
     method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
   }
 
+  public static interface RoomDatabase.QueryCallback {
+    method public void onQuery(String, java.util.List<java.lang.Object!>);
+  }
+
 }
 
 package androidx.room.migration {
diff --git a/room/runtime/api/public_plus_experimental_current.txt b/room/runtime/api/public_plus_experimental_current.txt
index a35e99f..e1cefb2 100644
--- a/room/runtime/api/public_plus_experimental_current.txt
+++ b/room/runtime/api/public_plus_experimental_current.txt
@@ -88,6 +88,7 @@
     method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
     method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
     method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+    method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
   }
@@ -116,6 +117,10 @@
     method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
   }
 
+  public static interface RoomDatabase.QueryCallback {
+    method public void onQuery(String, java.util.List<java.lang.Object!>);
+  }
+
 }
 
 package androidx.room.migration {
diff --git a/room/runtime/api/restricted_current.txt b/room/runtime/api/restricted_current.txt
index b875d71..4b2d12a 100644
--- a/room/runtime/api/restricted_current.txt
+++ b/room/runtime/api/restricted_current.txt
@@ -130,6 +130,7 @@
     method public androidx.room.RoomDatabase.Builder<T!> fallbackToDestructiveMigrationOnDowngrade();
     method public androidx.room.RoomDatabase.Builder<T!> openHelperFactory(androidx.sqlite.db.SupportSQLiteOpenHelper.Factory?);
     method public androidx.room.RoomDatabase.Builder<T!> setJournalMode(androidx.room.RoomDatabase.JournalMode);
+    method public androidx.room.RoomDatabase.Builder<T!> setQueryCallback(androidx.room.RoomDatabase.QueryCallback, java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setQueryExecutor(java.util.concurrent.Executor);
     method public androidx.room.RoomDatabase.Builder<T!> setTransactionExecutor(java.util.concurrent.Executor);
   }
@@ -158,6 +159,10 @@
     method public void onOpenPrepackagedDatabase(androidx.sqlite.db.SupportSQLiteDatabase);
   }
 
+  public static interface RoomDatabase.QueryCallback {
+    method public void onQuery(String, java.util.List<java.lang.Object!>);
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class RoomOpenHelper extends androidx.sqlite.db.SupportSQLiteOpenHelper.Callback {
     ctor public RoomOpenHelper(androidx.room.DatabaseConfiguration, androidx.room.RoomOpenHelper.Delegate, String, String);
     ctor public RoomOpenHelper(androidx.room.DatabaseConfiguration, androidx.room.RoomOpenHelper.Delegate, String);
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java
new file mode 100644
index 0000000..c5ef6bc
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorDatabase.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2020 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.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+
+
+/**
+ * Implements {@link SupportSQLiteDatabase} for SQLite queries.
+ */
+final class QueryInterceptorDatabase implements SupportSQLiteDatabase {
+
+    private final SupportSQLiteDatabase mDelegate;
+    private final RoomDatabase.QueryCallback mQueryCallback;
+    private final Executor mQueryCallbackExecutor;
+
+    QueryInterceptorDatabase(@NonNull SupportSQLiteDatabase supportSQLiteDatabase,
+            @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
+            queryCallbackExecutor) {
+        mDelegate = supportSQLiteDatabase;
+        mQueryCallback = queryCallback;
+        mQueryCallbackExecutor = queryCallbackExecutor;
+    }
+
+    @NonNull
+    @Override
+    public SupportSQLiteStatement compileStatement(@NonNull String sql) {
+        return new QueryInterceptorStatement(mDelegate.compileStatement(sql),
+                mQueryCallback, sql, mQueryCallbackExecutor);
+    }
+
+    @Override
+    public void beginTransaction() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.beginTransaction();
+    }
+
+    @Override
+    public void beginTransactionNonExclusive() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.beginTransactionNonExclusive();
+    }
+
+    @Override
+    public void beginTransactionWithListener(@NonNull SQLiteTransactionListener
+            transactionListener) {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.beginTransactionWithListener(transactionListener);
+    }
+
+    @Override
+    public void beginTransactionWithListenerNonExclusive(
+            @NonNull SQLiteTransactionListener transactionListener) {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
+    }
+
+    @Override
+    public void endTransaction() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("END TRANSACTION",
+                Collections.emptyList()));
+        mDelegate.endTransaction();
+    }
+
+    @Override
+    public void setTransactionSuccessful() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("TRANSACTION SUCCESSFUL",
+                Collections.emptyList()));
+        mDelegate.setTransactionSuccessful();
+    }
+
+    @Override
+    public boolean inTransaction() {
+        return mDelegate.inTransaction();
+    }
+
+    @Override
+    public boolean isDbLockedByCurrentThread() {
+        return mDelegate.isDbLockedByCurrentThread();
+    }
+
+    @Override
+    public boolean yieldIfContendedSafely() {
+        return mDelegate.yieldIfContendedSafely();
+    }
+
+    @Override
+    public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
+        return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
+    }
+
+    @Override
+    public int getVersion() {
+        return mDelegate.getVersion();
+    }
+
+    @Override
+    public void setVersion(int version) {
+        mDelegate.setVersion(version);
+    }
+
+    @Override
+    public long getMaximumSize() {
+        return mDelegate.getMaximumSize();
+    }
+
+    @Override
+    public long setMaximumSize(long numBytes) {
+        return mDelegate.setMaximumSize(numBytes);
+    }
+
+    @Override
+    public long getPageSize() {
+        return mDelegate.getPageSize();
+    }
+
+    @Override
+    public void setPageSize(long numBytes) {
+        mDelegate.setPageSize(numBytes);
+    }
+
+    @NonNull
+    @Override
+    public Cursor query(@NonNull String query) {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
+                Collections.emptyList()));
+        return mDelegate.query(query);
+    }
+
+    @NonNull
+    @Override
+    public Cursor query(@NonNull String query, @NonNull Object[] bindArgs) {
+        List<Object> inputArguments = new ArrayList<>();
+        inputArguments.addAll(Arrays.asList(bindArgs));
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
+                inputArguments));
+        return mDelegate.query(query, bindArgs);
+    }
+
+    @NonNull
+    @Override
+    public Cursor query(@NonNull SupportSQLiteQuery query) {
+        QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
+        query.bindTo(queryInterceptorProgram);
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
+                queryInterceptorProgram.getBindArgs()));
+        return mDelegate.query(query);
+    }
+
+    @NonNull
+    @Override
+    public Cursor query(@NonNull SupportSQLiteQuery query,
+            @NonNull CancellationSignal cancellationSignal) {
+        QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
+        query.bindTo(queryInterceptorProgram);
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
+                queryInterceptorProgram.getBindArgs()));
+        return mDelegate.query(query);
+    }
+
+    @Override
+    public long insert(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values)
+            throws SQLException {
+        return mDelegate.insert(table, conflictAlgorithm, values);
+    }
+
+    @Override
+    public int delete(@NonNull String table, @NonNull String whereClause,
+            @NonNull Object[] whereArgs) {
+        return mDelegate.delete(table, whereClause, whereArgs);
+    }
+
+    @Override
+    public int update(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values,
+            @NonNull String whereClause,
+            @NonNull Object[] whereArgs) {
+        return mDelegate.update(table, conflictAlgorithm, values, whereClause,
+                whereArgs);
+    }
+
+    @Override
+    public void execSQL(@NonNull String sql) throws SQLException {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, new ArrayList<>(0)));
+        mDelegate.execSQL(sql);
+    }
+
+    @Override
+    public void execSQL(@NonNull String sql, @NonNull Object[] bindArgs) throws SQLException {
+        List<Object> inputArguments = new ArrayList<>();
+        inputArguments.addAll(Arrays.asList(bindArgs));
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, inputArguments));
+        mDelegate.execSQL(sql, inputArguments.toArray());
+    }
+
+    @Override
+    public boolean isReadOnly() {
+        return mDelegate.isReadOnly();
+    }
+
+    @Override
+    public boolean isOpen() {
+        return mDelegate.isOpen();
+    }
+
+    @Override
+    public boolean needUpgrade(int newVersion) {
+        return mDelegate.needUpgrade(newVersion);
+    }
+
+    @NonNull
+    @Override
+    public String getPath() {
+        return mDelegate.getPath();
+    }
+
+    @Override
+    public void setLocale(@NonNull Locale locale) {
+        mDelegate.setLocale(locale);
+    }
+
+    @Override
+    public void setMaxSqlCacheSize(int cacheSize) {
+        mDelegate.setMaxSqlCacheSize(cacheSize);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    @Override
+    public void setForeignKeyConstraintsEnabled(boolean enable) {
+        mDelegate.setForeignKeyConstraintsEnabled(enable);
+    }
+
+    @Override
+    public boolean enableWriteAheadLogging() {
+        return mDelegate.enableWriteAheadLogging();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    @Override
+    public void disableWriteAheadLogging() {
+        mDelegate.disableWriteAheadLogging();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    @Override
+    public boolean isWriteAheadLoggingEnabled() {
+        return mDelegate.isWriteAheadLoggingEnabled();
+    }
+
+    @NonNull
+    @Override
+    public List<Pair<String, String>> getAttachedDbs() {
+        return mDelegate.getAttachedDbs();
+    }
+
+    @Override
+    public boolean isDatabaseIntegrityOk() {
+        return mDelegate.isDatabaseIntegrityOk();
+    }
+
+    @Override
+    public void close() throws IOException {
+        mDelegate.close();
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java
new file mode 100644
index 0000000..c2ca486
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 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.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.util.concurrent.Executor;
+
+final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper {
+
+    private final SupportSQLiteOpenHelper mDelegate;
+    private final RoomDatabase.QueryCallback mQueryCallback;
+    private final Executor mQueryCallbackExecutor;
+
+    QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
+            @NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
+            queryCallbackExecutor) {
+        mDelegate = supportSQLiteOpenHelper;
+        mQueryCallback = queryCallback;
+        mQueryCallbackExecutor = queryCallbackExecutor;
+    }
+
+    @Nullable
+    @Override
+    public String getDatabaseName() {
+        return mDelegate.getDatabaseName();
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    @Override
+    public void setWriteAheadLoggingEnabled(boolean enabled) {
+        mDelegate.setWriteAheadLoggingEnabled(enabled);
+    }
+
+    @Override
+    public SupportSQLiteDatabase getWritableDatabase() {
+        return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback,
+                mQueryCallbackExecutor);
+    }
+
+    @Override
+    public SupportSQLiteDatabase getReadableDatabase() {
+        return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback,
+                mQueryCallbackExecutor);
+    }
+
+    @Override
+    public void close() {
+        mDelegate.close();
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java
new file mode 100644
index 0000000..0e573fa
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorOpenHelperFactory.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 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 androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper.
+ */
+@SuppressWarnings("AcronymName")
+final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
+
+    private final SupportSQLiteOpenHelper.Factory mDelegate;
+    private final RoomDatabase.QueryCallback mQueryCallback;
+    private final Executor mQueryCallbackExecutor;
+
+    @SuppressWarnings("LambdaLast")
+    QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory,
+            @NonNull RoomDatabase.QueryCallback queryCallback,
+            @NonNull Executor queryCallbackExecutor) {
+        mDelegate = factory;
+        mQueryCallback = queryCallback;
+        mQueryCallbackExecutor = queryCallbackExecutor;
+    }
+
+    @NonNull
+    @Override
+    public SupportSQLiteOpenHelper create(
+            @NonNull @NotNull SupportSQLiteOpenHelper.Configuration configuration) {
+        return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback,
+                mQueryCallbackExecutor);
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java
new file mode 100644
index 0000000..2b9c554
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorProgram.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 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 androidx.sqlite.db.SupportSQLiteProgram;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A program implementing an {@link SupportSQLiteProgram} API to record bind arguments.
+ */
+final class QueryInterceptorProgram implements SupportSQLiteProgram {
+    private List<Object> mBindArgsCache = new ArrayList<>();
+
+    @Override
+    public void bindNull(int index) {
+        saveArgsToCache(index, null);
+    }
+
+    @Override
+    public void bindLong(int index, long value) {
+        saveArgsToCache(index, value);
+    }
+
+    @Override
+    public void bindDouble(int index, double value) {
+        saveArgsToCache(index, value);
+    }
+
+    @Override
+    public void bindString(int index, String value) {
+        saveArgsToCache(index, value);
+    }
+
+    @Override
+    public void bindBlob(int index, byte[] value) {
+        saveArgsToCache(index, value);
+    }
+
+    @Override
+    public void clearBindings() {
+        mBindArgsCache.clear();
+    }
+
+    @Override
+    public void close() { }
+
+    private void saveArgsToCache(int bindIndex, Object value) {
+        // The index into bind methods are 1...n
+        int index = bindIndex - 1;
+        if (index >= mBindArgsCache.size()) {
+            for (int i = mBindArgsCache.size(); i <= index; i++) {
+                mBindArgsCache.add(null);
+            }
+        }
+        mBindArgsCache.set(index, value);
+    }
+
+    /**
+     * Returns the list of arguments associated with the query.
+     *
+     * @return argument list.
+     */
+    List<Object> getBindArgs() {
+        return mBindArgsCache;
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java b/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java
new file mode 100644
index 0000000..8825252
--- /dev/null
+++ b/room/runtime/src/main/java/androidx/room/QueryInterceptorStatement.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 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 androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Implements an instance of {@link SupportSQLiteStatement} for SQLite queries.
+ */
+final class QueryInterceptorStatement implements SupportSQLiteStatement {
+
+    private final SupportSQLiteStatement mDelegate;
+    private final RoomDatabase.QueryCallback mQueryCallback;
+    private final String mSqlStatement;
+    private final List<Object> mBindArgsCache = new ArrayList<>();
+    private final Executor mQueryCallbackExecutor;
+
+    QueryInterceptorStatement(@NonNull SupportSQLiteStatement compileStatement,
+            @NonNull RoomDatabase.QueryCallback queryCallback, String sqlStatement,
+            @NonNull Executor queryCallbackExecutor) {
+        mDelegate = compileStatement;
+        mQueryCallback = queryCallback;
+        mSqlStatement = sqlStatement;
+        mQueryCallbackExecutor = queryCallbackExecutor;
+    }
+
+    @Override
+    public void execute() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        mDelegate.execute();
+    }
+
+    @Override
+    public int executeUpdateDelete() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        return mDelegate.executeUpdateDelete();
+    }
+
+    @Override
+    public long executeInsert() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        return mDelegate.executeInsert();
+    }
+
+    @Override
+    public long simpleQueryForLong() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        return mDelegate.simpleQueryForLong();
+    }
+
+    @Override
+    public String simpleQueryForString() {
+        mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
+        return mDelegate.simpleQueryForString();
+    }
+
+    @Override
+    public void bindNull(int index) {
+        saveArgsToCache(index, mBindArgsCache.toArray());
+        mDelegate.bindNull(index);
+    }
+
+    @Override
+    public void bindLong(int index, long value) {
+        saveArgsToCache(index, value);
+        mDelegate.bindLong(index, value);
+    }
+
+    @Override
+    public void bindDouble(int index, double value) {
+        saveArgsToCache(index, value);
+        mDelegate.bindDouble(index, value);
+    }
+
+    @Override
+    public void bindString(int index, String value) {
+        saveArgsToCache(index, value);
+        mDelegate.bindString(index, value);
+    }
+
+    @Override
+    public void bindBlob(int index, byte[] value) {
+        saveArgsToCache(index, value);
+        mDelegate.bindBlob(index, value);
+    }
+
+    @Override
+    public void clearBindings() {
+        mBindArgsCache.clear();
+        mDelegate.clearBindings();
+    }
+
+    @Override
+    public void close() throws IOException {
+        mDelegate.close();
+    }
+
+    private void saveArgsToCache(int bindIndex, Object value) {
+        int index = bindIndex - 1;
+        if (index >= mBindArgsCache.size()) {
+            // Add null entries to the list until we have the desired # of indices
+            for (int i = mBindArgsCache.size(); i <= index; i++) {
+                mBindArgsCache.add(null);
+            }
+        }
+        mBindArgsCache.set(index, value);
+    }
+}
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
index 7fbf181..76e6a8f 100644
--- a/room/runtime/src/main/java/androidx/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -620,6 +620,8 @@
         private final Context mContext;
         private ArrayList<Callback> mCallbacks;
         private PrepackagedDatabaseCallback mPrepackagedDatabaseCallback;
+        private QueryCallback mQueryCallback;
+        private Executor mQueryCallbackExecutor;
         private List<Object> mTypeConverters;
 
         /** The Executor used to run database queries. This should be background-threaded. */
@@ -1092,6 +1094,27 @@
         }
 
         /**
+         * Sets a {@link QueryCallback} to be invoked when queries are executed.
+         * <p>
+         * The callback is invoked whenever a query is executed, note that adding this callback
+         * has a small cost and should be avoided in production builds unless needed.
+         * <p>
+         * A use case for providing a callback is to allow logging executed queries. When the
+         * callback implementation logs then it is recommended to use an immediate executor.
+         *
+         * @param queryCallback The query callback.
+         * @param executor The executor on which the query callback will be invoked.
+         */
+        @SuppressWarnings("MissingGetterMatchingBuilder")
+        @NonNull
+        public Builder<T> setQueryCallback(@NonNull QueryCallback queryCallback,
+                @NonNull Executor executor) {
+            mQueryCallback = queryCallback;
+            mQueryCallbackExecutor = executor;
+            return this;
+        }
+
+        /**
          * Adds a type converter instance to this database.
          *
          * @param typeConverter The converter. It must be an instance of a class annotated with
@@ -1173,6 +1196,12 @@
                 mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
                         mCopyFromInputStream, mFactory);
             }
+
+            if (mQueryCallback != null) {
+                mFactory = new QueryInterceptorOpenHelperFactory(mFactory, mQueryCallback,
+                        mQueryCallbackExecutor);
+            }
+
             DatabaseConfiguration configuration =
                     new DatabaseConfiguration(
                             mContext,
@@ -1345,4 +1374,21 @@
         public void onOpenPrepackagedDatabase(@NonNull SupportSQLiteDatabase db) {
         }
     }
+
+    /**
+     * Callback interface for when SQLite queries are executed.
+     *
+     * @see RoomDatabase.Builder#setQueryCallback
+     */
+    public interface QueryCallback {
+
+        /**
+         * Called when a SQL query is executed.
+         *
+         * @param sqlQuery The SQLite query statement.
+         * @param bindArgs Arguments of the query if available, empty list otherwise.
+         */
+        void onQuery(@NonNull String sqlQuery, @NonNull List<Object>
+                bindArgs);
+    }
 }