LiveDataQuery

This CL adds support for returning LiveData<T> from select queries
in DAO classes.

Rather than extending ResultAdapter to handle this, I've created
a new class called ResultBinder which binds the sql and args with
the ResultAdapter. This would be the place for Rx etc to hook
to implement their observability.

Bug: 32342709
Test: LifeDataQueryTest
Change-Id: I44c7d0571e3d92df5d07fb3802791fc793bf5617
diff --git a/room/build.gradle b/room/build.gradle
deleted file mode 100644
index 2b4a885..0000000
--- a/room/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2016 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.
- */
-
-buildscript {
-    ext.supportRootFolder = new File(project.projectDir, "../")
-    apply from: '../app-toolkit/init.gradle'
-    ext.addRepos(repositories)
-    dependencies {
-        classpath "com.android.tools.build:gradle:$android_gradle_plugin_version"
-        if (enablePublicRepos) {
-            classpath "com.android.databinding:localizemaven:${localize_maven_version}"
-        }
-    }
-}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt b/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
index 4939d92..2b1dba5 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/ext/javapoet_ext.kt
@@ -61,6 +61,14 @@
             ClassName.get("com.android.support.room", "SharedSQLiteStatement")
     val INVALIDATION_TRACKER : ClassName =
             ClassName.get("com.android.support.room", "InvalidationTracker")
+    val INVALIDATION_OBSERVER : ClassName =
+            ClassName.get("com.android.support.room.InvalidationTracker", "Observer")
+}
+
+object LifecyclesTypeNames {
+    val LIVE_DATA: ClassName = ClassName.get("com.android.support.lifecycle", "LiveData")
+    val COMPUTABLE_LIVE_DATA : ClassName = ClassName.get("com.android.support.lifecycle",
+            "ComputableLiveData")
 }
 
 object AndroidTypeNames {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/Context.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/Context.kt
index 5f2a2b3..3deee97 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/Context.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/Context.kt
@@ -16,11 +16,14 @@
 
 package com.android.support.room.processor
 
+import com.android.support.room.ext.LifecyclesTypeNames
+import com.android.support.room.ext.RoomTypeNames
 import com.android.support.room.log.RLog
 import com.android.support.room.preconditions.Checks
 import com.android.support.room.solver.TypeAdapterStore
 import javax.annotation.processing.ProcessingEnvironment
 import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.type.TypeMirror
 
 data class Context(val processingEnv: ProcessingEnvironment) {
     val logger = RLog(processingEnv)
@@ -32,5 +35,13 @@
         val STRING by lazy {
             processingEnv.elementUtils.getTypeElement("java.lang.String").asType()
         }
+        val LIVE_DATA: TypeMirror? by lazy {
+            processingEnv.elementUtils.getTypeElement(LifecyclesTypeNames.LIVE_DATA.toString())
+                    ?.asType()
+        }
+        val COMPUTABLE_LIVE_DATA : TypeMirror? by lazy {
+            processingEnv.elementUtils.getTypeElement(LifecyclesTypeNames.COMPUTABLE_LIVE_DATA
+                    .toString())?.asType()
+        }
     }
 }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
index 80ca8b7..7158a72 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/ProcessorErrors.kt
@@ -89,6 +89,9 @@
     val DB_MUST_EXTEND_ROOM_DB = "Classes annotated with @Database should extend " +
             RoomTypeNames.ROOM_DB
 
+    val LIVE_DATA_QUERY_WITHOUT_SELECT = "LiveData return type can only be used with SELECT" +
+            " queries."
+
     private val TOO_MANY_MATCHING_GETTERS = "Ambiguous getter for %s. All of the following " +
             "match: %s. You can @Ignore the ones that you don't want to match."
 
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryMethodProcessor.kt
index 2a175f1..e1add0e 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/processor/QueryMethodProcessor.kt
@@ -20,6 +20,8 @@
 import com.android.support.room.parser.ParsedQuery
 import com.android.support.room.parser.QueryType
 import com.android.support.room.parser.SqlParser
+import com.android.support.room.solver.query.result.InstantQueryResultBinder
+import com.android.support.room.solver.query.result.LiveDataQueryResultBinder
 import com.android.support.room.vo.QueryMethod
 import com.google.auto.common.AnnotationMirrors
 import com.google.auto.common.MoreElements
@@ -65,10 +67,15 @@
             )
         }
 
-        val resultAdapter = context.typeAdapterStore
-                .findQueryResultAdapter(executableType.returnType)
-        context.checker.check(resultAdapter != null || query.type != QueryType.SELECT,
+        val resultBinder = context.typeAdapterStore
+                .findQueryResultBinder(executableType.returnType, query.tables)
+        context.checker.check(resultBinder.adapter != null || query.type != QueryType.SELECT,
                 executableElement, ProcessorErrors.CANNOT_FIND_QUERY_RESULT_ADAPTER)
+        if (resultBinder is LiveDataQueryResultBinder) {
+            context.checker.check(query.type == QueryType.SELECT, executableElement,
+                    ProcessorErrors.LIVE_DATA_QUERY_WITHOUT_SELECT)
+        }
+
         val queryMethod = QueryMethod(
                 element = executableElement,
                 query = query,
@@ -76,7 +83,7 @@
                 returnType = executableType.returnType,
                 parameters = executableElement.parameters
                         .map { parameterParser.parse(containing, it) },
-                resultAdapter = resultAdapter)
+                queryResultBinder = resultBinder)
 
         val missing = queryMethod.sectionToParamMapping
                 .filter { it.second == null }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
index 38510cd..51e399d 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/TypeAdapterStore.kt
@@ -17,7 +17,10 @@
 package com.android.support.room.solver
 
 import com.android.support.room.Entity
+import com.android.support.room.ext.LifecyclesTypeNames
+import com.android.support.room.ext.RoomTypeNames
 import com.android.support.room.ext.hasAnnotation
+import com.android.support.room.parser.Table
 import com.android.support.room.processor.Context
 import com.android.support.room.solver.query.parameter.ArrayQueryParameterAdapter
 import com.android.support.room.solver.query.parameter.BasicQueryParameterAdapter
@@ -25,8 +28,11 @@
 import com.android.support.room.solver.query.parameter.QueryParameterAdapter
 import com.android.support.room.solver.query.result.ArrayQueryResultAdapter
 import com.android.support.room.solver.query.result.EntityRowAdapter
+import com.android.support.room.solver.query.result.InstantQueryResultBinder
 import com.android.support.room.solver.query.result.ListQueryResultAdapter
+import com.android.support.room.solver.query.result.LiveDataQueryResultBinder
 import com.android.support.room.solver.query.result.QueryResultAdapter
+import com.android.support.room.solver.query.result.QueryResultBinder
 import com.android.support.room.solver.query.result.RowAdapter
 import com.android.support.room.solver.query.result.SingleColumnRowAdapter
 import com.android.support.room.solver.query.result.SingleEntityQueryResultAdapter
@@ -44,10 +50,12 @@
 import com.android.support.room.solver.types.ReverseTypeConverter
 import com.android.support.room.solver.types.StringColumnTypeAdapter
 import com.android.support.room.solver.types.TypeConverter
+import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
 import com.google.common.annotations.VisibleForTesting
 import java.util.LinkedList
 import javax.lang.model.type.ArrayType
+import javax.lang.model.type.DeclaredType
 import javax.lang.model.type.TypeKind
 import javax.lang.model.type.TypeMirror
 
@@ -56,16 +64,16 @@
  * Holds all type adapters and can create on demand composite type adapters to convert a type into a
  * database column.
  */
