Merge "Move to Gradle 4.4" into oc-mr1-jetpack-dev
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
index 5f61e21..e9245cc 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/ext/element_ext.kt
@@ -30,7 +30,10 @@
 import javax.lang.model.element.VariableElement
 import javax.lang.model.type.TypeKind
 import javax.lang.model.type.TypeMirror
+import javax.lang.model.type.WildcardType
 import javax.lang.model.util.SimpleAnnotationValueVisitor6
+import javax.lang.model.util.SimpleTypeVisitor7
+import javax.lang.model.util.Types
 import kotlin.reflect.KClass
 
 fun Element.hasAnyOf(vararg modifiers: Modifier): Boolean {
@@ -161,3 +164,50 @@
 fun AnnotationValue.getAsStringList(): List<String> {
     return ANNOTATION_VALUE_STRING_ARR_VISITOR.visit(this)
 }
+
+// a variant of Types.isAssignable that ignores variance.
+fun Types.isAssignableWithoutVariance(from: TypeMirror, to: TypeMirror): Boolean {
+    val assignable = isAssignable(from, to)
+    if (assignable) {
+        return true
+    }
+    if (from.kind != TypeKind.DECLARED || to.kind != TypeKind.DECLARED) {
+        return false
+    }
+    val declaredFrom = MoreTypes.asDeclared(from)
+    val declaredTo = MoreTypes.asDeclared(to)
+    val fromTypeArgs = declaredFrom.typeArguments
+    val toTypeArgs = declaredTo.typeArguments
+    // no type arguments, we don't need extra checks
+    if (fromTypeArgs.isEmpty() || fromTypeArgs.size != toTypeArgs.size) {
+        return false
+    }
+    // check erasure version first, if it does not match, no reason to proceed
+    if (!isAssignable(erasure(from), erasure(to))) {
+        return false
+    }
+    // convert from args to their upper bounds if it exists
+    val fromUpperBounds = fromTypeArgs.map {
+        it.getUpperBound()
+    }
+    // if there are no upper bound conversions, return.
+    if (fromUpperBounds.all { it == null }) {
+        return false
+    }
+    // try to move the types of the from to their upper bounds. It does not matter for the "to"
+    // because Types.isAssignable handles it as it is valid java
+    return (0 until fromTypeArgs.size).all { index ->
+        isAssignableWithoutVariance(
+                from = fromUpperBounds[index] ?: fromTypeArgs[index],
+                to = toTypeArgs[index])
+    }
+}
+
+// converts ? in Set< ? extends Foo> to Foo
+private fun TypeMirror.getUpperBound(): TypeMirror? {
+    return this.accept(object : SimpleTypeVisitor7<TypeMirror, Void?>() {
+        override fun visitWildcard(type: WildcardType, ignored: Void?): TypeMirror {
+            return type.extendsBound
+        }
+    }, null)
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
index cb63b6d..f3b505d 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/PojoProcessor.kt
@@ -26,6 +26,7 @@
 import android.arch.persistence.room.ext.getAsStringList
 import android.arch.persistence.room.ext.hasAnnotation
 import android.arch.persistence.room.ext.hasAnyOf
+import android.arch.persistence.room.ext.isAssignableWithoutVariance
 import android.arch.persistence.room.ext.isCollection
 import android.arch.persistence.room.ext.toClassType
 import android.arch.persistence.room.processor.ProcessorErrors.CANNOT_FIND_GETTER_FOR_FIELD
@@ -216,7 +217,8 @@
                     } else if (!field.nameWithVariations.contains(paramName)) {
                         false
                     } else {
-                        typeUtils.isAssignable(paramType, field.type)
+                        // see: b/69164099
+                        typeUtils.isAssignableWithoutVariance(paramType, field.type)
                     }
                 }
 
