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);
+ }
+ }
+ });
+ }
+ }
+}