-class TypeAdapterStore(val context : Context,
-                       @VisibleForTesting vararg extras : Any) {
-    private val columnTypeAdapters : List<ColumnTypeAdapter>
-    private val typeConverters : List<TypeConverter>
+class TypeAdapterStore(val context: Context,
+                       @VisibleForTesting vararg extras: Any) {
+    private val columnTypeAdapters: List<ColumnTypeAdapter>
+    private val typeConverters: List<TypeConverter>
 
     init {
         val adapters = arrayListOf<ColumnTypeAdapter>()
         val converters = arrayListOf<TypeConverter>()
         extras.forEach {
-            when(it) {
+            when (it) {
                 is TypeConverter -> converters.add(it)
                 is ColumnTypeAdapter -> adapters.add(it)
                 else -> throw IllegalArgumentException("unknown extra")
@@ -121,7 +129,35 @@
         return findTypeConverter(input, listOf(output))
     }
 
-    fun findQueryResultAdapter(typeMirror: TypeMirror) : QueryResultAdapter? {
+    private fun isLiveData(declared : DeclaredType) : Boolean {
+        val typeElement = MoreElements.asType(declared.asElement())
+        val qName = typeElement.qualifiedName.toString()
+        // even though computable live data is internal, we still check for it as we may inherit
+        // it from some internal class.
+        return qName == LifecyclesTypeNames.COMPUTABLE_LIVE_DATA.toString() ||
+                qName == LifecyclesTypeNames.LIVE_DATA.toString()
+    }
+
+    fun findQueryResultBinder(typeMirror: TypeMirror, tables: Set<Table>): QueryResultBinder {
+        return if (typeMirror.kind == TypeKind.DECLARED) {
+            val declared = MoreTypes.asDeclared(typeMirror)
+            if (declared.typeArguments.isEmpty()) {
+                InstantQueryResultBinder(findQueryResultAdapter(typeMirror))
+            } else {
+                if (isLiveData(declared)) {
+                    val liveDataTypeArg = declared.typeArguments.first()
+                    LiveDataQueryResultBinder(liveDataTypeArg, tables,
+                            findQueryResultAdapter(liveDataTypeArg))
+                } else {
+                    InstantQueryResultBinder(findQueryResultAdapter(typeMirror))
+                }
+            }
+        } else {
+            InstantQueryResultBinder(findQueryResultAdapter(typeMirror))
+        }
+    }
+
+    private fun findQueryResultAdapter(typeMirror: TypeMirror): QueryResultAdapter? {
         if (typeMirror.kind == TypeKind.DECLARED) {
             val declared = MoreTypes.asDeclared(typeMirror)
             if (declared.typeArguments.isEmpty()) {
@@ -145,7 +181,7 @@
         }
     }
 
-    private fun findRowAdapter(typeMirror: TypeMirror) : RowAdapter? {
+    private fun findRowAdapter(typeMirror: TypeMirror): RowAdapter? {
         if (typeMirror.kind == TypeKind.DECLARED) {
             val declared = MoreTypes.asDeclared(typeMirror)
             if (declared.typeArguments.isNotEmpty()) {
@@ -170,10 +206,10 @@
         }
     }
 
-    fun findQueryParameterAdapter(typeMirror : TypeMirror) : QueryParameterAdapter? {
+    fun findQueryParameterAdapter(typeMirror: TypeMirror): QueryParameterAdapter? {
         if (MoreTypes.isType(typeMirror)
                 && (MoreTypes.isTypeOf(java.util.List::class.java, typeMirror)
-                        || MoreTypes.isTypeOf(java.util.Set::class.java, typeMirror))) {
+                || MoreTypes.isTypeOf(java.util.Set::class.java, typeMirror))) {
             val declared = MoreTypes.asDeclared(typeMirror)
             val converter = findTypeConverter(declared.typeArguments.first(),
                     context.COMMON_TYPES.STRING)
@@ -208,7 +244,8 @@
             val from = prev?.to ?: input
             val candidates = getAllTypeConverters(from, excludes)
             val match = candidates.firstOrNull {
-                outputs.any { output -> types.isSameType(output, it.to) } }
+                outputs.any { output -> types.isSameType(output, it.to) }
+            }
             if (match != null) {
                 return if (prev == null) match else CompositeTypeConverter(prev, match)
             }
@@ -228,7 +265,7 @@
         }
     }
 
-    private fun getAllTypeConverters(input: TypeMirror, excludes : List<TypeMirror>):
+    private fun getAllTypeConverters(input: TypeMirror, excludes: List<TypeMirror>):
             List<TypeConverter> {
         val types = context.processingEnv.typeUtils
         return typeConverters.filter { converter ->
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/InstantQueryResultBinder.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/InstantQueryResultBinder.kt
new file mode 100644
index 0000000..9ab0a3d
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/InstantQueryResultBinder.kt
@@ -0,0 +1,47 @@
+/*
+ * 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 com.android.support.room.solver.query.result
+
+import com.android.support.room.ext.AndroidTypeNames
+import com.android.support.room.ext.L
+import com.android.support.room.ext.N
+import com.android.support.room.ext.T
+import com.android.support.room.solver.CodeGenScope
+import com.android.support.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+
+/**
+ * Instantly runs and returns the query.
+ */
+class InstantQueryResultBinder(adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
+    override fun convertAndReturn(sqlVar: String, argsVar: String, dbField: FieldSpec,
+                                  scope: CodeGenScope) {
+        scope.builder().apply {
+            val outVar = scope.getTmpVar("_result")
+            val cursorVar = scope.getTmpVar("_cursor")
+            addStatement("final $T $L = $N.query($L, $L)", AndroidTypeNames.CURSOR, cursorVar,
+                    DaoWriter.dbField, sqlVar, argsVar)
+            beginControlFlow("try").apply {
+                adapter?.convert(outVar, cursorVar, scope)
+                addStatement("return $L", outVar)
+            }
+            nextControlFlow("finally").apply {
+                addStatement("$L.close()", cursorVar)
+            }
+            endControlFlow()
+        }
+    }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/LiveDataQueryResultBinder.kt
new file mode 100644
index 0000000..acebc75
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -0,0 +1,112 @@
+/*
+ * 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 com.android.support.room.solver.query.result
+
+import com.android.support.room.ext.AndroidTypeNames
+import com.android.support.room.ext.L
+import com.android.support.room.ext.LifecyclesTypeNames
+import com.android.support.room.ext.N
+import com.android.support.room.ext.RoomTypeNames.INVALIDATION_OBSERVER
+import com.android.support.room.ext.T
+import com.android.support.room.ext.typeName
+import com.android.support.room.parser.Table
+import com.android.support.room.solver.CodeGenScope
+import com.android.support.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+import javax.lang.model.type.TypeMirror
+
+/**
+ * Converts the query into a LiveData and returns it. No query is run until necessary.
+ */
+class LiveDataQueryResultBinder(val typeArg: TypeMirror, val tableNames: Set<Table>,
+                                adapter: QueryResultAdapter?)
+    : QueryResultBinder(adapter) {
+    override fun convertAndReturn(sqlVar: String, argsVar: String, dbField: FieldSpec,
+                                  scope: CodeGenScope) {
+        val typeName = typeArg.typeName()
+
+        val liveDataImpl = TypeSpec.anonymousClassBuilder("").apply {
+            superclass(ParameterizedTypeName.get(LifecyclesTypeNames.COMPUTABLE_LIVE_DATA,
+                    typeName))
+            val observerLockField = FieldSpec.builder(TypeName.BOOLEAN,
+                    scope.getTmpVar("_startedObserving"), Modifier.PRIVATE).initializer("false")
+                    .build()
+            addField(observerLockField)
+            addMethod(createComputeMethod(
+                    observerLockField = observerLockField,
+                    typeName = typeName,
+                    sqlVar = sqlVar,
+                    argsVar = argsVar,
+                    dbField = dbField,
+                    scope = scope
+            ))
+        }.build()
+        scope.builder().apply {
+            addStatement("return $L.getLiveData()", liveDataImpl)
+        }
+    }
+
+    private fun createComputeMethod(sqlVar: String, argsVar: String, typeName: TypeName,
+                                    observerLockField : FieldSpec,  dbField: FieldSpec,
+                                    scope: CodeGenScope): MethodSpec {
+        return MethodSpec.methodBuilder("compute").apply {
+            addAnnotation(Override::class.java)
+            addModifiers(Modifier.PROTECTED)
+            returns(typeName)
+            val outVar = scope.getTmpVar("_result")
+            val cursorVar = scope.getTmpVar("_cursor")
+
+            beginControlFlow("if (!$N)", observerLockField).apply {
+                addStatement("$N = true", observerLockField)
+                addStatement("$N.getInvalidationTracker().addWeakObserver($L)",
+                        dbField, createAnonymousObserver())
+            }
+            endControlFlow()
+
+            addStatement("final $T $L = $N.query($L, $L)", AndroidTypeNames.CURSOR, cursorVar,
+                    DaoWriter.dbField, sqlVar, argsVar)
+            beginControlFlow("try").apply {
+                val adapterScope = scope.fork()
+                adapter?.convert(outVar, cursorVar, adapterScope)
+                addCode(adapterScope.builder().build())
+                addStatement("return $L", outVar)
+            }
+            nextControlFlow("finally").apply {
+                addStatement("$L.close()", cursorVar)
+            }
+            endControlFlow()
+        }.build()
+    }
+
+    private fun createAnonymousObserver(): TypeSpec {
+        val tableNamesList = tableNames.joinToString(",") { "\"${it.name}\"" }
+        return TypeSpec.anonymousClassBuilder(tableNamesList).apply {
+            superclass(INVALIDATION_OBSERVER)
+            addMethod(MethodSpec.methodBuilder("onInvalidated").apply {
+                returns(TypeName.VOID)
+                addAnnotation(Override::class.java)
+                addModifiers(Modifier.PUBLIC)
+                addStatement("invalidate()")
+            }.build())
+        }.build()
+    }
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultBinder.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultBinder.kt
new file mode 100644
index 0000000..b280f4a
--- /dev/null
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/query/result/QueryResultBinder.kt
@@ -0,0 +1,36 @@
+/*
+ * 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 com.android.support.room.solver.query.result
+
+import com.android.support.room.solver.CodeGenScope
+import com.squareup.javapoet.FieldSpec
+
+/**
+ * Connects the query, db and the ResultAdapter.
+ * <p>
+ * The default implementation is InstantResultBinder. If the query is deferred rather than executed
+ * directly, such alternative implementations can be implement using this interface (e.g. LiveData,
+ * Rx, caching etc)
+ */
+abstract class QueryResultBinder(val adapter: QueryResultAdapter?) {
+    /**
+     * receives the sql, bind args and adapter and generates the code that runs the query
+     * and returns the result.
+     */
+    abstract fun convertAndReturn(sqlVar : String, argsVar: String,
+                                  dbField : FieldSpec, scope : CodeGenScope)
+}
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt b/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
index aae8112..eef5e9c 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/vo/QueryMethod.kt
@@ -19,6 +19,7 @@
 import com.android.support.room.ext.typeName
 import com.android.support.room.parser.ParsedQuery
 import com.android.support.room.solver.query.result.QueryResultAdapter
+import com.android.support.room.solver.query.result.QueryResultBinder
 import com.squareup.javapoet.TypeName
 import javax.lang.model.element.ExecutableElement
 import javax.lang.model.type.TypeMirror
@@ -29,7 +30,7 @@
  */
 data class QueryMethod(val element: ExecutableElement, val query: ParsedQuery, val name: String,
                        val returnType: TypeMirror, val parameters: List<QueryParameter>,
-                       val resultAdapter : QueryResultAdapter?) {
+                       val queryResultBinder : QueryResultBinder) {
     val sectionToParamMapping by lazy {
         query.bindSections.map {
             if (it.text.trim() == "?") {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/DaoWriter.kt
index 4b80720..3200e1f 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/DaoWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/DaoWriter.kt
@@ -16,7 +16,6 @@
 
 package com.android.support.room.writer
 
-import com.android.support.room.ext.AndroidTypeNames
 import com.android.support.room.ext.L
 import com.android.support.room.ext.N
 import com.android.support.room.ext.RoomTypeNames
@@ -342,18 +341,7 @@
         val sqlVar = scope.getTmpVar("_sql")
         val argsVar = scope.getTmpVar("_args")
         queryWriter.prepareReadAndBind(sqlVar, argsVar, scope)
-        scope.builder().apply {
-            val cursorVar = scope.getTmpVar("_cursor")
-            val outVar = scope.getTmpVar("_result")
-            addStatement("final $T $L = $N.query($L, $L)", AndroidTypeNames.CURSOR, cursorVar,
-                    dbField, sqlVar, argsVar)
-            beginControlFlow("try")
-            method.resultAdapter?.convert(outVar, cursorVar, scope)
-            addStatement("return $L", outVar)
-            nextControlFlow("finally")
-            addStatement("$L.close()", cursorVar)
-            endControlFlow()
-        }
+        method.queryResultBinder.convertAndReturn(sqlVar, argsVar, dbField, scope)
         return scope.builder().build()
     }
 
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/QueryWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/QueryWriter.kt
index 6b54658..d910bac 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/QueryWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/QueryWriter.kt
@@ -85,20 +85,20 @@
                     }
                 }
 
-                addStatement("$T $L = $L.toString()", String::class.typeName(),
+                addStatement("final $T $L = $L.toString()", String::class.typeName(),
                         outSqlQueryName, stringBuilderVar)
                 if (outArgsName != null) {
                     val argCount = scope.getTmpVar("_argCount")
                     addStatement("final $T $L = $L$L", TypeName.INT, argCount, knownQueryArgsCount,
                             listSizeVars.joinToString("") { " + ${it.second}" })
-                    addStatement("$T $L = new String[$L]",
+                    addStatement("final $T $L = new String[$L]",
                             String::class.arrayTypeName(), outArgsName, argCount)
                 }
             } else {
-                addStatement("$T $L = $S", String::class.typeName(),
+                addStatement("final $T $L = $S", String::class.typeName(),
                         outSqlQueryName, queryMethod.query.queryWithReplacedBindParams)
                 if (outArgsName != null) {
-                    addStatement("$T $L = new String[$L]",
+                    addStatement("final $T $L = new String[$L]",
                             String::class.arrayTypeName(), outArgsName, knownQueryArgsCount)
                 }
             }
diff --git a/room/compiler/src/test/data/common/input/ComputableLiveData.java b/room/compiler/src/test/data/common/input/ComputableLiveData.java
new file mode 100644
index 0000000..d661580
--- /dev/null
+++ b/room/compiler/src/test/data/common/input/ComputableLiveData.java
@@ -0,0 +1,9 @@
+//ComputableLiveData interface for tests
+package com.android.support.lifecycle;
+import com.android.support.lifecycle.LiveData;
+public abstract class ComputableLiveData<T> {
+    public ComputableLiveData(){}
+    abstract protected T compute();
+    public LiveData<T> getLiveData() {return null;}
+    public void invalidate() {}
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/data/common/input/LiveData.java b/room/compiler/src/test/data/common/input/LiveData.java
new file mode 100644
index 0000000..93ad2fd
--- /dev/null
+++ b/room/compiler/src/test/data/common/input/LiveData.java
@@ -0,0 +1,4 @@
+//LiveData interface for tests
+package com.android.support.lifecycle;
+public class LiveData<T> {
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
index 2ea5719..eaf9f66 100644
--- a/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/input/ComplexDao.java
@@ -17,6 +17,8 @@
 package foo.bar;
 import com.android.support.room.*;
 import java.util.List;
+import com.android.support.lifecycle.LiveData;
+
 @Dao
 abstract class ComplexDao {
     @Query("SELECT * FROM user where uid = :id")
@@ -36,4 +38,10 @@
 
     @Query("SELECT age FROM user where id = IN(:ids)")
     abstract public List<Integer> getAllAgesAsList(List<Integer> ids);
+
+    @Query("SELECT * FROM user where uid = :id")
+    abstract public LiveData<User> getByIdLive(int id);
+
+    @Query("SELECT * FROM user where uid IN (:ids)")
+    abstract public LiveData<List<User>> loadUsersByIdsLive(int... ids);
 }
diff --git a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
index 29adf7b..e2a1de9 100644
--- a/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/ComplexDao.java
@@ -1,23 +1,10 @@
-/*
- * Copyright (C) 2016 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 foo.bar;
 
 import android.database.Cursor;
+import com.android.support.lifecycle.ComputableLiveData;
+import com.android.support.lifecycle.LiveData;
 import com.android.support.room.CursorConverter;
+import com.android.support.room.InvalidationTracker.Observer;
 import com.android.support.room.Room;
 import com.android.support.room.RoomDatabase;
 import com.android.support.room.util.StringUtil;
@@ -28,7 +15,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-
 public class ComplexDao_Impl extends ComplexDao {
     private final RoomDatabase __db;
 
@@ -38,15 +24,15 @@
 
     @Override
     public User getById(int id) {
-        String _sql = "SELECT * FROM user where uid = ?";
-        String[] _args = new String[1];
+        final String _sql = "SELECT * FROM user where uid = ?";
+        final String[] _args = new String[1];
         int _argIndex = 0;
         _args[_argIndex] = Integer.toString(id);
         final Cursor _cursor = __db.query(_sql, _args);
         try {
             final CursorConverter<User> _converter = Room.getConverter(User.class);
             final User _result;
-            if (_cursor.moveToFirst()) {
+            if(_cursor.moveToFirst()) {
                 _result = _converter.convert(_cursor);
             } else {
                 _result = null;
@@ -59,8 +45,8 @@
 
     @Override
     public User findByName(String name, String lastName) {
-        String _sql = "SELECT * FROM user where name LIKE ? AND lastName LIKE ?";
-        String[] _args = new String[2];
+        final String _sql = "SELECT * FROM user where name LIKE ? AND lastName LIKE ?";
+        final String[] _args = new String[2];
         int _argIndex = 0;
         _args[_argIndex] = name;
         _argIndex = 1;
@@ -69,7 +55,7 @@
         try {
             final CursorConverter<User> _converter = Room.getConverter(User.class);
             final User _result;
-            if (_cursor.moveToFirst()) {
+            if(_cursor.moveToFirst()) {
                 _result = _converter.convert(_cursor);
             } else {
                 _result = null;
@@ -87,19 +73,19 @@
         final int _inputSize = ids.length;
         StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
         _stringBuilder.append(")");
-        String _sql = _stringBuilder.toString();
+        final String _sql = _stringBuilder.toString();
         final int _argCount = 0 + _inputSize;
-        String[] _args = new String[_argCount];
+        final String[] _args = new String[_argCount];
         int _argIndex = 0;
         for (int _item : ids) {
             _args[_argIndex] = Integer.toString(_item);
-            _argIndex++;
+            _argIndex ++;
         }
         final Cursor _cursor = __db.query(_sql, _args);
         try {
             final CursorConverter<User> _converter = Room.getConverter(User.class);
             final List<User> _result = new ArrayList<User>(_cursor.getCount());
-            while (_cursor.moveToNext()) {
+            while(_cursor.moveToNext()) {
                 final User _item_1;
                 _item_1 = _converter.convert(_cursor);
                 _result.add(_item_1);
@@ -112,14 +98,14 @@
 
     @Override
     int getAge(int id) {
-        String _sql = "SELECT age FROM user where id = ?";
-        String[] _args = new String[1];
+        final String _sql = "SELECT age FROM user where id = ?";
+        final String[] _args = new String[1];
         int _argIndex = 0;
         _args[_argIndex] = Integer.toString(id);
         final Cursor _cursor = __db.query(_sql, _args);
         try {
             final int _result;
-            if (_cursor.moveToFirst()) {
+            if(_cursor.moveToFirst()) {
                 _result = _cursor.getInt(0);
             } else {
                 _result = 0;
@@ -137,19 +123,19 @@
         final int _inputSize = ids.length;
         StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
         _stringBuilder.append(")");
-        String _sql = _stringBuilder.toString();
+        final String _sql = _stringBuilder.toString();
         final int _argCount = 0 + _inputSize;
-        String[] _args = new String[_argCount];
+        final String[] _args = new String[_argCount];
         int _argIndex = 0;
         for (int _item : ids) {
             _args[_argIndex] = Integer.toString(_item);
-            _argIndex++;
+            _argIndex ++;
         }
         final Cursor _cursor = __db.query(_sql, _args);
         try {
             final int[] _result = new int[_cursor.getCount()];
             int _index = 0;
-            while (_cursor.moveToNext()) {
+            while(_cursor.moveToNext()) {
                 final int _item_1;
                 _item_1 = _cursor.getInt(0);
                 _result[_index] = _item_1;
@@ -168,18 +154,18 @@
         final int _inputSize = ids.size();
         StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
         _stringBuilder.append(")");
-        String _sql = _stringBuilder.toString();
+        final String _sql = _stringBuilder.toString();
         final int _argCount = 0 + _inputSize;
-        String[] _args = new String[_argCount];
+        final String[] _args = new String[_argCount];
         int _argIndex = 0;
         for (Integer _item : ids) {
             _args[_argIndex] = _item == null ? null : Integer.toString(_item);
-            _argIndex++;
+            _argIndex ++;
         }
         final Cursor _cursor = __db.query(_sql, _args);
         try {
             final List<Integer> _result = new ArrayList<Integer>(_cursor.getCount());
-            while (_cursor.moveToNext()) {
+            while(_cursor.moveToNext()) {
                 final Integer _item_1;
                 if (_cursor.isNull(0)) {
                     _item_1 = null;
@@ -193,4 +179,87 @@
             _cursor.close();
         }
     }
+
+    @Override
+    public LiveData<User> getByIdLive(int id) {
+        final String _sql = "SELECT * FROM user where uid = ?";
+        final String[] _args = new String[1];
+        int _argIndex = 0;
+        _args[_argIndex] = Integer.toString(id);
+        return new ComputableLiveData<User>() {
+            private boolean _startedObserving = false;
+
+            @Override
+            protected User compute() {
+                if (!_startedObserving) {
+                    _startedObserving = true;
+                    __db.getInvalidationTracker().addWeakObserver(new Observer("user") {
+                        @Override
+                        public void onInvalidated() {
+                            invalidate();
+                        }
+                    });
+                }
+                final Cursor _cursor = __db.query(_sql, _args);
+                try {
+                    final CursorConverter<User> _converter = Room.getConverter(User.class);
+                    final User _result;
+                    if(_cursor.moveToFirst()) {
+                        _result = _converter.convert(_cursor);
+                    } else {
+                        _result = null;
+                    }
+                    return _result;
+                } finally {
+                    _cursor.close();
+                }
+            }
+        }.getLiveData();
+    }
+
+    @Override
+    public LiveData<List<User>> loadUsersByIdsLive(int... ids) {
+        StringBuilder _stringBuilder = StringUtil.newStringBuilder();
+        _stringBuilder.append("SELECT * FROM user where uid IN (");
+        final int _inputSize = ids.length;
+        StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
+        _stringBuilder.append(")");
+        final String _sql = _stringBuilder.toString();
+        final int _argCount = 0 + _inputSize;
+        final String[] _args = new String[_argCount];
+        int _argIndex = 0;
+        for (int _item : ids) {
+            _args[_argIndex] = Integer.toString(_item);
+            _argIndex ++;
+        }
+        return new ComputableLiveData<List<User>>() {
+            private boolean _startedObserving = false;
+
+            @Override
+            protected List<User> compute() {
+                if (!_startedObserving) {
+                    _startedObserving = true;
+                    __db.getInvalidationTracker().addWeakObserver(new Observer("user") {
+                        @Override
+                        public void onInvalidated() {
+                            invalidate();
+                        }
+                    });
+                }
+                final Cursor _cursor = __db.query(_sql, _args);
+                try {
+                    final CursorConverter<User> _converter = Room.getConverter(User.class);
+                    final List<User> _result = new ArrayList<User>(_cursor.getCount());
+                    while(_cursor.moveToNext()) {
+                        final User _item_1;
+                        _item_1 = _converter.convert(_cursor);
+                        _result.add(_item_1);
+                    }
+                    return _result;
+                } finally {
+                    _cursor.close();
+                }
+            }
+        }.getLiveData();
+    }
 }
diff --git a/room/compiler/src/test/data/daoWriter/output/DeletionDao.java b/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
index ce5316b..11bff9b 100644
--- a/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/DeletionDao.java
@@ -55,7 +55,7 @@
     this._preparedStmtOfDeleteByUid = new SharedSQLiteStatement(__db) {
       @Override
       public String createQuery() {
-        String _query = "DELETE FROM user where uid = ?";
+        final String _query = "DELETE FROM user where uid = ?";
         return _query;
       }
     };
@@ -171,7 +171,7 @@
     final int _inputSize = uid.length;
     StringUtil.appendPlaceholders(_stringBuilder, _inputSize);
     _stringBuilder.append(")");
-    String _sql = _stringBuilder.toString();
+    final String _sql = _stringBuilder.toString();
     SupportSQLiteStatement _stmt = __db.compileStatement(_sql);
     int _argIndex = 1;
     for (int _item : uid) {
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryMethodProcessorTest.kt
index 556bd41..3943e46 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/QueryMethodProcessorTest.kt
@@ -16,10 +16,13 @@
 
 package com.android.support.room.processor
 
+import COMMON
 import com.android.support.room.Dao
 import com.android.support.room.Query
+import com.android.support.room.ext.LifecyclesTypeNames
 import com.android.support.room.ext.hasAnnotation
 import com.android.support.room.ext.typeName
+import com.android.support.room.solver.query.result.LiveDataQueryResultBinder
 import com.android.support.room.testing.TestInvocation
 import com.android.support.room.testing.TestProcessor
 import com.android.support.room.vo.QueryMethod
@@ -28,13 +31,14 @@
 import com.google.common.truth.Truth.assertAbout
 import com.google.testing.compile.CompileTester
 import com.google.testing.compile.JavaFileObjects
-import com.google.testing.compile.JavaSourceSubjectFactory
+import com.google.testing.compile.JavaSourcesSubjectFactory
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
 import com.squareup.javapoet.TypeVariableName
 import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.instanceOf
 import org.hamcrest.CoreMatchers.notNullValue
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Test
@@ -222,7 +226,7 @@
                 @Query("select * from users where id = :_blah")
                 abstract public long getSth(int _blah);
                 """
-        ){parsedQuery, invocation -> }
+        ) { parsedQuery, invocation -> }
                 .failsToCompile()
                 .withErrorContaining(ProcessorErrors.QUERY_PARAMETERS_CANNOT_START_WITH_UNDERSCORE)
     }
@@ -341,13 +345,41 @@
         }.compilesWithoutError()
     }
 
+    @Test
+    fun testLiveDataQuery() {
+        singleQueryMethod(
+                """
+                @Query("select name from user where id = :id")
+                abstract ${LifecyclesTypeNames.LIVE_DATA}<String> nameLiveData(String id);
+                """
+        ) { parsedQuery, invocation ->
+            assertThat(parsedQuery.returnType.typeName(),
+                    `is`(ParameterizedTypeName.get(LifecyclesTypeNames.LIVE_DATA,
+                            String::class.typeName()) as TypeName))
+            assertThat(parsedQuery.queryResultBinder,
+                    instanceOf(LiveDataQueryResultBinder::class.java))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun testNonSelectLiveData() {
+        singleQueryMethod(
+                """
+                @Query("delete from user where id = :id")
+                abstract ${LifecyclesTypeNames.LIVE_DATA}<Integer> deleteLiveData(String id);
+                """
+        ) { parsedQuery, invocation ->
+        }.failsToCompile()
+                .withErrorContaining(ProcessorErrors.DELETION_METHODS_MUST_RETURN_VOID_OR_INT)
+    }
+
     fun singleQueryMethod(vararg input: String,
                           handler: (QueryMethod, TestInvocation) -> Unit):
             CompileTester {
-        return assertAbout(JavaSourceSubjectFactory.javaSource())
-                .that(JavaFileObjects.forSourceString("foo.bar.MyClass",
+        return assertAbout(JavaSourcesSubjectFactory.javaSources())
+                .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyClass",
                         DAO_PREFIX + input.joinToString("\n") + DAO_SUFFIX
-                ))
+                ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA))
                 .processedWith(TestProcessor.builder()
                         .forAnnotations(Query::class, Dao::class)
                         .nextRunHandler { invocation ->
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/solver/query/QueryWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/solver/query/QueryWriterTest.kt
index 4a2f4a2..2179791 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/solver/query/QueryWriterTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/solver/query/QueryWriterTest.kt
@@ -58,8 +58,8 @@
             writer.prepareReadAndBind("_sql", "_args", scope)
             assertThat(scope.generate().trim(), `is`(
                     """
-                    java.lang.String _sql = "SELECT id FROM users";
-                    java.lang.String[] _args = new String[0];
+                    final java.lang.String _sql = "SELECT id FROM users";
+                    final java.lang.String[] _args = new String[0];
                     """.trimIndent()))
         }.compilesWithoutError()
     }
@@ -74,8 +74,8 @@
             writer.prepareReadAndBind("_sql", "_args", scope)
             assertThat(scope.generate().trim(), `is`(
                     """
-                    java.lang.String _sql = "SELECT id FROM users WHERE name LIKE ?";
-                    java.lang.String[] _args = new String[1];
+                    final java.lang.String _sql = "SELECT id FROM users WHERE name LIKE ?";
+                    final java.lang.String[] _args = new String[1];
                     int _argIndex = 0;
                     _args[_argIndex] = name;
                     """.trimIndent()))
@@ -92,8 +92,8 @@
             writer.prepareReadAndBind("_sql", "_args", scope)
             assertThat(scope.generate().trim(), `is`(
                     """
-                    java.lang.String _sql = "SELECT id FROM users WHERE id IN(?,?)";
-                    java.lang.String[] _args = new String[2];
+                    final java.lang.String _sql = "SELECT id FROM users WHERE id IN(?,?)";
+                    final java.lang.String[] _args = new String[2];
                     int _argIndex = 0;
                     _args[_argIndex] = java.lang.Integer.toString(id1);
                     _argIndex = 1;
@@ -118,9 +118,9 @@
                     $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
                     _stringBuilder.append(") AND age > ");
                     _stringBuilder.append("?");
-                    java.lang.String _sql = _stringBuilder.toString();
+                    final java.lang.String _sql = _stringBuilder.toString();
                     final int _argCount = 1 + _inputSize;
-                    java.lang.String[] _args = new String[_argCount];
+                    final java.lang.String[] _args = new String[_argCount];
                     int _argIndex = 0;
                     for (int _item : ids) {
                       _args[_argIndex] = java.lang.Integer.toString(_item);
@@ -139,9 +139,9 @@
                     $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
                     _stringBuilder.append(") AND age > ");
                     _stringBuilder.append("?");
-                    java.lang.String _sql = _stringBuilder.toString();
+                    final java.lang.String _sql = _stringBuilder.toString();
                     final int _argCount = 1 + _inputSize;
-                    java.lang.String[] _args = new String[_argCount];
+                    final java.lang.String[] _args = new String[_argCount];
                     int _argIndex = 0;
                     for (java.lang.Integer _item : ids) {
                       _args[_argIndex] = _item == null ? null : java.lang.Integer.toString(_item);
@@ -184,8 +184,8 @@
             val scope = CodeGenScope()
             writer.prepareReadAndBind("_sql", "_args", scope)
             assertThat(scope.generate().trim(), `is`("""
-                    java.lang.String _sql = "SELECT id FROM users WHERE age > ? OR bage > ?";
-                    java.lang.String[] _args = new String[2];
+                    final java.lang.String _sql = "SELECT id FROM users WHERE age > ? OR bage > ?";
+                    final java.lang.String[] _args = new String[2];
                     int _argIndex = 0;
                     _args[_argIndex] = java.lang.Integer.toString(age);
                     _argIndex = 1;
@@ -212,9 +212,9 @@
                     final int _inputSize = ages.length;
                     $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize);
                     _stringBuilder.append(")");
-                    java.lang.String _sql = _stringBuilder.toString();
+                    final java.lang.String _sql = _stringBuilder.toString();
                     final int _argCount = 2 + _inputSize;
-                    java.lang.String[] _args = new String[_argCount];
+                    final java.lang.String[] _args = new String[_argCount];
                     int _argIndex = 0;
                     _args[_argIndex] = java.lang.Integer.toString(age);
                     _argIndex = 1;
@@ -247,9 +247,9 @@
                     final int _inputSize_1 = ages.length;
                     $STRING_UTIL.appendPlaceholders(_stringBuilder, _inputSize_1);
                     _stringBuilder.append(")");
-                    java.lang.String _sql = _stringBuilder.toString();
+                    final java.lang.String _sql = _stringBuilder.toString();
                     final int _argCount = 1 + _inputSize + _inputSize_1;
-                    java.lang.String[] _args = new String[_argCount];
+                    final java.lang.String[] _args = new String[_argCount];
                     int _argIndex = 0;
                     for (int _item : ages) {
                       _args[_argIndex] = java.lang.Integer.toString(_item);
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/testing/test_util.kt b/room/compiler/src/test/kotlin/com/android/support/room/testing/test_util.kt
index 77755b4..5e25b90 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/testing/test_util.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/testing/test_util.kt
@@ -15,6 +15,8 @@
  */
 
 import com.android.support.room.Query
+import com.android.support.room.ext.LifecyclesTypeNames
+import com.android.support.room.ext.RoomTypeNames
 import com.android.support.room.testing.TestInvocation
 import com.android.support.room.testing.TestProcessor
 import com.google.common.truth.Truth
@@ -41,6 +43,13 @@
     val MULTI_PKEY_ENTITY by lazy {
         loadJavaCode("common/input/MultiPKeyEntity.java", "MultiPKeyEntity")
     }
+    val LIVE_DATA by lazy {
+        loadJavaCode("common/input/LiveData.java", LifecyclesTypeNames.LIVE_DATA.toString())
+    }
+    val COMPUTABLE_LIVE_DATA by lazy {
+        loadJavaCode("common/input/ComputableLiveData.java",
+                LifecyclesTypeNames.COMPUTABLE_LIVE_DATA.toString())
+    }
 }
 fun simpleRun(f: (TestInvocation) -> Unit): CompileTester {
     return Truth.assertAbout(JavaSourceSubjectFactory.javaSource())
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/writer/DaoWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/writer/DaoWriterTest.kt
index b596d4b..9821688 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/writer/DaoWriterTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/writer/DaoWriterTest.kt
@@ -59,7 +59,8 @@
 
     fun singleDao(vararg jfo : JavaFileObject): CompileTester {
         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
-                .that(jfo.toList() + COMMON.USER + COMMON.MULTI_PKEY_ENTITY)
+                .that(jfo.toList() + COMMON.USER + COMMON.MULTI_PKEY_ENTITY +
+                        COMMON.LIVE_DATA + COMMON.COMPUTABLE_LIVE_DATA)
                 .processedWith(TestProcessor.builder()
                         .forAnnotations(com.android.support.room.Dao::class)
                         .nextRunHandler { invocation ->
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/writer/DatabaseWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/writer/DatabaseWriterTest.kt
index c2fce0f..0ee6073 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/writer/DatabaseWriterTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/writer/DatabaseWriterTest.kt
@@ -45,7 +45,7 @@
 
     private fun singleDb(vararg jfo : JavaFileObject): CompileTester {
         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
-                .that(jfo.toList() + COMMON.USER)
+                .that(jfo.toList() + COMMON.USER +  COMMON.LIVE_DATA + COMMON.COMPUTABLE_LIVE_DATA)
                 .processedWith(RoomProcessor())
     }
 }
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index e096049..22b9a16 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -51,6 +51,10 @@
     androidTestCompile("com.android.support.test.espresso:espresso-core:$espresso_version", {
         exclude group: 'com.android.support', module: 'support-annotations'
     })
+    // IJ's gradle integration just cannot figure this out ...
+    androidTestCompile project(':lifecycle:extensions')
+    androidTestCompile project(':lifecycle:common')
+    androidTestCompile project(':lifecycle:runtime')
     testCompile "junit:junit:$junit_version"
     testCompile "org.mockito:mockito-core:$mockito_version"
 
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/dao/UserDao.java
index 13051a4..ac752c6 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/dao/UserDao.java
@@ -16,6 +16,7 @@
 
 package com.android.support.room.integration.testapp.dao;
 
+import com.android.support.lifecycle.LiveData;
 import com.android.support.room.Dao;
 import com.android.support.room.Delete;
 import com.android.support.room.Insert;
@@ -70,4 +71,10 @@
 
     @Query("select mId from user order by mId ASC")
     List<Integer> loadIds();
+
+    @Query("select * from user where mId = :id")
+    LiveData<User> liveUserById(int id);
+
+    @Query("select * from user where mName LIKE '%' || :name || '%' ORDER BY mId DESC")
+    LiveData<List<User>> liveUsersListByName(String name);
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/InvalidationTest.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/InvalidationTest.java
index 859858a..6aa0168 100644
--- a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/InvalidationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/InvalidationTest.java
@@ -20,21 +20,17 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import android.content.Context;
-import android.os.Handler;
-import android.os.Looper;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
 import com.android.support.executors.AppToolkitTaskExecutor;
-import com.android.support.executors.TaskExecutor;
 import com.android.support.room.InvalidationTracker;
 import com.android.support.room.Room;
 import com.android.support.room.integration.testapp.TestDatabase;
 import com.android.support.room.integration.testapp.dao.UserDao;
 import com.android.support.room.integration.testapp.vo.User;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,8 +38,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
 
@@ -63,33 +57,6 @@
         mUserDao = mDb.getUserDao();
     }
 
-    @Before
-    public void swapExecutorDelegate() {
-        final ExecutorService ioService = Executors.newSingleThreadExecutor();
-        final Handler mainHandler = new Handler(Looper.getMainLooper());
-        AppToolkitTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
-            @Override
-            public void executeOnDiskIO(Runnable runnable) {
-                ioService.execute(runnable);
-            }
-
-            @Override
-            public void executeOnMainThread(Runnable runnable) {
-                mainHandler.post(runnable);
-            }
-
-            @Override
-            public boolean isMainThread() {
-                return Looper.getMainLooper().getThread() == Thread.currentThread();
-            }
-        });
-    }
-
-    @After
-    public void removeExecutorDelegate() {
-        AppToolkitTaskExecutor.getInstance().setDelegate(null);
-    }
-
     private void waitUntilIOThreadIsIdle() {
         FutureTask<Void> future = new FutureTask<>(new Callable<Void>() {
             @Override
@@ -162,7 +129,7 @@
         }
 
         @Override
-        protected void onInvalidated() {
+        public void onInvalidated() {
             mLatch.countDown();
         }
     }
diff --git a/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/LiveDataQueryTest.java b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/LiveDataQueryTest.java
new file mode 100644
index 0000000..144e02e
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/com/android/support/room/integration/testapp/test/LiveDataQueryTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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 com.android.support.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.support.executors.AppToolkitTaskExecutor;
+import com.android.support.lifecycle.Lifecycle;
+import com.android.support.lifecycle.LifecycleProvider;
+import com.android.support.lifecycle.LifecycleRegistry;
+import com.android.support.lifecycle.LiveData;
+import com.android.support.lifecycle.Observer;
+import com.android.support.room.Room;
+import com.android.support.room.integration.testapp.TestDatabase;
+import com.android.support.room.integration.testapp.dao.UserDao;
+import com.android.support.room.integration.testapp.vo.User;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests invalidation tracking.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LiveDataQueryTest {
+    private UserDao mUserDao;
+    private TestDatabase mDb;
+
+    @Before
+    public void createDb() {
+        Context context = InstrumentationRegistry.getTargetContext();
+        mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
+        mUserDao = mDb.getUserDao();
+    }
+
+    @Test
+    public void observeById() throws InterruptedException, ExecutionException {
+        final LiveData<User> userLiveData = mUserDao.liveUserById(5);
+        final TestLifecycleProvider testProvider = new TestLifecycleProvider();
+        testProvider.handleEvent(Lifecycle.ON_CREATE);
+        final LatchObserver<User> observer = new LatchObserver<>();
+        observe(userLiveData, testProvider, observer);
+
+        observer.assertNoUpdate();
+
+        observer.reset();
+        testProvider.handleEvent(Lifecycle.ON_START);
+        assertThat(observer.get(), is(nullValue()));
+
+        // another id
+        observer.reset();
+        mUserDao.insert(TestUtil.createUser(7));
+        assertThat(observer.get(), is(nullValue()));
+
+        observer.reset();
+        final User u5 = TestUtil.createUser(5);
+        mUserDao.insert(u5);
+        assertThat(observer.get(), is(notNullValue()));
+
+        u5.setName("foo-foo-foo");
+        observer.reset();
+        mUserDao.insertOrReplace(u5);
+        final User updated = observer.get();
+        assertThat(updated, is(notNullValue()));
+        assertThat(updated.getName(), is("foo-foo-foo"));
+
+        testProvider.handleEvent(Lifecycle.ON_STOP);
+        observer.reset();
+        u5.setName("baba");
+        mUserDao.insertOrReplace(u5);
+        observer.assertNoUpdate();
+    }
+
+    @Test
+    public void observeListQuery() throws InterruptedException, ExecutionException {
+        final LiveData<List<User>> userLiveData = mUserDao.liveUsersListByName("frida");
+        final TestLifecycleProvider lifecycleProvider = new TestLifecycleProvider();
+        lifecycleProvider.handleEvent(Lifecycle.ON_START);
+        final LatchObserver<List<User>> observer = new LatchObserver<>();
+        observe(userLiveData, lifecycleProvider, observer);
+        assertThat(observer.get(), is(Collections.<User>emptyList()));
+
+        observer.reset();
+        final User user1 = TestUtil.createUser(3);
+        user1.setName("dog frida");
+        mUserDao.insert(user1);
+        assertThat(observer.get(), is(Collections.singletonList(user1)));
+
+
+        observer.reset();
+        final User user2 = TestUtil.createUser(5);
+        user2.setName("does not match");
+        mUserDao.insert(user2);
+        assertThat(observer.get(), is(Collections.singletonList(user1)));
+
+        observer.reset();
+        user1.setName("i don't match either");
+        mUserDao.insertOrReplace(user1);
+        assertThat(observer.get(), is(Collections.<User>emptyList()));
+
+        lifecycleProvider.handleEvent(Lifecycle.ON_STOP);
+
+        observer.reset();
+        final User user3 = TestUtil.createUser(9);
+        user3.setName("painter frida");
+        mUserDao.insertOrReplace(user3);
+        observer.assertNoUpdate();
+
+        observer.reset();
+        final User user4 = TestUtil.createUser(11);
+        user4.setName("friday");
+        mUserDao.insertOrReplace(user4);
+        observer.assertNoUpdate();
+
+        lifecycleProvider.handleEvent(Lifecycle.ON_START);
+        assertThat(observer.get(), is(Arrays.asList(user4, user3)));
+    }
+
+    private void observe(final LiveData liveData, final LifecycleProvider provider,
+            final Observer observer) throws ExecutionException, InterruptedException {
+        FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                //noinspection unchecked
+                liveData.observe(provider, observer);
+                return null;
+            }
+        });
+        AppToolkitTaskExecutor.getInstance().executeOnMainThread(futureTask);
+        futureTask.get();
+    }
+
+    static class TestLifecycleProvider implements LifecycleProvider {
+        private LifecycleRegistry mLifecycle;
+
+        TestLifecycleProvider() {
+            mLifecycle = new LifecycleRegistry(this);
+        }
+
+        @Override
+        public Lifecycle getLifecycle() {
+            return mLifecycle;
+        }
+
+        void handleEvent(@Lifecycle.Event int event) {
+            mLifecycle.handleLifecycleEvent(event);
+        }
+    }
+
+    private class LatchObserver<T> implements Observer<T> {
+        static final int TIMEOUT = 3;
+        T mLastData;
+        CountDownLatch mSetLatch = new CountDownLatch(1);
+
+        void reset() {
+            mSetLatch = new CountDownLatch(1);
+        }
+        @Override
+        public void onChanged(@Nullable T o) {
+            mLastData = o;
+            mSetLatch.countDown();
+        }
+
+        void assertNoUpdate() throws InterruptedException {
+            assertThat(mSetLatch.await(TIMEOUT, TimeUnit.SECONDS),
+                    is(false));
+        }
+
+        T get() throws InterruptedException {
+            assertThat(mSetLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true));
+            return mLastData;
+        }
+    }
+}
diff --git a/room/runtime/src/main/java/com/android/support/room/InvalidationTracker.java b/room/runtime/src/main/java/com/android/support/room/InvalidationTracker.java
index 8baf4d1..49b2605 100644
--- a/room/runtime/src/main/java/com/android/support/room/InvalidationTracker.java
+++ b/room/runtime/src/main/java/com/android/support/room/InvalidationTracker.java
@@ -19,6 +19,7 @@
 import android.database.Cursor;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
 import android.support.annotation.VisibleForTesting;
 import android.support.v4.util.ArrayMap;
 import android.util.Log;
@@ -28,6 +29,7 @@
 import com.android.support.db.SupportSQLiteStatement;
 import com.android.support.executors.AppToolkitTaskExecutor;
 
+import java.lang.ref.WeakReference;
 import java.util.Arrays;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -84,6 +86,7 @@
     @VisibleForTesting
     ArrayMap<String, Integer> mTableIdLookup;
     private String[] mTableNames;
+
     @NonNull
     @VisibleForTesting
     long[] mTableVersions;
@@ -104,7 +107,7 @@
     private ObservedTableTracker mObservedTableTracker;
 
     @VisibleForTesting
-    ObserverSet<ObserverWrapper> mObserverSet;
+    SyncObserverSet<ObserverWrapper> mObserverSet;
 
     private ObserverSet.Callback<ObserverWrapper> mInvalidCheck =
             new ObserverSet.Callback<ObserverWrapper>() {
@@ -246,6 +249,21 @@
     }
 
     /**
+     * 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)
+    public void addWeakObserver(Observer observer) {
+        addObserver(new WeakObserver(this, observer));
+    }
+
+    /**
      * Removes the observer from observers list.
      *
      * @param observer The observer to remove.
@@ -438,7 +456,10 @@
             mTables = Arrays.copyOf(tables, tables.length);
         }
 
-        protected abstract void onInvalidated();
+        /**
+         * Called when one of the observed tables is invalidated in the database.
+         */
+        public abstract void onInvalidated();
     }
 
 
@@ -554,6 +575,31 @@
     }
 
     /**
+     * An Observer wrapper that keeps a weak reference to the given object.
+     * <p>
+     * This class with 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() {
+            final Observer observer = mDelegateRef.get();
+            if (observer == null) {
+                mTracker.removeObserver(this);
+            } else {
+                observer.onInvalidated();
+            }
+        }
+    }
+
+    /**
      * Poor man's sync on observer set.
      * <p>
      * When we revisit observer set, we should consider making it thread safe.
diff --git a/room/runtime/src/test/java/com/android/support/room/InvalidationTrackerTest.java b/room/runtime/src/test/java/com/android/support/room/InvalidationTrackerTest.java
index 30e3ac5..3a3e52b 100644
--- a/room/runtime/src/test/java/com/android/support/room/InvalidationTrackerTest.java
+++ b/room/runtime/src/test/java/com/android/support/room/InvalidationTrackerTest.java
@@ -17,18 +17,14 @@
 package com.android.support.room;
 
 import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -37,11 +33,10 @@
 import com.android.support.db.SupportSQLiteDatabase;
 import com.android.support.db.SupportSQLiteOpenHelper;
 import com.android.support.db.SupportSQLiteStatement;
-import com.android.support.executors.AppToolkitTaskExecutor;
-import com.android.support.executors.TaskExecutor;
+import com.android.support.room.testutil.JunitTaskExecutorRule;
 
-import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -56,48 +51,27 @@
 @RunWith(JUnit4.class)
 public class InvalidationTrackerTest {
     private InvalidationTracker mTracker;
-    private TaskExecutor mTaskExecutor;
     private RoomDatabase mRoomDatabase;
+    @Rule
+    public JunitTaskExecutorRule mTaskExecutorRule = new JunitTaskExecutorRule(1,
+            true);
 
     @Before
     public void setup() {
         mRoomDatabase = mock(RoomDatabase.class);
         SupportSQLiteDatabase sqliteDb = mock(SupportSQLiteDatabase.class);
-        when(sqliteDb.compileStatement(eq(InvalidationTracker.CLEANUP_SQL))).thenReturn(
-                mock(SupportSQLiteStatement.class));
+        final SupportSQLiteStatement statement = mock(SupportSQLiteStatement.class);
         SupportSQLiteOpenHelper openHelper = mock(SupportSQLiteOpenHelper.class);
-        when(openHelper.getWritableDatabase()).thenReturn(sqliteDb);
-        when(mRoomDatabase.getOpenHelper()).thenReturn(openHelper);
+
+        doReturn(statement).when(sqliteDb).compileStatement(eq(InvalidationTracker.CLEANUP_SQL));
+        doReturn(sqliteDb).when(openHelper).getWritableDatabase();
+        //noinspection ResultOfMethodCallIgnored
+        doReturn(openHelper).when(mRoomDatabase).getOpenHelper();
+
         mTracker = new InvalidationTracker(mRoomDatabase, "a", "B");
         mTracker.internalInit(sqliteDb);
     }
 
-    @Before
-    public void swapExecutorDelegate() {
-        mTaskExecutor = spy(new TaskExecutor() {
-            @Override
-            public void executeOnDiskIO(Runnable runnable) {
-                runnable.run();
-            }
-
-            @Override
-            public void executeOnMainThread(Runnable runnable) {
-                runnable.run();
-            }
-
-            @Override
-            public boolean isMainThread() {
-                return true;
-            }
-        });
-        AppToolkitTaskExecutor.getInstance().setDelegate(mTaskExecutor);
-    }
-
-    @After
-    public void removeExecutorDelegate() {
-        AppToolkitTaskExecutor.getInstance().setDelegate(null);
-    }
-
     @Test
     public void tableIds() {
         assertThat(mTracker.mTableIdLookup.get("a"), is(0));
@@ -108,13 +82,20 @@
     public void addRemoveObserver() throws Exception {
         InvalidationTracker.Observer observer = new LatchObserver(1, "a");
         mTracker.addObserver(observer);
+        drainTasks();
         assertThat(mTracker.mObserverSet.size(), is(1));
         mTracker.removeObserver(new LatchObserver(1, "a"));
+        drainTasks();
         assertThat(mTracker.mObserverSet.size(), is(1));
         mTracker.removeObserver(observer);
+        drainTasks();
         assertThat(mTracker.mObserverSet.size(), is(0));
     }
 
+    private void drainTasks() throws InterruptedException {
+        mTaskExecutorRule.drainTasks(200);
+    }
+
     @Test(expected = IllegalArgumentException.class)
     public void badObserver() {
         InvalidationTracker.Observer observer = new LatchObserver(1, "x");
@@ -123,43 +104,39 @@
 
     @Test
     public void refreshReadValues() throws Exception {
-        doAnswer(new Answer() {
-            @Override
-            public Object answer(InvocationOnMock invocation) throws Throwable {
-                ((Runnable) invocation.getArguments()[0]).run();
-                return nullValue();
-            }
-        }).when(mTaskExecutor).executeOnDiskIO(any(Runnable.class));
-
         setVersions(1, 0, 2, 1);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(mTracker.mTableVersions, is(new long[]{1, 2}));
 
         setVersions(3, 1);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(mTracker.mTableVersions, is(new long[]{1, 3}));
 
         setVersions(7, 0);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(mTracker.mTableVersions, is(new long[]{7, 3}));
 
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(mTracker.mTableVersions, is(new long[]{7, 3}));
     }
 
+    private void refreshSync() throws InterruptedException {
+        mTracker.refreshVersionsAsync();
+        drainTasks();
+    }
+
     @Test
     public void refreshCheckTasks() throws Exception {
         when(mRoomDatabase.query(anyString(), any(String[].class)))
                 .thenReturn(mock(Cursor.class));
-        doNothing().when(mTaskExecutor).executeOnDiskIO(any(Runnable.class));
         mTracker.refreshVersionsAsync();
         mTracker.refreshVersionsAsync();
-        verify(mTaskExecutor).executeOnDiskIO(mTracker.mRefreshRunnable);
-        mTracker.mRefreshRunnable.run();
+        verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable);
+        drainTasks();
 
-        reset(mTaskExecutor);
+        reset(mTaskExecutorRule.getTaskExecutor());
         mTracker.refreshVersionsAsync();
-        verify(mTaskExecutor).executeOnDiskIO(mTracker.mRefreshRunnable);
+        verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable);
     }
 
     @Test
@@ -167,16 +144,16 @@
         LatchObserver observer = new LatchObserver(1, "a");
         mTracker.addObserver(observer);
         setVersions(1, 0, 2, 1);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(observer.await(), is(true));
 
         setVersions(3, 1);
         observer.reset(1);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(observer.await(), is(false));
 
         setVersions(4, 0);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(observer.await(), is(true));
     }
 
@@ -185,28 +162,31 @@
         LatchObserver observer = new LatchObserver(1, "A", "B");
         mTracker.addObserver(observer);
         setVersions(1, 0, 2, 1);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(observer.await(), is(true));
 
         setVersions(3, 1);
         observer.reset(1);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(observer.await(), is(true));
 
         setVersions(4, 0);
         observer.reset(1);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(observer.await(), is(true));
 
         observer.reset(1);
-        mTracker.refreshVersionsAsync();
+        refreshSync();
         assertThat(observer.await(), is(false));
     }
 
     /**
      * Key value pairs of VERSION, TABLE_ID
      */
-    private void setVersions(int... keyValuePairs) {
+    private void setVersions(int... keyValuePairs) throws InterruptedException {
+        // mockito does not like multi-threaded access so before setting versions, make sure we
+        // sync background tasks.
+        drainTasks();
         Cursor cursor = createCursorWithValues(keyValuePairs);
         doReturn(cursor).when(mRoomDatabase).query(
                 Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL),
@@ -247,7 +227,7 @@
         }
 
         @Override
-        protected void onInvalidated() {
+        public void onInvalidated() {
             mLatch.countDown();
         }
 
diff --git a/room/runtime/src/test/java/com/android/support/room/testutil/JunitTaskExecutorRule.java b/room/runtime/src/test/java/com/android/support/room/testutil/JunitTaskExecutorRule.java
new file mode 100644
index 0000000..def85e7
--- /dev/null
+++ b/room/runtime/src/test/java/com/android/support/room/testutil/JunitTaskExecutorRule.java
@@ -0,0 +1,86 @@
+/*
+ * 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 com.android.support.room.testutil;
+
+import com.android.support.executors.AppToolkitTaskExecutor;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.MultipleFailureException;
+import org.junit.runners.model.Statement;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+/**
+ * A JUnit rule that swaps the task executor with a more controllable one.
+ * Once we have the TaskExecutor API, we should consider making this public (via some test package).
+ */
+public class JunitTaskExecutorRule implements TestRule {
+    private final TaskExecutorWIthFakeMainThread mTaskExecutor;
+
+    public JunitTaskExecutorRule(int ioThreadCount, boolean spyOnExecutor) {
+        if (spyOnExecutor) {
+            mTaskExecutor = Mockito.spy(new TaskExecutorWIthFakeMainThread(ioThreadCount));
+        } else {
+            mTaskExecutor = new TaskExecutorWIthFakeMainThread(ioThreadCount);
+        }
+
+    }
+
+    private void beforeStart() {
+        AppToolkitTaskExecutor.getInstance().setDelegate(mTaskExecutor);
+    }
+
+    private void afterFinished() {
+        AppToolkitTaskExecutor.getInstance().setDelegate(null);
+    }
+
+    public TaskExecutorWIthFakeMainThread getTaskExecutor() {
+        return mTaskExecutor;
+    }
+
+    public void drainTasks(int seconds) throws InterruptedException {
+        mTaskExecutor.drainTasks(seconds);
+    }
+
+    @Override
+    public Statement apply(final Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                beforeStart();
+                try {
+                    base.evaluate();
+                    finishExecutors();
+                } catch (Throwable t) {
+                    throw new RuntimeException(t);
+                } finally {
+                    afterFinished();
+                }
+            }
+        };
+    }
+
+    private void finishExecutors() throws InterruptedException, MultipleFailureException {
+        mTaskExecutor.shutdown(10);
+        final List<Throwable> errors = mTaskExecutor.getErrors();
+        if (!errors.isEmpty()) {
+            throw new MultipleFailureException(errors);
+        }
+    }
+}
diff --git a/room/runtime/src/test/java/com/android/support/room/testutil/TaskExecutorWIthFakeMainThread.java b/room/runtime/src/test/java/com/android/support/room/testutil/TaskExecutorWIthFakeMainThread.java
new file mode 100644
index 0000000..3a985ea
--- /dev/null
+++ b/room/runtime/src/test/java/com/android/support/room/testutil/TaskExecutorWIthFakeMainThread.java
@@ -0,0 +1,140 @@
+/*
+ * 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 com.android.support.room.testutil;
+
+import android.support.annotation.NonNull;
+
+import com.android.support.executors.TaskExecutor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A TaskExecutor that has a real thread for main thread operations and can wait for execution etc.
+ */
+public class TaskExecutorWIthFakeMainThread extends TaskExecutor {
+    private List<Throwable> mCaughtExceptions = Collections.synchronizedList(new ArrayList
+            <Throwable>());
+
+    private ExecutorService mIOService;
+
+    private Thread mMainThread;
+    private final int mIOThreadCount;
+
+    private ExecutorService mMainThreadService =
+            Executors.newSingleThreadExecutor(new ThreadFactory() {
+                @Override
+                public Thread newThread(@NonNull final Runnable r) {
+                    mMainThread = new LoggingThread(r);
+                    return mMainThread;
+                }
+            });
+
+    TaskExecutorWIthFakeMainThread(int ioThreadCount) {
+        mIOThreadCount = ioThreadCount;
+        mIOService = Executors.newFixedThreadPool(ioThreadCount, new ThreadFactory() {
+            @Override
+            public Thread newThread(@NonNull Runnable r) {
+                return new LoggingThread(r);
+            }
+        });
+    }
+
+    @Override
+    public void executeOnDiskIO(Runnable runnable) {
+        mIOService.execute(runnable);
+    }
+
+    @Override
+    public void executeOnMainThread(Runnable runnable) {
+        mMainThreadService.execute(runnable);
+    }
+
+    @Override
+    public boolean isMainThread() {
+        return Thread.currentThread() == mMainThread;
+    }
+
+    List<Throwable> getErrors() {
+        return mCaughtExceptions;
+    }
+
+    void shutdown(@SuppressWarnings("SameParameterValue") int timeoutInSeconds)
+            throws InterruptedException {
+        mMainThreadService.shutdown();
+        mIOService.shutdown();
+        mMainThreadService.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS);
+        mIOService.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS);
+    }
+
+    void drainTasks(int seconds) throws InterruptedException {
+        final CountDownLatch enterLatch = new CountDownLatch(mIOThreadCount);
+        final CountDownLatch exitLatch = new CountDownLatch(1);
+        for (int i = 0; i < mIOThreadCount; i++) {
+            executeOnDiskIO(new Runnable() {
+                @Override
+                public void run() {
+                    enterLatch.countDown();
+                    try {
+                        exitLatch.await();
+                    } catch (InterruptedException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            });
+        }
+
+        final CountDownLatch mainLatch = new CountDownLatch(1);
+        executeOnMainThread(new Runnable() {
+            @Override
+            public void run() {
+                mainLatch.countDown();
+            }
+        });
+        if (!enterLatch.await(seconds, TimeUnit.SECONDS)) {
+            throw new AssertionError("Could not drain IO tasks in " + seconds
+                    + " seconds");
+        }
+        exitLatch.countDown();
+        if (!mainLatch.await(seconds, TimeUnit.SECONDS)) {
+            throw new AssertionError("Could not drain UI tasks in " + seconds
+                    + " seconds");
+        }
+    }
+
+
+    class LoggingThread extends Thread {
+        LoggingThread(final Runnable target) {
+            super(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        target.run();
+                    } catch (Throwable t) {
+                        mCaughtExceptions.add(t);
+                    }
+                }
+            });
+        }
+    }
+}