@@ -555,7 +557,8 @@
 
         val matching = candidates
                 .filter {
-                    types.isAssignable(getType(it), field.element.asType())
+                    // b/69164099
+                    types.isAssignableWithoutVariance(getType(it), field.element.asType())
                             && (field.nameWithVariations.contains(it.simpleName.toString())
                             || nameVariations.contains(it.simpleName.toString()))
                 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
index dcd5ee6..d64300c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/TypeAdapterStore.kt
@@ -362,8 +362,14 @@
                 || MoreTypes.isTypeOf(java.util.Set::class.java, typeMirror))) {
             val declared = MoreTypes.asDeclared(typeMirror)
             val binder = findStatementValueBinder(declared.typeArguments.first(),
-                    null) ?: return null
-            return CollectionQueryParameterAdapter(binder)
+                    null)
+            if (binder != null) {
+                return CollectionQueryParameterAdapter(binder)
+            } else {
+                // maybe user wants to convert this collection themselves. look for a match
+                val collectionBinder = findStatementValueBinder(typeMirror, null) ?: return null
+                return BasicQueryParameterAdapter(collectionBinder)
+            }
         } else if (typeMirror is ArrayType && typeMirror.componentType.kind != TypeKind.BYTE) {
             val component = typeMirror.componentType
             val binder = findStatementValueBinder(component, null) ?: return null
@@ -439,8 +445,10 @@
     private fun getAllTypeConverters(input: TypeMirror, excludes: List<TypeMirror>):
             List<TypeConverter> {
         val types = context.processingEnv.typeUtils
+        // for input, check assignability because it defines whether we can use the method or not.
+        // for excludes, use exact match
         return typeConverters.filter { converter ->
-            types.isSameType(input, converter.from) &&
+            types.isAssignable(input, converter.from) &&
                     !excludes.any { types.isSameType(it, converter.to) }
         }
     }
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt
index d9b4997..ae635b2 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/CustomTypeConverterResolutionTest.kt
@@ -39,6 +39,7 @@
 import com.squareup.javapoet.FieldSpec
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
+import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
 import com.squareup.javapoet.TypeSpec
 import org.junit.Test
@@ -83,6 +84,26 @@
                     }
                 }
                 """)
+        val CUSTOM_TYPE_SET = ParameterizedTypeName.get(
+                ClassName.get(Set::class.java), CUSTOM_TYPE)
+        val CUSTOM_TYPE_SET_CONVERTER = ClassName.get("foo.bar", "MySetConverter")
+        val CUSTOM_TYPE_SET_CONVERTER_JFO = JavaFileObjects.forSourceLines(
+                CUSTOM_TYPE_SET_CONVERTER.toString(),
+                """
+                package ${CUSTOM_TYPE_SET_CONVERTER.packageName()};
+                import java.util.HashSet;
+                import java.util.Set;
+                public class ${CUSTOM_TYPE_SET_CONVERTER.simpleName()} {
+                    @${TypeConverter::class.java.canonicalName}
+                    public static $CUSTOM_TYPE_SET toCustom(int value) {
+                        return null;
+                    }
+                    @${TypeConverter::class.java.canonicalName}
+                    public static int fromCustom($CUSTOM_TYPE_SET input) {
+                        return 0;
+                    }
+                }
+                """)
     }
 
     @Test
@@ -94,6 +115,36 @@
     }
 
     @Test
+    fun collection_forEntity() {
+        val entity = createEntity(
+                hasCustomField = true,
+                useCollection = true)
+        val database = createDatabase(
+                hasConverters = true,
+                hasDao = true,
+                useCollection = true)
+        val dao = createDao(
+                hasQueryWithCustomParam = false,
+                useCollection = true)
+        run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+    }
+
+    @Test
+    fun collection_forDao() {
+        val entity = createEntity(
+                hasCustomField = true,
+                useCollection = true)
+        val database = createDatabase(
+                hasConverters = true,
+                hasDao = true,
+                useCollection = true)
+        val dao = createDao(
+                hasQueryWithCustomParam = true,
+                useCollection = true)
+        run(entity.toJFO(), dao.toJFO(), database.toJFO()).compilesWithoutError()
+    }
+
+    @Test
     fun useFromDatabase_forQueryParameter() {
         val entity = createEntity()
         val database = createDatabase(hasConverters = true, hasDao = true)
@@ -170,21 +221,29 @@
 
     fun run(vararg jfos: JavaFileObject): CompileTester {
         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
-                .that(jfos.toList() + CUSTOM_TYPE_JFO + CUSTOM_TYPE_CONVERTER_JFO)
+                .that(jfos.toList() + CUSTOM_TYPE_JFO + CUSTOM_TYPE_CONVERTER_JFO
+                        + CUSTOM_TYPE_SET_CONVERTER_JFO)
                 .processedWith(RoomProcessor())
     }
 
-    private fun createEntity(hasCustomField: Boolean = false,
-                             hasConverters: Boolean = false,
-                             hasConverterOnField: Boolean = false): TypeSpec {
+    private fun createEntity(
+            hasCustomField: Boolean = false,
+            hasConverters: Boolean = false,
+            hasConverterOnField: Boolean = false,
+            useCollection: Boolean = false): TypeSpec {
         if (hasConverterOnField && hasConverters) {
             throw IllegalArgumentException("cannot have both converters")
         }
+        val type = if (useCollection) {
+            CUSTOM_TYPE_SET
+        } else {
+            CUSTOM_TYPE
+        }
         return TypeSpec.classBuilder(ENTITY).apply {
             addAnnotation(Entity::class.java)
             addModifiers(Modifier.PUBLIC)
             if (hasCustomField) {
-                addField(FieldSpec.builder(CUSTOM_TYPE, "myCustomField", Modifier.PUBLIC).apply {
+                addField(FieldSpec.builder(type, "myCustomField", Modifier.PUBLIC).apply {
                     if (hasConverterOnField) {
                         addAnnotation(createConvertersAnnotation())
                     }
@@ -199,13 +258,15 @@
         }.build()
     }
 
-    private fun createDatabase(hasConverters: Boolean = false,
-                               hasDao: Boolean = false): TypeSpec {
+    private fun createDatabase(
+            hasConverters: Boolean = false,
+            hasDao: Boolean = false,
+            useCollection: Boolean = false): TypeSpec {
         return TypeSpec.classBuilder(DB).apply {
             addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
             superclass(RoomTypeNames.ROOM_DB)
             if (hasConverters) {
-                addAnnotation(createConvertersAnnotation())
+                addAnnotation(createConvertersAnnotation(useCollection = useCollection))
             }
             addField(FieldSpec.builder(TypeName.INT, "id", Modifier.PUBLIC).apply {
                 addAnnotation(PrimaryKey::class.java)
@@ -225,11 +286,13 @@
         }.build()
     }
 
-    private fun createDao(hasConverters: Boolean = false,
-                          hasQueryReturningEntity: Boolean = false,
-                          hasQueryWithCustomParam: Boolean = false,
-                          hasMethodConverters: Boolean = false,
-                          hasParameterConverters: Boolean = false): TypeSpec {
+    private fun createDao(
+            hasConverters: Boolean = false,
+            hasQueryReturningEntity: Boolean = false,
+            hasQueryWithCustomParam: Boolean = false,
+            hasMethodConverters: Boolean = false,
+            hasParameterConverters: Boolean = false,
+            useCollection: Boolean = false): TypeSpec {
         val annotationCount = listOf(hasMethodConverters, hasConverters, hasParameterConverters)
                 .map { if (it) 1 else 0 }.sum()
         if (annotationCount > 1) {
@@ -242,7 +305,7 @@
             addAnnotation(Dao::class.java)
             addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
             if (hasConverters) {
-                addAnnotation(createConvertersAnnotation())
+                addAnnotation(createConvertersAnnotation(useCollection = useCollection))
             }
             if (hasQueryReturningEntity) {
                 addMethod(MethodSpec.methodBuilder("loadAll").apply {
@@ -253,18 +316,23 @@
                     returns(ENTITY)
                 }.build())
             }
+            val customType = if (useCollection) {
+                CUSTOM_TYPE_SET
+            } else {
+                CUSTOM_TYPE
+            }
             if (hasQueryWithCustomParam) {
                 addMethod(MethodSpec.methodBuilder("queryWithCustom").apply {
                     addAnnotation(AnnotationSpec.builder(Query::class.java).apply {
                         addMember("value", S, "SELECT COUNT(*) FROM ${ENTITY.simpleName()} where" +
-                                " id IN(:customs)")
+                                " id = :custom")
                     }.build())
                     if (hasMethodConverters) {
-                        addAnnotation(createConvertersAnnotation())
+                        addAnnotation(createConvertersAnnotation(useCollection = useCollection))
                     }
-                    addParameter(ParameterSpec.builder(CUSTOM_TYPE, "customs").apply {
+                    addParameter(ParameterSpec.builder(customType, "custom").apply {
                         if (hasParameterConverters) {
-                            addAnnotation(createConvertersAnnotation())
+                            addAnnotation(createConvertersAnnotation(useCollection = useCollection))
                         }
                     }.build())
                     addModifiers(Modifier.ABSTRACT)
@@ -274,8 +342,13 @@
         }.build()
     }
 
-    private fun createConvertersAnnotation(): AnnotationSpec {
+    private fun createConvertersAnnotation(useCollection: Boolean = false): AnnotationSpec {
+        val converter = if (useCollection) {
+            CUSTOM_TYPE_SET_CONVERTER
+        } else {
+            CUSTOM_TYPE_CONVERTER
+        }
         return AnnotationSpec.builder(TypeConverters::class.java)
-                .addMember("value", "$T.class", CUSTOM_TYPE_CONVERTER).build()
+                .addMember("value", "$T.class", converter).build()
     }
 }
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt
new file mode 100644
index 0000000..0945ae1
--- /dev/null
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/solver/TypeAssignmentTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 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 android.arch.persistence.room.solver
+
+import android.arch.persistence.room.ext.getAllFieldsIncludingPrivateSupers
+import android.arch.persistence.room.ext.isAssignableWithoutVariance
+import android.arch.persistence.room.testing.TestInvocation
+import com.google.testing.compile.JavaFileObjects
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import simpleRun
+import javax.annotation.processing.ProcessingEnvironment
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+
+class TypeAssignmentTest {
+    companion object {
+        private val TEST_OBJECT = JavaFileObjects.forSourceString("foo.bar.MyObject",
+                """
+            package foo.bar;
+            import java.util.Set;
+            import java.util.HashSet;
+            class MyObject {
+                String mString;
+                Integer mInteger;
+                Set<MyObject> mSet;
+                Set<? extends MyObject> mVarianceSet;
+                HashSet<MyObject> mHashSet;
+            }
+            """.trimIndent())
+    }
+
+    @Test
+    fun basic() {
+        runTest {
+            val testObject = typeElement("foo.bar.MyObject")
+            val string = testObject.getField(processingEnv, "mString")
+            val integer = testObject.getField(processingEnv, "mInteger")
+            assertThat(typeUtils.isAssignableWithoutVariance(string.asType(),
+                    integer.asType()),
+                    `is`(false))
+        }
+    }
+
+    @Test
+    fun generics() {
+        runTest {
+            val testObject = typeElement("foo.bar.MyObject")
+            val set = testObject.getField(processingEnv, "mSet").asType()
+            val hashSet = testObject.getField(processingEnv, "mHashSet").asType()
+            assertThat(typeUtils.isAssignableWithoutVariance(
+                    from = set,
+                    to = hashSet
+            ), `is`(false))
+            assertThat(typeUtils.isAssignableWithoutVariance(
+                    from = hashSet,
+                    to = set
+            ), `is`(true))
+        }
+    }
+
+    @Test
+    fun variance() {
+        /**
+         *  Set<User> userSet = null;
+         *  Set<? extends User> userSet2 = null;
+         *  userSet = userSet2;  // NOT OK for java but kotlin data classes hit this so we want
+         *                       // to accept it
+         */
+        runTest {
+            val testObject = typeElement("foo.bar.MyObject")
+            val set = testObject.getField(processingEnv, "mSet").asType()
+            val varianceSet = testObject.getField(processingEnv, "mVarianceSet").asType()
+            assertThat(typeUtils.isAssignableWithoutVariance(
+                    from = set,
+                    to = varianceSet
+            ), `is`(true))
+            assertThat(typeUtils.isAssignableWithoutVariance(
+                    from = varianceSet,
+                    to = set
+            ), `is`(true))
+        }
+    }
+
+    private fun TypeElement.getField(
+            env: ProcessingEnvironment,
+            name: String): VariableElement {
+        return getAllFieldsIncludingPrivateSupers(env).first {
+            it.simpleName.toString() == name
+        }
+    }
+
+    private fun runTest(handler: TestInvocation.() -> Unit) {
+        simpleRun(TEST_OBJECT) {
+            it.apply { handler() }
+        }.compilesWithoutError()
+    }
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt
index 86b6e01..e03f1db 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/TestInvocation.kt
@@ -29,4 +29,6 @@
     fun typeElement(qName: String): TypeElement {
         return processingEnv.elementUtils.getTypeElement(qName)
     }
+
+    val typeUtils by lazy { processingEnv.typeUtils }
 }
diff --git a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json b/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
index e6bb21c..04e7cad 100644
--- a/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
+++ b/room/integration-tests/kotlintestapp/schemas/android.arch.persistence.room.integration.kotlintestapp.TestDatabase/1.json
@@ -2,11 +2,11 @@
   "formatVersion": 1,
   "database": {
     "version": 1,
-    "identityHash": "933c7e2810b0f89ab84faa68bbea5852",
+    "identityHash": "c7e8b9f03366a1b0a03ab28b7c9c5ad7",
     "entities": [
       {
         "tableName": "Book",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `title` TEXT NOT NULL, `bookPublisherId` TEXT NOT NULL, PRIMARY KEY(`bookId`), FOREIGN KEY(`bookPublisherId`) REFERENCES `Publisher`(`publisherId`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `title` TEXT NOT NULL, `bookPublisherId` TEXT NOT NULL, `languages` INTEGER NOT NULL, PRIMARY KEY(`bookId`), FOREIGN KEY(`bookPublisherId`) REFERENCES `Publisher`(`publisherId`) ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED)",
         "fields": [
           {
             "fieldPath": "bookId",
@@ -25,6 +25,12 @@
             "columnName": "bookPublisherId",
             "affinity": "TEXT",
             "notNull": true
+          },
+          {
+            "fieldPath": "languages",
+            "columnName": "languages",
+            "affinity": "INTEGER",
+            "notNull": true
           }
         ],
         "primaryKey": {
@@ -191,7 +197,7 @@
     ],
     "setupQueries": [
       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
-      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"933c7e2810b0f89ab84faa68bbea5852\")"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"c7e8b9f03366a1b0a03ab28b7c9c5ad7\")"
     ]
   }
 }
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt
index 20e9d18..39e8cae 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -20,10 +20,12 @@
 import android.arch.persistence.room.Dao
 import android.arch.persistence.room.Insert
 import android.arch.persistence.room.Query
+import android.arch.persistence.room.TypeConverters
 import android.arch.persistence.room.integration.kotlintestapp.vo.Author
 import android.arch.persistence.room.integration.kotlintestapp.vo.Book
 import android.arch.persistence.room.integration.kotlintestapp.vo.BookAuthor
 import android.arch.persistence.room.integration.kotlintestapp.vo.BookWithPublisher
+import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
 import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
 import android.arch.persistence.room.integration.kotlintestapp.vo.PublisherWithBooks
 import io.reactivex.Flowable
@@ -91,4 +93,8 @@
 
     @Query("UPDATE book SET title = :title WHERE bookId = :bookId")
     fun updateBookTitle(bookId: String, title: String?)
+
+    @Query("SELECT * FROM book WHERE languages & :langs != 0 ORDER BY bookId ASC")
+    @TypeConverters(Lang::class)
+    fun findByLanguages(langs: Set<Lang>): List<Book>
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
index acc2c7d..93be6e9 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/BooksDaoTest.kt
@@ -19,6 +19,7 @@
 import android.arch.persistence.room.integration.kotlintestapp.vo.Author
 import android.arch.persistence.room.integration.kotlintestapp.vo.Book
 import android.arch.persistence.room.integration.kotlintestapp.vo.BookWithPublisher
+import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
 import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
 import android.database.sqlite.SQLiteConstraintException
 import android.support.test.filters.SmallTest
@@ -119,4 +120,25 @@
                 TestUtil.BOOK_2.bookId))
         assertThat(books, `is`(listOf(TestUtil.BOOK_2, TestUtil.BOOK_1)))
     }
+
+    @Test
+    fun findBooksByLanguage() {
+        booksDao.addPublishers(TestUtil.PUBLISHER)
+        val book1 = TestUtil.BOOK_1.copy(languages = setOf(Lang.TR))
+        val book2 = TestUtil.BOOK_2.copy(languages = setOf(Lang.ES, Lang.TR))
+        val book3 = TestUtil.BOOK_3.copy(languages = setOf(Lang.EN))
+        booksDao.addBooks(book1, book2, book3)
+
+        assertThat(booksDao.findByLanguages(setOf(Lang.EN, Lang.TR)),
+                `is`(listOf(book1, book2, book3)))
+
+        assertThat(booksDao.findByLanguages(setOf(Lang.TR)),
+                `is`(listOf(book1, book2)))
+
+        assertThat(booksDao.findByLanguages(setOf(Lang.ES)),
+                `is`(listOf(book2)))
+
+        assertThat(booksDao.findByLanguages(setOf(Lang.EN)),
+                `is`(listOf(book3)))
+    }
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt
index c4d406c..57546d0 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/test/TestUtil.kt
@@ -19,6 +19,7 @@
 import android.arch.persistence.room.integration.kotlintestapp.vo.Author
 import android.arch.persistence.room.integration.kotlintestapp.vo.Book
 import android.arch.persistence.room.integration.kotlintestapp.vo.BookAuthor
+import android.arch.persistence.room.integration.kotlintestapp.vo.Lang
 import android.arch.persistence.room.integration.kotlintestapp.vo.Publisher
 
 class TestUtil {
@@ -31,8 +32,12 @@
         val AUTHOR_1 = Author("a1", "author 1")
         val AUTHOR_2 = Author("a2", "author 2")
 
-        val BOOK_1 = Book("b1", "book title 1", "ph1")
-        val BOOK_2 = Book("b2", "book title 2", "ph1")
+        val BOOK_1 = Book("b1", "book title 1", "ph1",
+                setOf(Lang.EN))
+        val BOOK_2 = Book("b2", "book title 2", "ph1",
+                setOf(Lang.TR))
+        val BOOK_3 = Book("b3", "book title 2", "ph1",
+                setOf(Lang.ES))
 
         val BOOK_AUTHOR_1_1 = BookAuthor(BOOK_1.bookId, AUTHOR_1.authorId)
         val BOOK_AUTHOR_1_2 = BookAuthor(BOOK_1.bookId, AUTHOR_2.authorId)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt
index 794e60b..fe4a600 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Book.kt
@@ -19,10 +19,16 @@
 import android.arch.persistence.room.Entity
 import android.arch.persistence.room.ForeignKey
 import android.arch.persistence.room.PrimaryKey
+import android.arch.persistence.room.TypeConverters
 
 @Entity(foreignKeys = arrayOf(
         ForeignKey(entity = Publisher::class,
                 parentColumns = arrayOf("publisherId"),
                 childColumns = arrayOf("bookPublisherId"),
                 deferred = true)))
-data class Book(@PrimaryKey val bookId: String, val title: String, val bookPublisherId: String)
+data class Book(
+        @PrimaryKey val bookId: String,
+        val title: String,
+        val bookPublisherId: String,
+        @field:TypeConverters(Lang::class)
+        val languages: Set<Lang>)
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt
new file mode 100644
index 0000000..1d5fb5c
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/android/arch/persistence/room/integration/kotlintestapp/vo/Lang.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 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 android.arch.persistence.room.integration.kotlintestapp.vo
+
+import android.arch.persistence.room.TypeConverter
+
+/**
+ * An enum class which gets saved as a bit set in the database.
+ */
+enum class Lang {
+    TR,
+    EN,
+    ES;
+
+    companion object {
+        @JvmStatic
+        @TypeConverter
+        fun toInt(langs: Set<Lang>): Int {
+            return langs.fold(0) { left, lang ->
+                left.or(1 shl lang.ordinal)
+            }
+        }
+
+        @JvmStatic
+        @TypeConverter
+        fun toSet(value: Int): Set<Lang> {
+            return Lang.values().filter {
+                (1 shl it.ordinal).and(value) != 0
+            }.toSet()
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
index 2fad7b1..610afb2 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/TestDatabase.java
@@ -32,6 +32,7 @@
 import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
 import android.arch.persistence.room.integration.testapp.dao.WithClauseDao;
 import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
+import android.arch.persistence.room.integration.testapp.vo.Day;
 import android.arch.persistence.room.integration.testapp.vo.FunnyNamedEntity;
 import android.arch.persistence.room.integration.testapp.vo.Pet;
 import android.arch.persistence.room.integration.testapp.vo.PetCouple;
@@ -41,6 +42,8 @@
 import android.arch.persistence.room.integration.testapp.vo.User;
 
 import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
 
 @Database(entities = {User.class, Pet.class, School.class, PetCouple.class, Toy.class,
         BlobEntity.class, Product.class, FunnyNamedEntity.class},
@@ -74,5 +77,25 @@
                 return date.getTime();
             }
         }
+
+        @TypeConverter
+        public Set<Day> decomposeDays(int flags) {
+            Set<Day> result = new HashSet<>();
+            for (Day day : Day.values()) {
+                if ((flags & (1 << day.ordinal())) != 0) {
+                    result.add(day);
+                }
+            }
+            return result;
+        }
+
+        @TypeConverter
+        public int composeDays(Set<Day> days) {
+            int result = 0;
+            for (Day day : days) {
+                result |= 1 << day.ordinal();
+            }
+            return result;
+        }
     }
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 0b184a9..69463da 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -29,6 +29,7 @@
 import android.arch.persistence.room.Update;
 import android.arch.persistence.room.integration.testapp.TestDatabase;
 import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
+import android.arch.persistence.room.integration.testapp.vo.Day;
 import android.arch.persistence.room.integration.testapp.vo.User;
 import android.database.Cursor;
 
@@ -36,6 +37,7 @@
 
 import java.util.Date;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Callable;
 
 import io.reactivex.Flowable;
@@ -203,6 +205,9 @@
     @Query("UPDATE User set mWeight = :weight WHERE mId IN (:ids) AND mAge == :age")
     public abstract int updateByAgeAndIds(float weight, int age, List<Integer> ids);
 
+    @Query("SELECT * FROM user WHERE (mWorkDays & :days) != 0")
+    public abstract List<User> findUsersByWorkDays(Set<Day> days);
+
     // QueryLoader
 
     @Query("SELECT COUNT(*) from user")
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index f8049f3..2f1ca54 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -35,6 +35,7 @@
 import android.arch.persistence.room.integration.testapp.dao.UserDao;
 import android.arch.persistence.room.integration.testapp.dao.UserPetDao;
 import android.arch.persistence.room.integration.testapp.vo.BlobEntity;
+import android.arch.persistence.room.integration.testapp.vo.Day;
 import android.arch.persistence.room.integration.testapp.vo.Pet;
 import android.arch.persistence.room.integration.testapp.vo.Product;
 import android.arch.persistence.room.integration.testapp.vo.User;
@@ -57,7 +58,9 @@
 import java.util.Calendar;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 @SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
 @SmallTest
@@ -524,4 +527,35 @@
         assertTrue("SQLiteConstraintException expected", caught);
         assertThat(mUserDao.count(), is(0));
     }
+
+    @Test
+    public void enumSet_simpleLoad() {
+        User a = TestUtil.createUser(3);
+        Set<Day> expected = toSet(Day.MONDAY, Day.TUESDAY);
+        a.setWorkDays(expected);
+        mUserDao.insert(a);
+        User loaded = mUserDao.load(3);
+        assertThat(loaded.getWorkDays(), is(expected));
+    }
+
+    @Test
+    public void enumSet_query() {
+        User user1 = TestUtil.createUser(3);
+        user1.setWorkDays(toSet(Day.MONDAY, Day.FRIDAY));
+        User user2 = TestUtil.createUser(5);
+        user2.setWorkDays(toSet(Day.MONDAY, Day.THURSDAY));
+        mUserDao.insert(user1);
+        mUserDao.insert(user2);
+        List<User> empty = mUserDao.findUsersByWorkDays(toSet(Day.WEDNESDAY));
+        assertThat(empty.size(), is(0));
+        List<User> friday = mUserDao.findUsersByWorkDays(toSet(Day.FRIDAY));
+        assertThat(friday, is(Arrays.asList(user1)));
+        List<User> monday = mUserDao.findUsersByWorkDays(toSet(Day.MONDAY));
+        assertThat(monday, is(Arrays.asList(user1, user2)));
+
+    }
+
+    private Set<Day> toSet(Day... days) {
+        return new HashSet<>(Arrays.asList(days));
+    }
 }
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java
new file mode 100644
index 0000000..e02b91c
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/Day.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 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 android.arch.persistence.room.integration.testapp.vo;
+
+public enum Day {
+    MONDAY,
+    TUESDAY,
+    WEDNESDAY,
+    THURSDAY,
+    FRIDAY,
+    SATURDAY,
+    SUNDAY
+}
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java
index a5b8839..a615819 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/vo/User.java
@@ -23,6 +23,8 @@
 import android.arch.persistence.room.integration.testapp.TestDatabase;
 
 import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
 
 @Entity
 @TypeConverters({TestDatabase.Converters.class})
@@ -46,6 +48,9 @@
     @ColumnInfo(name = "custommm", collate = ColumnInfo.NOCASE)
     private String mCustomField;
 
+    // bit flags
+    private Set<Day> mWorkDays = new HashSet<>();
+
     public int getId() {
         return mId;
     }
@@ -110,6 +115,15 @@
         mCustomField = customField;
     }
 
+    public Set<Day> getWorkDays() {
+        return mWorkDays;
+    }
+
+    public void setWorkDays(
+            Set<Day> workDays) {
+        mWorkDays = workDays;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
@@ -128,8 +142,11 @@
         if (mBirthday != null ? !mBirthday.equals(user.mBirthday) : user.mBirthday != null) {
             return false;
         }
-        return mCustomField != null ? mCustomField.equals(user.mCustomField)
-                : user.mCustomField == null;
+        if (mCustomField != null ? !mCustomField.equals(user.mCustomField)
+                : user.mCustomField != null) {
+            return false;
+        }
+        return mWorkDays != null ? mWorkDays.equals(user.mWorkDays) : user.mWorkDays == null;
     }
 
     @Override
@@ -142,6 +159,7 @@
         result = 31 * result + (mWeight != +0.0f ? Float.floatToIntBits(mWeight) : 0);
         result = 31 * result + (mBirthday != null ? mBirthday.hashCode() : 0);
         result = 31 * result + (mCustomField != null ? mCustomField.hashCode() : 0);
+        result = 31 * result + (mWorkDays != null ? mWorkDays.hashCode() : 0);
         return result;
     }
 
@@ -155,7 +173,8 @@
                 + ", mAdmin=" + mAdmin
                 + ", mWeight=" + mWeight
                 + ", mBirthday=" + mBirthday
-                + ", mCustom=" + mCustomField
+                + ", mCustomField='" + mCustomField + '\''
+                + ", mWorkDays=" + mWorkDays
                 + '}';
     }
 }