Merge "Add DiffUtil.ItemCallback" into oc-mr1-jetpack-dev
diff --git a/buildSrc/init.gradle b/buildSrc/init.gradle
index f09c777..a81a2a9 100644
--- a/buildSrc/init.gradle
+++ b/buildSrc/init.gradle
@@ -127,7 +127,7 @@
 def configureBuildOnServer() {
     def buildOnServerTask = rootProject.tasks.create("buildOnServer")
     rootProject.tasks.whenTaskAdded { task ->
-        if ("createArchive".equals(task.name)) {
+        if ("createArchive".equals(task.name) || "distDocs".equals(task.name)) {
             buildOnServerTask.dependsOn task
         }
     }
diff --git a/lifecycle/common/src/main/java/android/arch/lifecycle/ClassesInfoCache.java b/lifecycle/common/src/main/java/android/arch/lifecycle/ClassesInfoCache.java
index f077dae..d88e276 100644
--- a/lifecycle/common/src/main/java/android/arch/lifecycle/ClassesInfoCache.java
+++ b/lifecycle/common/src/main/java/android/arch/lifecycle/ClassesInfoCache.java
@@ -46,7 +46,7 @@
             return mHasLifecycleMethods.get(klass);
         }
 
-        Method[] methods = klass.getDeclaredMethods();
+        Method[] methods = getDeclaredMethods(klass);
         for (Method method : methods) {
             OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
             if (annotation != null) {
@@ -64,6 +64,18 @@
         return false;
     }
 
+    private Method[] getDeclaredMethods(Class klass) {
+        try {
+            return klass.getDeclaredMethods();
+        } catch (NoClassDefFoundError e) {
+            throw new IllegalArgumentException("The observer class has some methods that use "
+                    + "newer APIs which are not available in the current OS version. Lifecycles "
+                    + "cannot access even other methods so you should make sure that your "
+                    + "observer classes only access framework classes that are available "
+                    + "in your min API level OR use lifecycle:compiler annotation processor.", e);
+        }
+    }
+
     CallbackInfo getInfo(Class klass) {
         CallbackInfo existing = mCallbackMap.get(klass);
         if (existing != null) {
@@ -106,7 +118,7 @@
             }
         }
 
-        Method[] methods = declaredMethods != null ? declaredMethods : klass.getDeclaredMethods();
+        Method[] methods = declaredMethods != null ? declaredMethods : getDeclaredMethods(klass);
         boolean hasLifecycleMethods = false;
         for (Method method : methods) {
             OnLifecycleEvent annotation = method.getAnnotation(OnLifecycleEvent.class);
diff --git a/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java b/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
index 672b3a3..718c79e 100644
--- a/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
+++ b/lifecycle/extensions/src/main/java/android/arch/lifecycle/MediatorLiveData.java
@@ -24,11 +24,43 @@
 import java.util.Map;
 
 /**
- * {@link LiveData} subclass which may observer other {@code LiveData} objects and react on
+ * {@link LiveData} subclass which may observe other {@code LiveData} objects and react on
  * {@code OnChanged} events from them.
  * <p>
  * This class correctly propagates its active/inactive states down to source {@code LiveData}
  * objects.
+ * <p>
+ * Consider the following scenario: we have 2 instances of {@code LiveData}, let's name them
+ * {@code liveData1} and {@code liveData2}, and we want to merge their emissions in one object:
+ * {@code liveDataMerger}. Then, {@code liveData1} and {@code liveData2} will become sources for
+ * the {@code MediatorLiveData liveDataMerger} and every time {@code onChanged} callback
+ * is called for either of them, we set a new value in {@code liveDataMerger}.
+ *
+ * <pre>
+ * LiveData<Integer> liveData1 = ...;
+ * LiveData<Integer> liveData2 = ...;
+ *
+ * MediatorLiveData<Integer> liveDataMerger = new MediatorLiveData<>();
+ * liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
+ * liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
+ * </pre>
+ * <p>
+ * Let's consider that we only want 10 values emitted by {@code liveData1}, to be
+ * merged in the {@code liveDataMerger}. Then, after 10 values, we can stop listening to {@code
+ * liveData1} and remove it as a source.
+ * <pre>
+ * liveDataMerger.addSource(liveData1, new Observer<Integer>() {
+ *      private int count = 1;
+ *
+ *      {@literal @}Override public void onChanged(@Nullable Integer s) {
+ *          count++;
+ *          liveDataMerger.setValue(s);
+ *          if (count > 10) {
+ *              liveDataMerger.removeSource(liveData1);
+ *          }
+ *      }
+ * });
+ * </pre>
  *
  * @param <T> The type of data hold by this instance
  */
diff --git a/lifecycle/runtime/build.gradle b/lifecycle/runtime/build.gradle
index 7678e95..111429c 100644
--- a/lifecycle/runtime/build.gradle
+++ b/lifecycle/runtime/build.gradle
@@ -32,6 +32,9 @@
 
     testCompile libs.junit
     testCompile libs.mockito_core
+
+    androidTestImplementation libs.junit
+    androidTestImplementation libs.test_runner,      { exclude module: 'support-annotations' }
 }
 
 createAndroidCheckstyle(project)
diff --git a/lifecycle/runtime/src/androidTest/java/android/arch/lifecycle/MissingClassTest.java b/lifecycle/runtime/src/androidTest/java/android/arch/lifecycle/MissingClassTest.java
new file mode 100644
index 0000000..81a0756
--- /dev/null
+++ b/lifecycle/runtime/src/androidTest/java/android/arch/lifecycle/MissingClassTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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 android.arch.lifecycle;
+
+import android.app.PictureInPictureParams;
+import android.os.Build;
+import android.support.test.filters.SdkSuppress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.N_MR1)
+@SmallTest
+public class MissingClassTest {
+    public static class ObserverWithMissingClasses {
+        @SuppressWarnings("unused")
+        public void newApiMethod(PictureInPictureParams params) {}
+
+        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+        public void onResume() {}
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testMissingApi() {
+        new ReflectiveGenericLifecycleObserver(new ObserverWithMissingClasses());
+    }
+}
diff --git a/media-compat/Android.mk b/media-compat/Android.mk
index 02ed90d..4a4cd02 100644
--- a/media-compat/Android.mk
+++ b/media-compat/Android.mk
@@ -28,7 +28,6 @@
 LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
 LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/java
 LOCAL_SRC_FILES := \
-    $(call all-java-files-under,kitkat) \
     $(call all-java-files-under,api21) \
     $(call all-java-files-under,api22) \
     $(call all-java-files-under,api23) \
diff --git a/media-compat/build.gradle b/media-compat/build.gradle
index 2ea2483..c504996 100644
--- a/media-compat/build.gradle
+++ b/media-compat/build.gradle
@@ -18,8 +18,6 @@
 
     sourceSets {
         main.java.srcDirs = [
-                'jellybean-mr2',
-                'kitkat',
                 'api21',
                 'api22',
                 'api23',
diff --git a/media-compat/java/android/support/v4/media/RatingCompat.java b/media-compat/java/android/support/v4/media/RatingCompat.java
index b538cac..e70243f 100644
--- a/media-compat/java/android/support/v4/media/RatingCompat.java
+++ b/media-compat/java/android/support/v4/media/RatingCompat.java
@@ -18,6 +18,7 @@
 
 import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
 
+import android.media.Rating;
 import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -326,25 +327,25 @@
      */
     public static RatingCompat fromRating(Object ratingObj) {
         if (ratingObj != null && Build.VERSION.SDK_INT >= 19) {
-            final int ratingStyle = RatingCompatKitkat.getRatingStyle(ratingObj);
+            final int ratingStyle = ((Rating) ratingObj).getRatingStyle();
             final RatingCompat rating;
-            if (RatingCompatKitkat.isRated(ratingObj)) {
+            if (((Rating) ratingObj).isRated()) {
                 switch (ratingStyle) {
                     case RATING_HEART:
-                        rating = newHeartRating(RatingCompatKitkat.hasHeart(ratingObj));
+                        rating = newHeartRating(((Rating) ratingObj).hasHeart());
                         break;
                     case RATING_THUMB_UP_DOWN:
-                        rating = newThumbRating(RatingCompatKitkat.isThumbUp(ratingObj));
+                        rating = newThumbRating(((Rating) ratingObj).isThumbUp());
                         break;
                     case RATING_3_STARS:
                     case RATING_4_STARS:
                     case RATING_5_STARS:
                         rating = newStarRating(ratingStyle,
-                                RatingCompatKitkat.getStarRating(ratingObj));
+                                ((Rating) ratingObj).getStarRating());
                         break;
                     case RATING_PERCENTAGE:
                         rating = newPercentageRating(
-                                RatingCompatKitkat.getPercentRating(ratingObj));
+                                ((Rating) ratingObj).getPercentRating());
                         break;
                     default:
                         return null;
@@ -372,25 +373,25 @@
             if (isRated()) {
                 switch (mRatingStyle) {
                     case RATING_HEART:
-                        mRatingObj = RatingCompatKitkat.newHeartRating(hasHeart());
+                        mRatingObj = Rating.newHeartRating(hasHeart());
                         break;
                     case RATING_THUMB_UP_DOWN:
-                        mRatingObj = RatingCompatKitkat.newThumbRating(isThumbUp());
+                        mRatingObj = Rating.newThumbRating(isThumbUp());
                         break;
                     case RATING_3_STARS:
                     case RATING_4_STARS:
                     case RATING_5_STARS:
-                        mRatingObj = RatingCompatKitkat.newStarRating(mRatingStyle,
+                        mRatingObj = Rating.newStarRating(mRatingStyle,
                                 getStarRating());
                         break;
                     case RATING_PERCENTAGE:
-                        mRatingObj = RatingCompatKitkat.newPercentageRating(getPercentRating());
+                        mRatingObj = Rating.newPercentageRating(getPercentRating());
                         break;
                     default:
                         return null;
                 }
             } else {
-                mRatingObj = RatingCompatKitkat.newUnratedRating(mRatingStyle);
+                mRatingObj = Rating.newUnratedRating(mRatingStyle);
             }
         }
         return mRatingObj;
diff --git a/media-compat/kitkat/android/support/v4/media/RatingCompatKitkat.java b/media-compat/kitkat/android/support/v4/media/RatingCompatKitkat.java
deleted file mode 100644
index 1d3fa50..0000000
--- a/media-compat/kitkat/android/support/v4/media/RatingCompatKitkat.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2014 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.support.v4.media;
-
-import android.media.Rating;
-import android.support.annotation.RequiresApi;
-
-@RequiresApi(19)
-class RatingCompatKitkat {
-    public static Object newUnratedRating(int ratingStyle) {
-        return Rating.newUnratedRating(ratingStyle);
-    }
-
-    public static Object newHeartRating(boolean hasHeart) {
-        return Rating.newHeartRating(hasHeart);
-    }
-
-    public static Object newThumbRating(boolean thumbIsUp) {
-        return Rating.newThumbRating(thumbIsUp);
-    }
-
-    public static Object newStarRating(int starRatingStyle, float starRating) {
-        return Rating.newStarRating(starRatingStyle, starRating);
-    }
-
-    public static Object newPercentageRating(float percent) {
-        return Rating.newPercentageRating(percent);
-    }
-
-    public static boolean isRated(Object ratingObj) {
-        return ((Rating)ratingObj).isRated();
-    }
-
-    public static int getRatingStyle(Object ratingObj) {
-        return ((Rating)ratingObj).getRatingStyle();
-    }
-
-    public static boolean hasHeart(Object ratingObj) {
-        return ((Rating)ratingObj).hasHeart();
-    }
-
-    public static boolean isThumbUp(Object ratingObj) {
-        return ((Rating)ratingObj).isThumbUp();
-    }
-
-    public static float getStarRating(Object ratingObj) {
-        return ((Rating)ratingObj).getStarRating();
-    }
-
-    public static float getPercentRating(Object ratingObj) {
-        return ((Rating)ratingObj).getPercentRating();
-    }
-}
diff --git a/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java b/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
index c64be96..f05e6be 100644
--- a/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
+++ b/room/common/src/main/java/android/arch/persistence/room/RoomWarnings.java
@@ -125,4 +125,12 @@
      * annotation.
      */
     public static final String DEFAULT_CONSTRUCTOR = "ROOM_DEFAULT_CONSTRUCTOR";
+
+    /**
+     * Reported when a @Query method returns a Pojo that has relations but the method is not
+     * annotated with @Transaction. Relations are run as separate queries and if the query is not
+     * run inside a transaction, it might return inconsistent results from the database.
+     */
+    public static final String RELATION_QUERY_WITHOUT_TRANSACTION =
+            "ROOM_RELATION_QUERY_WITHOUT_TRANSACTION";
 }
diff --git a/room/common/src/main/java/android/arch/persistence/room/Transaction.java b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
index 914e4f4..3b6ede9 100644
--- a/room/common/src/main/java/android/arch/persistence/room/Transaction.java
+++ b/room/common/src/main/java/android/arch/persistence/room/Transaction.java
@@ -22,9 +22,10 @@
 import java.lang.annotation.Target;
 
 /**
- * Marks a method in an abstract {@link Dao} class as a transaction method.
+ * Marks a method in a {@link Dao} class as a transaction method.
  * <p>
- * The derived implementation of the method will execute the super method in a database transaction.
+ * When used on a non-abstract method of an abstract {@link Dao} class,
+ * the derived implementation of the method will execute the super method in a database transaction.
  * All the parameters and return types are preserved. The transaction will be marked as successful
  * unless an exception is thrown in the method body.
  * <p>
@@ -44,6 +45,38 @@
  *     }
  * }
  * </pre>
+ * <p>
+ * When used on a {@link Query} method that has a {@code Select} statement, the generated code for
+ * the Query will be run in a transaction. There are 2 main cases where you may want to do that:
+ * <ol>
+ *     <li>If the result of the query is fairly big, it is better to run it inside a transaction
+ *     to receive a consistent result. Otherwise, if the query result does not fit into a single
+ *     {@link android.database.CursorWindow CursorWindow}, the query result may be corrupted due to
+ *     changes in the database in between cursor window swaps.
+ *     <li>If the result of the query is a Pojo with {@link Relation} fields, these fields are
+ *     queried separately. To receive consistent results between these queries, you probably want
+ *     to run them in a single transaction.
+ * </ol>
+ * Example:
+ * <pre>
+ * class ProductWithReviews extends Product {
+ *     {@literal @}Relation(parentColumn = "id", entityColumn = "productId", entity = Review.class)
+ *     public List&lt;Review> reviews;
+ * }
+ * {@literal @}Dao
+ * public interface ProductDao {
+ *     {@literal @}Transaction {@literal @}Query("SELECT * from products")
+ *     public List&lt;ProductWithReviews> loadAll();
+ * }
+ * </pre>
+ * If the query is an async query (e.g. returns a {@link android.arch.lifecycle.LiveData LiveData}
+ * or RxJava Flowable, the transaction is properly handled when the query is run, not when the
+ * method is called.
+ * <p>
+ * Putting this annotation on an {@link Insert}, {@link Update} or {@link Delete} method has no
+ * impact because they are always run inside a transaction. Similarly, if it is annotated with
+ * {@link Query} but runs an update or delete statement, it is automatically wrapped in a
+ * transaction.
  */
 @Target({ElementType.METHOD})
 @Retention(RetentionPolicy.CLASS)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
index 2093d91..c705eef 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/DaoProcessor.kt
@@ -110,9 +110,10 @@
                     executableElement = it).process()
         } ?: emptyList()
 
-        val transactionMethods = allMembers.filter {
-            it.hasAnnotation(Transaction::class)
-                    && it.kind == ElementKind.METHOD
+        val transactionMethods = allMembers.filter { member ->
+            member.hasAnnotation(Transaction::class)
+                    && member.kind == ElementKind.METHOD
+                    && PROCESSED_ANNOTATIONS.none { member.hasAnnotation(it) }
             // TODO: Exclude abstract methods and let @Query handle that case
         }.map {
             TransactionMethodProcessor(
@@ -161,5 +162,4 @@
                     element.toString(), dbType.toString()))
         }
     }
-
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
index be26297..4752478 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
@@ -37,6 +37,8 @@
     val INVALID_ON_CONFLICT_VALUE = "On conflict value must be one of @OnConflictStrategy values."
     val INVALID_INSERTION_METHOD_RETURN_TYPE = "Methods annotated with @Insert can return either" +
             " void, long, Long, long[], Long[] or List<Long>."
+    val TRANSACTION_REFERENCE_DOCS = "https://developer.android.com/reference/android/arch/" +
+            "persistence/room/Transaction.html"
 
     fun insertionMethodReturnTypeMismatch(definedReturn : TypeName,
                                           expectedReturnTypes : List<TypeName>) : String {
@@ -122,7 +124,13 @@
             " @Update but does not have any parameters to update."
 
     val TRANSACTION_METHOD_MODIFIERS = "Method annotated with @Transaction must not be " +
-            "private, final, or abstract."
+            "private, final, or abstract. It can be abstract only if the method is also" +
+            " annotated with @Query."
+
+    val TRANSACTION_MISSING_ON_RELATION = "The return value includes a Pojo with a @Relation." +
+            " It is usually desired to annotate this method with @Transaction to avoid" +
+            " possibility of inconsistent results between the Pojo and its relations. See " +
+            TRANSACTION_REFERENCE_DOCS + " for details."
 
     val CANNOT_FIND_ENTITY_FOR_SHORTCUT_QUERY_PARAMETER = "Type of the parameter must be a class " +
             "annotated with @Entity or a collection/array of it."
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
index fc60074..ebd586a 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/QueryMethodProcessor.kt
@@ -18,15 +18,18 @@
 
 import android.arch.persistence.room.Query
 import android.arch.persistence.room.SkipQueryVerification
+import android.arch.persistence.room.Transaction
 import android.arch.persistence.room.ext.hasAnnotation
 import android.arch.persistence.room.parser.ParsedQuery
 import android.arch.persistence.room.parser.QueryType
 import android.arch.persistence.room.parser.SqlParser
 import android.arch.persistence.room.solver.query.result.LiveDataQueryResultBinder
+import android.arch.persistence.room.solver.query.result.PojoRowAdapter
 import android.arch.persistence.room.verifier.DatabaseVerificaitonErrors
 import android.arch.persistence.room.verifier.DatabaseVerifier
 import android.arch.persistence.room.vo.QueryMethod
 import android.arch.persistence.room.vo.QueryParameter
+import android.arch.persistence.room.vo.Warning
 import com.google.auto.common.AnnotationMirrors
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
@@ -91,6 +94,22 @@
                     ProcessorErrors.LIVE_DATA_QUERY_WITHOUT_SELECT)
         }
 
+        val inTransaction = when (query.type) {
+            QueryType.SELECT -> executableElement.hasAnnotation(Transaction::class)
+            else -> true
+        }
+
+        if (query.type == QueryType.SELECT && !inTransaction) {
+            // put a warning if it is has relations and not annotated w/ transaction
+            resultBinder.adapter?.rowAdapter?.let { rowAdapter ->
+                if (rowAdapter is PojoRowAdapter
+                        && rowAdapter.relationCollectors.isNotEmpty()) {
+                    context.logger.w(Warning.RELATION_QUERY_WITHOUT_TRANSACTION,
+                            executableElement, ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+                }
+            }
+        }
+
         val queryMethod = QueryMethod(
                 element = executableElement,
                 query = query,
@@ -101,6 +120,7 @@
                                 baseContext = context,
                                 containing = containing,
                                 element = it).process() },
+                inTransaction = inTransaction,
                 queryResultBinder = resultBinder)
 
         val missing = queryMethod.sectionToParamMapping
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
index f861ebe..256304a 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/TransactionMethodProcessor.kt
@@ -20,9 +20,9 @@
 import android.arch.persistence.room.vo.TransactionMethod
 import javax.lang.model.element.ExecutableElement
 import javax.lang.model.element.Modifier.ABSTRACT
-import javax.lang.model.type.DeclaredType
 import javax.lang.model.element.Modifier.FINAL
 import javax.lang.model.element.Modifier.PRIVATE
+import javax.lang.model.type.DeclaredType
 
 class TransactionMethodProcessor(baseContext: Context,
                                  val containing: DeclaredType,
@@ -31,7 +31,6 @@
     val context = baseContext.fork(executableElement)
 
     fun process(): TransactionMethod {
-        // TODO: Remove abstract check
         context.checker.check(!executableElement.hasAnyOf(PRIVATE, FINAL, ABSTRACT),
                 executableElement, ProcessorErrors.TRANSACTION_METHOD_MODIFIERS)
 
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
index 84c71eb..0bd3fb1 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/RxCallableQueryResultBinderProvider.kt
@@ -38,8 +38,7 @@
     override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
         val typeArg = declared.typeArguments.first()
         val adapter = context.typeAdapterStore.findQueryResultAdapter(typeArg, query)
-        return RxCallableQueryResultBinder(rxType,
-                typeArg, InstantQueryResultBinder(adapter), adapter)
+        return RxCallableQueryResultBinder(rxType, typeArg, adapter)
     }
 
     override fun matches(declared: DeclaredType): Boolean =
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
index 7872c33..883180c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/BaseObservableQueryResultBinder.kt
@@ -22,6 +22,7 @@
 import android.arch.persistence.room.ext.T
 import android.arch.persistence.room.solver.CodeGenScope
 import android.arch.persistence.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
 import com.squareup.javapoet.MethodSpec
 import javax.lang.model.element.Modifier
 
@@ -42,9 +43,17 @@
 
     protected fun createRunQueryAndReturnStatements(builder: MethodSpec.Builder,
                                                     roomSQLiteQueryVar: String,
+                                                    dbField: FieldSpec,
+                                                    inTransaction: Boolean,
                                                     scope: CodeGenScope) {
+        val transactionWrapper = if (inTransaction) {
+            builder.transactionWrapper(dbField)
+        } else {
+            null
+        }
         val outVar = scope.getTmpVar("_result")
         val cursorVar = scope.getTmpVar("_cursor")
+        transactionWrapper?.beginTransactionWithControlFlow()
         builder.apply {
             addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
                     DaoWriter.dbField, roomSQLiteQueryVar)
@@ -52,6 +61,7 @@
                 val adapterScope = scope.fork()
                 adapter?.convert(outVar, cursorVar, adapterScope)
                 addCode(adapterScope.builder().build())
+                transactionWrapper?.commitTransaction()
                 addStatement("return $L", outVar)
             }
             nextControlFlow("finally").apply {
@@ -59,5 +69,6 @@
             }
             endControlFlow()
         }
+        transactionWrapper?.endTransactionWithControlFlow()
     }
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
index cd8ea74..06ec339 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/CursorQueryResultBinder.kt
@@ -16,19 +16,34 @@
 
 package android.arch.persistence.room.solver.query.result
 
+import android.arch.persistence.room.ext.AndroidTypeNames
 import android.arch.persistence.room.ext.L
 import android.arch.persistence.room.ext.N
+import android.arch.persistence.room.ext.T
 import android.arch.persistence.room.solver.CodeGenScope
 import android.arch.persistence.room.writer.DaoWriter
 import com.squareup.javapoet.FieldSpec
 
 class CursorQueryResultBinder : QueryResultBinder(NO_OP_RESULT_ADAPTER) {
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
-        scope.builder().apply {
-            addStatement("return $N.query($L)", DaoWriter.dbField, roomSQLiteQueryVar)
+        val builder = scope.builder()
+        val transactionWrapper = if (inTransaction) {
+            builder.transactionWrapper(dbField)
+        } else {
+            null
         }
+        transactionWrapper?.beginTransactionWithControlFlow()
+        val resultName = scope.getTmpVar("_tmpResult")
+        builder.addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, resultName,
+                dbField, roomSQLiteQueryVar)
+        transactionWrapper?.commitTransaction()
+        builder.addStatement("return $L", resultName)
+        transactionWrapper?.endTransactionWithControlFlow()
     }
+
     companion object {
         private val NO_OP_RESULT_ADAPTER = object : QueryResultAdapter(null) {
             override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
index 5c8e3e7..e1a75b5 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/FlowableQueryResultBinder.kt
@@ -37,7 +37,9 @@
 class FlowableQueryResultBinder(val typeArg: TypeMirror, val queryTableNames: Set<String>,
                                 adapter: QueryResultAdapter?)
     : BaseObservableQueryResultBinder(adapter) {
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         val callableImpl = TypeSpec.anonymousClassBuilder("").apply {
             val typeName = typeArg.typeName()
@@ -47,7 +49,11 @@
                 returns(typeName)
                 addException(Exception::class.typeName())
                 addModifiers(Modifier.PUBLIC)
-                createRunQueryAndReturnStatements(this, roomSQLiteQueryVar, scope)
+                createRunQueryAndReturnStatements(builder = this,
+                        roomSQLiteQueryVar = roomSQLiteQueryVar,
+                        inTransaction = inTransaction,
+                        dbField = dbField,
+                        scope = scope)
             }.build())
             addMethod(createFinalizeMethod(roomSQLiteQueryVar))
         }.build()
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
index ff15252..c18bf2d 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/InstantQueryResultBinder.kt
@@ -27,8 +27,16 @@
  * Instantly runs and returns the query.
  */
 class InstantQueryResultBinder(adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
-    override fun convertAndReturn(roomSQLiteQueryVar : String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar : String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
+        val transactionWrapper = if (inTransaction) {
+            scope.builder().transactionWrapper(dbField)
+        } else {
+            null
+        }
+        transactionWrapper?.beginTransactionWithControlFlow()
         scope.builder().apply {
             val outVar = scope.getTmpVar("_result")
             val cursorVar = scope.getTmpVar("_cursor")
@@ -36,6 +44,7 @@
                     DaoWriter.dbField, roomSQLiteQueryVar)
             beginControlFlow("try").apply {
                 adapter?.convert(outVar, cursorVar, scope)
+                transactionWrapper?.commitTransaction()
                 addStatement("return $L", outVar)
             }
             nextControlFlow("finally").apply {
@@ -44,5 +53,6 @@
             }
             endControlFlow()
         }
+        transactionWrapper?.endTransactionWithControlFlow()
     }
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
index a385a94..0ef8a93 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LiveDataQueryResultBinder.kt
@@ -40,7 +40,9 @@
                                 adapter: QueryResultAdapter?)
     : BaseObservableQueryResultBinder(adapter) {
     @Suppress("JoinDeclarationAndAssignment")
-    override fun convertAndReturn(roomSQLiteQueryVar : String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar : String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         val typeName = typeArg.typeName()
 
@@ -55,6 +57,7 @@
                     typeName = typeName,
                     roomSQLiteQueryVar = roomSQLiteQueryVar,
                     dbField = dbField,
+                    inTransaction = inTransaction,
                     scope = scope
             ))
             addMethod(createFinalizeMethod(roomSQLiteQueryVar))
@@ -66,6 +69,7 @@
 
     private fun createComputeMethod(roomSQLiteQueryVar: String, typeName: TypeName,
                                     observerField: FieldSpec, dbField: FieldSpec,
+                                    inTransaction: Boolean,
                                     scope: CodeGenScope): MethodSpec {
         return MethodSpec.methodBuilder("compute").apply {
             addAnnotation(Override::class.java)
@@ -79,7 +83,11 @@
             }
             endControlFlow()
 
-            createRunQueryAndReturnStatements(this, roomSQLiteQueryVar, scope)
+            createRunQueryAndReturnStatements(builder = this,
+                    roomSQLiteQueryVar = roomSQLiteQueryVar,
+                    dbField = dbField,
+                    inTransaction = inTransaction,
+                    scope = scope)
         }.build()
     }
 
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
index 39ea32b..c10f9fb 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/LivePagedListQueryResultBinder.kt
@@ -31,14 +31,20 @@
     : QueryResultBinder(tiledDataSourceQueryResultBinder.listAdapter) {
     @Suppress("HasPlatformType")
     val typeName = tiledDataSourceQueryResultBinder.itemTypeName
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         scope.builder().apply {
             val pagedListProvider = TypeSpec
                     .anonymousClassBuilder("").apply {
                 superclass(ParameterizedTypeName.get(PagingTypeNames.LIVE_PAGED_LIST_PROVIDER,
                         Integer::class.typeName(), typeName))
-                addMethod(createCreateDataSourceMethod(roomSQLiteQueryVar, dbField, scope))
+                addMethod(createCreateDataSourceMethod(
+                        roomSQLiteQueryVar = roomSQLiteQueryVar,
+                        dbField = dbField,
+                        inTransaction = inTransaction,
+                        scope = scope))
             }.build()
             addStatement("return $L", pagedListProvider)
         }
@@ -46,14 +52,18 @@
 
     private fun createCreateDataSourceMethod(roomSQLiteQueryVar: String,
                                              dbField: FieldSpec,
+                                             inTransaction : Boolean,
                                              scope: CodeGenScope): MethodSpec
             = MethodSpec.methodBuilder("createDataSource").apply {
         addAnnotation(Override::class.java)
         addModifiers(Modifier.PROTECTED)
         returns(tiledDataSourceQueryResultBinder.typeName)
         val countedBinderScope = scope.fork()
-        tiledDataSourceQueryResultBinder.convertAndReturn(roomSQLiteQueryVar, dbField,
-                countedBinderScope)
+        tiledDataSourceQueryResultBinder.convertAndReturn(
+                roomSQLiteQueryVar = roomSQLiteQueryVar,
+                dbField = dbField,
+                inTransaction = inTransaction,
+                scope = countedBinderScope)
         addCode(countedBinderScope.builder().build())
     }.build()
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
index 205bd88..652de46 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/QueryResultBinder.kt
@@ -32,5 +32,7 @@
      * and returns the result.
      */
     abstract fun convertAndReturn(roomSQLiteQueryVar: String,
-                                  dbField: FieldSpec, scope: CodeGenScope)
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
+                                  scope: CodeGenScope)
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
index da960f6..1ab91e8 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/RxCallableQueryResultBinder.kt
@@ -38,28 +38,42 @@
  */
 class RxCallableQueryResultBinder(val rxType: RxType,
                                   val typeArg: TypeMirror,
-                                  val instantBinder : InstantQueryResultBinder,
-                                  adapter: QueryResultAdapter?) : QueryResultBinder(adapter) {
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+                                  adapter: QueryResultAdapter?)
+    : QueryResultBinder(adapter) {
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         val callable = TypeSpec.anonymousClassBuilder("").apply {
             val typeName = typeArg.typeName()
             superclass(ParameterizedTypeName.get(java.util.concurrent.Callable::class.typeName(),
                     typeName))
-            addMethod(createCallMethod(roomSQLiteQueryVar, dbField, scope))
+            addMethod(createCallMethod(
+                    roomSQLiteQueryVar = roomSQLiteQueryVar,
+                    dbField = dbField,
+                    inTransaction = inTransaction,
+                    scope = scope))
         }.build()
         scope.builder().apply {
             addStatement("return $T.fromCallable($L)", rxType.className, callable)
         }
     }
 
-    fun createCallMethod(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    fun createCallMethod(roomSQLiteQueryVar: String,
+                         dbField: FieldSpec,
+                         inTransaction: Boolean,
                          scope: CodeGenScope): MethodSpec {
         val adapterScope = scope.fork()
         return MethodSpec.methodBuilder("call").apply {
             returns(typeArg.typeName())
             addException(Exception::class.typeName())
             addModifiers(Modifier.PUBLIC)
+            val transactionWrapper = if (inTransaction) {
+                transactionWrapper(dbField)
+            } else {
+                null
+            }
+            transactionWrapper?.beginTransactionWithControlFlow()
             val outVar = scope.getTmpVar("_result")
             val cursorVar = scope.getTmpVar("_cursor")
             addStatement("final $T $L = $N.query($L)", AndroidTypeNames.CURSOR, cursorVar,
@@ -76,6 +90,7 @@
                     }
                     endControlFlow()
                 }
+                transactionWrapper?.commitTransaction()
                 addStatement("return $L", outVar)
             }
             nextControlFlow("finally").apply {
@@ -83,10 +98,11 @@
                 addStatement("$L.release()", roomSQLiteQueryVar)
             }
             endControlFlow()
+            transactionWrapper?.endTransactionWithControlFlow()
         }.build()
     }
 
-    enum class RxType(val className : ClassName, val canBeNull : Boolean) {
+    enum class RxType(val className: ClassName, val canBeNull: Boolean) {
         SINGLE(RxJava2TypeNames.SINGLE, canBeNull = false),
         MAYBE(RxJava2TypeNames.MAYBE, canBeNull = true);
     }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
index 1b4b3fe..2281cfb 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TiledDataSourceQueryResultBinder.kt
@@ -37,11 +37,13 @@
     val itemTypeName : TypeName = listAdapter?.rowAdapter?.out?.typeName() ?: TypeName.OBJECT
     val typeName : ParameterizedTypeName = ParameterizedTypeName.get(
             RoomTypeNames.LIMIT_OFFSET_DATA_SOURCE, itemTypeName)
-    override fun convertAndReturn(roomSQLiteQueryVar: String, dbField: FieldSpec,
+    override fun convertAndReturn(roomSQLiteQueryVar: String,
+                                  dbField: FieldSpec,
+                                  inTransaction : Boolean,
                                   scope: CodeGenScope) {
         val tableNamesList = tableNames.joinToString(",") { "\"$it\"" }
-        val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L",
-                dbField, roomSQLiteQueryVar, tableNamesList).apply {
+        val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L, $L",
+                dbField, roomSQLiteQueryVar, inTransaction, tableNamesList).apply {
             superclass(typeName)
             addMethod(createConvertRowsMethod(scope))
         }.build()
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt
new file mode 100644
index 0000000..30d02ee
--- /dev/null
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/TransactionWrapper.kt
@@ -0,0 +1,65 @@
+/*
+ * 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 android.arch.persistence.room.solver.query.result
+
+import android.arch.persistence.room.ext.N
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.MethodSpec
+
+/**
+ * helper class to create correct transaction code.
+ */
+interface TransactionWrapper {
+    fun beginTransactionWithControlFlow()
+    fun commitTransaction()
+    fun endTransactionWithControlFlow()
+}
+
+fun MethodSpec.Builder.transactionWrapper(dbField : FieldSpec) = object : TransactionWrapper {
+    override fun beginTransactionWithControlFlow() {
+        addStatement("$N.beginTransaction()", dbField)
+        beginControlFlow("try")
+    }
+
+    override fun commitTransaction() {
+        addStatement("$N.setTransactionSuccessful()", dbField)
+    }
+
+    override fun endTransactionWithControlFlow() {
+        nextControlFlow("finally")
+        addStatement("$N.endTransaction()", dbField)
+        endControlFlow()
+    }
+}
+
+fun CodeBlock.Builder.transactionWrapper(dbField: FieldSpec) = object : TransactionWrapper {
+    override fun beginTransactionWithControlFlow() {
+        addStatement("$N.beginTransaction()", dbField)
+        beginControlFlow("try")
+    }
+
+    override fun commitTransaction() {
+        addStatement("$N.setTransactionSuccessful()", dbField)
+    }
+
+    override fun endTransactionWithControlFlow() {
+        nextControlFlow("finally")
+        addStatement("$N.endTransaction()", dbField)
+        endControlFlow()
+    }
+}
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
index 1aa8430..a6b8cb7 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/QueryMethod.kt
@@ -29,6 +29,7 @@
  */
 data class QueryMethod(val element: ExecutableElement, val query: ParsedQuery, val name: String,
                        val returnType: TypeMirror, val parameters: List<QueryParameter>,
+                       val inTransaction : Boolean,
                        val queryResultBinder : QueryResultBinder) {
     val sectionToParamMapping by lazy {
         query.bindSections.map {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
index 91bcb33..dd154c4 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/vo/Warning.kt
@@ -32,6 +32,7 @@
     RELATION_TYPE_MISMATCH("ROOM_RELATION_TYPE_MISMATCH"),
     MISSING_SCHEMA_LOCATION("ROOM_MISSING_SCHEMA_LOCATION"),
     MISSING_INDEX_ON_FOREIGN_KEY_CHILD("ROOM_MISSING_FOREIGN_KEY_CHILD_INDEX"),
+    RELATION_QUERY_WITHOUT_TRANSACTION("ROOM_RELATION_QUERY_WITHOUT_TRANSACTION"),
     DEFAULT_CONSTRUCTOR("ROOM_DEFAULT_CONSTRUCTOR");
 
     companion object {
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
index 5c7d8a3..0bb9c19 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/writer/DaoWriter.kt
@@ -445,7 +445,11 @@
         val sqlVar = scope.getTmpVar("_sql")
         val roomSQLiteQueryVar = scope.getTmpVar("_statement")
         queryWriter.prepareReadAndBind(sqlVar, roomSQLiteQueryVar, scope)
-        method.queryResultBinder.convertAndReturn(roomSQLiteQueryVar, dbField, scope)
+        method.queryResultBinder.convertAndReturn(
+                roomSQLiteQueryVar = roomSQLiteQueryVar,
+                dbField = dbField,
+                inTransaction = method.inTransaction,
+                scope = scope)
         return scope.builder().build()
     }
 
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
index 3ade277..55f335d 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/DaoProcessorTest.kt
@@ -192,6 +192,81 @@
         }.compilesWithoutError()
     }
 
+    @Test
+    fun query_warnIfTransactionIsMissingForRelation() {
+        if (!enableVerification) {
+            return
+        }
+        singleDao(
+                """
+                @Dao interface MyDao {
+                    static class Merged extends User {
+                       @Relation(parentColumn = "name", entityColumn = "lastName",
+                                 entity = User.class)
+                       java.util.List<User> users;
+                    }
+                    @Query("select * from user")
+                    abstract java.util.List<Merged> loadUsers();
+                }
+                """
+        ) { dao, _ ->
+            assertThat(dao.queryMethods.size, `is`(1))
+            assertThat(dao.queryMethods.first().inTransaction, `is`(false))
+        }.compilesWithoutError()
+                .withWarningContaining(ProcessorErrors.TRANSACTION_MISSING_ON_RELATION)
+    }
+
+    @Test
+    fun query_dontWarnIfTransactionIsMissingForRelation_suppressed() {
+        if (!enableVerification) {
+            return
+        }
+        singleDao(
+                """
+                @Dao interface MyDao {
+                    static class Merged extends User {
+                       @Relation(parentColumn = "name", entityColumn = "lastName",
+                                 entity = User.class)
+                       java.util.List<User> users;
+                    }
+                    @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
+                    @Query("select * from user")
+                    abstract java.util.List<Merged> loadUsers();
+                }
+                """
+        ) { dao, _ ->
+            assertThat(dao.queryMethods.size, `is`(1))
+            assertThat(dao.queryMethods.first().inTransaction, `is`(false))
+        }.compilesWithoutError()
+                .withWarningCount(0)
+    }
+
+    @Test
+    fun query_dontWarnIfTransactionNotIsMissingForRelation() {
+        if (!enableVerification) {
+            return
+        }
+        singleDao(
+                """
+                @Dao interface MyDao {
+                    static class Merged extends User {
+                       @Relation(parentColumn = "name", entityColumn = "lastName",
+                                 entity = User.class)
+                       java.util.List<User> users;
+                    }
+                    @Transaction
+                    @Query("select * from user")
+                    abstract java.util.List<Merged> loadUsers();
+                }
+                """
+        ) { dao, _ ->
+            // test sanity
+            assertThat(dao.queryMethods.size, `is`(1))
+            assertThat(dao.queryMethods.first().inTransaction, `is`(true))
+        }.compilesWithoutError()
+                .withWarningCount(0)
+    }
+
     fun singleDao(vararg inputs: String, handler: (Dao, TestInvocation) -> Unit):
             CompileTester {
         return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
@@ -200,7 +275,12 @@
                 ), COMMON.USER))
                 .processedWith(TestProcessor.builder()
                         .forAnnotations(android.arch.persistence.room.Dao::class,
-                                android.arch.persistence.room.Entity::class)
+                                android.arch.persistence.room.Entity::class,
+                                android.arch.persistence.room.Relation::class,
+                                android.arch.persistence.room.Transaction::class,
+                                android.arch.persistence.room.ColumnInfo::class,
+                                android.arch.persistence.room.PrimaryKey::class,
+                                android.arch.persistence.room.Query::class)
                         .nextRunHandler { invocation ->
                             val dao = invocation.roundEnv
                                     .getElementsAnnotatedWith(
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
index a2cb768..e07e04d 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/QueryMethodProcessorTest.kt
@@ -445,6 +445,55 @@
     }
 
     @Test
+    fun query_detectTransaction_delete() {
+        singleQueryMethod(
+                """
+                @Query("delete from user where uid = :id")
+                abstract int deleteUser(String id);
+                """
+        ) { method, _ ->
+            assertThat(method.inTransaction, `is`(true))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun query_detectTransaction_update() {
+        singleQueryMethod(
+                """
+                @Query("UPDATE user set uid = :id + 1 where uid = :id")
+                abstract int incrementId(String id);
+                """
+        ) { method, _ ->
+            assertThat(method.inTransaction, `is`(true))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun query_detectTransaction_select() {
+        singleQueryMethod(
+                """
+                @Query("select * from user")
+                abstract int loadUsers();
+                """
+        ) { method, _ ->
+            assertThat(method.inTransaction, `is`(false))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun query_detectTransaction_selectInTransaction() {
+        singleQueryMethod(
+                """
+                @Transaction
+                @Query("select * from user")
+                abstract int loadUsers();
+                """
+        ) { method, _ ->
+            assertThat(method.inTransaction, `is`(true))
+        }.compilesWithoutError()
+    }
+
+    @Test
     fun skipVerification() {
         singleQueryMethod(
                 """
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
new file mode 100644
index 0000000..dcf98c9
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
@@ -0,0 +1,471 @@
+/*
+ * 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 android.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.core.executor.ArchTaskExecutor;
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.arch.lifecycle.Lifecycle;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.Observer;
+import android.arch.paging.LivePagedListProvider;
+import android.arch.paging.PagedList;
+import android.arch.paging.TiledDataSource;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Entity;
+import android.arch.persistence.room.Ignore;
+import android.arch.persistence.room.Insert;
+import android.arch.persistence.room.PrimaryKey;
+import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Relation;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.RoomWarnings;
+import android.arch.persistence.room.Transaction;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+
+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.Parameterized;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.reactivex.Flowable;
+import io.reactivex.Maybe;
+import io.reactivex.Single;
+import io.reactivex.observers.TestObserver;
+import io.reactivex.schedulers.Schedulers;
+import io.reactivex.subscribers.TestSubscriber;
+
+@SmallTest
+@RunWith(Parameterized.class)
+public class QueryTransactionTest {
+    @Rule
+    public CountingTaskExecutorRule countingTaskExecutorRule = new CountingTaskExecutorRule();
+    private static final AtomicInteger sStartedTransactionCount = new AtomicInteger(0);
+    private TransactionDb mDb;
+    private final boolean mUseTransactionDao;
+    private Entity1Dao mDao;
+    private final LiveDataQueryTest.TestLifecycleOwner mLifecycleOwner = new LiveDataQueryTest
+            .TestLifecycleOwner();
+
+    @NonNull
+    @Parameterized.Parameters(name = "useTransaction_{0}")
+    public static Boolean[] getParams() {
+        return new Boolean[]{false, true};
+    }
+
+    public QueryTransactionTest(boolean useTransactionDao) {
+        mUseTransactionDao = useTransactionDao;
+    }
+
+    @Before
+    public void initDb() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+            }
+        });
+
+        resetTransactionCount();
+        mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+                TransactionDb.class).build();
+        mDao = mUseTransactionDao ? mDb.transactionDao() : mDb.dao();
+        drain();
+    }
+
+    @After
+    public void closeDb() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mLifecycleOwner.handleEvent(Lifecycle.Event.ON_DESTROY);
+            }
+        });
+        drain();
+        mDb.close();
+    }
+
+    @Test
+    public void readList() {
+        mDao.insert(new Entity1(1, "foo"));
+        resetTransactionCount();
+
+        int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+        List<Entity1> allEntities = mDao.allEntities();
+        assertTransactionCount(allEntities, expectedTransactionCount);
+    }
+
+    @Test
+    public void liveData() {
+        LiveData<List<Entity1>> listLiveData = mDao.liveData();
+        observeForever(listLiveData);
+        drain();
+        assertThat(listLiveData.getValue(), is(Collections.<Entity1>emptyList()));
+
+        resetTransactionCount();
+        mDao.insert(new Entity1(1, "foo"));
+        drain();
+
+        //noinspection ConstantConditions
+        assertThat(listLiveData.getValue().size(), is(1));
+        int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
+        assertTransactionCount(listLiveData.getValue(), expectedTransactionCount);
+    }
+
+    @Test
+    public void flowable() {
+        Flowable<List<Entity1>> flowable = mDao.flowable();
+        TestSubscriber<List<Entity1>> subscriber = observe(flowable);
+        drain();
+        assertThat(subscriber.values().size(), is(1));
+
+        resetTransactionCount();
+        mDao.insert(new Entity1(1, "foo"));
+        drain();
+
+        List<Entity1> allEntities = subscriber.values().get(1);
+        assertThat(allEntities.size(), is(1));
+        int expectedTransactionCount = mUseTransactionDao ? 2 : 1;
+        assertTransactionCount(allEntities, expectedTransactionCount);
+    }
+
+    @Test
+    public void maybe() {
+        mDao.insert(new Entity1(1, "foo"));
+        resetTransactionCount();
+
+        int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+        Maybe<List<Entity1>> listMaybe = mDao.maybe();
+        TestObserver<List<Entity1>> observer = observe(listMaybe);
+        drain();
+        List<Entity1> allEntities = observer.values().get(0);
+        assertTransactionCount(allEntities, expectedTransactionCount);
+    }
+
+    @Test
+    public void single() {
+        mDao.insert(new Entity1(1, "foo"));
+        resetTransactionCount();
+
+        int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+        Single<List<Entity1>> listMaybe = mDao.single();
+        TestObserver<List<Entity1>> observer = observe(listMaybe);
+        drain();
+        List<Entity1> allEntities = observer.values().get(0);
+        assertTransactionCount(allEntities, expectedTransactionCount);
+    }
+
+    @Test
+    public void relation() {
+        mDao.insert(new Entity1(1, "foo"));
+        mDao.insert(new Child(1, 1));
+        mDao.insert(new Child(2, 1));
+        resetTransactionCount();
+
+        List<Entity1WithChildren> result = mDao.withRelation();
+        int expectedTransactionCount = mUseTransactionDao ? 1 : 0;
+        assertTransactionCountWithChildren(result, expectedTransactionCount);
+    }
+
+    @Test
+    public void pagedList() {
+        LiveData<PagedList<Entity1>> pagedList = mDao.pagedList().create(null, 10);
+        observeForever(pagedList);
+        drain();
+        assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+
+        mDao.insert(new Entity1(1, "foo"));
+        drain();
+        //noinspection ConstantConditions
+        assertThat(pagedList.getValue().size(), is(1));
+        assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 3 : 1);
+
+        mDao.insert(new Entity1(2, "bar"));
+        drain();
+        assertThat(pagedList.getValue().size(), is(2));
+        assertTransactionCount(pagedList.getValue(), mUseTransactionDao ? 5 : 2);
+    }
+
+    @Test
+    public void dataSource() {
+        mDao.insert(new Entity1(2, "bar"));
+        drain();
+        resetTransactionCount();
+        TiledDataSource<Entity1> dataSource = mDao.dataSource();
+        dataSource.loadRange(0, 10);
+        assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
+    }
+
+    private void assertTransactionCount(List<Entity1> allEntities, int expectedTransactionCount) {
+        assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
+        assertThat(allEntities.isEmpty(), is(false));
+        for (Entity1 entity1 : allEntities) {
+            assertThat(entity1.transactionId, is(expectedTransactionCount));
+        }
+    }
+
+    private void assertTransactionCountWithChildren(List<Entity1WithChildren> allEntities,
+            int expectedTransactionCount) {
+        assertThat(sStartedTransactionCount.get(), is(expectedTransactionCount));
+        assertThat(allEntities.isEmpty(), is(false));
+        for (Entity1WithChildren entity1 : allEntities) {
+            assertThat(entity1.transactionId, is(expectedTransactionCount));
+            assertThat(entity1.children, notNullValue());
+            assertThat(entity1.children.isEmpty(), is(false));
+            for (Child child : entity1.children) {
+                assertThat(child.transactionId, is(expectedTransactionCount));
+            }
+        }
+    }
+
+    private void resetTransactionCount() {
+        sStartedTransactionCount.set(0);
+    }
+
+    private void drain() {
+        try {
+            countingTaskExecutorRule.drainTasks(30, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            throw new AssertionError("interrupted", e);
+        } catch (TimeoutException e) {
+            throw new AssertionError("drain timed out", e);
+        }
+    }
+
+    private <T> TestSubscriber<T> observe(final Flowable<T> flowable) {
+        TestSubscriber<T> subscriber = new TestSubscriber<>();
+        flowable.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+                .subscribeWith(subscriber);
+        return subscriber;
+    }
+
+    private <T> TestObserver<T> observe(final Maybe<T> maybe) {
+        TestObserver<T> observer = new TestObserver<>();
+        maybe.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+                .subscribeWith(observer);
+        return observer;
+    }
+
+    private <T> TestObserver<T> observe(final Single<T> single) {
+        TestObserver<T> observer = new TestObserver<>();
+        single.observeOn(Schedulers.from(ArchTaskExecutor.getMainThreadExecutor()))
+                .subscribeWith(observer);
+        return observer;
+    }
+
+    private <T> void observeForever(final LiveData<T> liveData) {
+        FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                liveData.observe(mLifecycleOwner, new Observer<T>() {
+                    @Override
+                    public void onChanged(@Nullable T t) {
+
+                    }
+                });
+                return null;
+            }
+        });
+        ArchTaskExecutor.getMainThreadExecutor().execute(futureTask);
+        try {
+            futureTask.get();
+        } catch (InterruptedException e) {
+            throw new AssertionError("interrupted", e);
+        } catch (ExecutionException e) {
+            throw new AssertionError("execution error", e);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    static class Entity1WithChildren extends Entity1 {
+        @Relation(entity = Child.class, parentColumn = "id",
+                entityColumn = "entity1Id")
+        public List<Child> children;
+
+        Entity1WithChildren(int id, String value) {
+            super(id, value);
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    @Entity
+    static class Child {
+        @PrimaryKey(autoGenerate = true)
+        public int id;
+        public int entity1Id;
+        @Ignore
+        public final int transactionId = sStartedTransactionCount.get();
+
+        Child(int id, int entity1Id) {
+            this.id = id;
+            this.entity1Id = entity1Id;
+        }
+    }
+
+    @SuppressWarnings("WeakerAccess")
+    @Entity
+    static class Entity1 {
+        @PrimaryKey(autoGenerate = true)
+        public int id;
+        public String value;
+        @Ignore
+        public final int transactionId = sStartedTransactionCount.get();
+
+        Entity1(int id, String value) {
+            this.id = id;
+            this.value = value;
+        }
+    }
+
+    // we don't support dao inheritance for queries so for now, go with this
+    interface Entity1Dao {
+        String SELECT_ALL = "select * from Entity1";
+
+        List<Entity1> allEntities();
+
+        Flowable<List<Entity1>> flowable();
+
+        Maybe<List<Entity1>> maybe();
+
+        Single<List<Entity1>> single();
+
+        LiveData<List<Entity1>> liveData();
+
+        List<Entity1WithChildren> withRelation();
+
+        LivePagedListProvider<Integer, Entity1> pagedList();
+
+        TiledDataSource<Entity1> dataSource();
+
+        @Insert
+        void insert(Entity1 entity1);
+
+        @Insert
+        void insert(Child entity1);
+    }
+
+    @Dao
+    interface EntityDao extends Entity1Dao {
+        @Override
+        @Query(SELECT_ALL)
+        List<Entity1> allEntities();
+
+        @Override
+        @Query(SELECT_ALL)
+        Flowable<List<Entity1>> flowable();
+
+        @Override
+        @Query(SELECT_ALL)
+        LiveData<List<Entity1>> liveData();
+
+        @Override
+        @Query(SELECT_ALL)
+        Maybe<List<Entity1>> maybe();
+
+        @Override
+        @Query(SELECT_ALL)
+        Single<List<Entity1>> single();
+
+        @Override
+        @Query(SELECT_ALL)
+        @SuppressWarnings(RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION)
+        List<Entity1WithChildren> withRelation();
+
+        @Override
+        @Query(SELECT_ALL)
+        LivePagedListProvider<Integer, Entity1> pagedList();
+
+        @Override
+        @Query(SELECT_ALL)
+        TiledDataSource<Entity1> dataSource();
+    }
+
+    @Dao
+    interface TransactionDao extends Entity1Dao {
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        List<Entity1> allEntities();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        Flowable<List<Entity1>> flowable();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        LiveData<List<Entity1>> liveData();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        Maybe<List<Entity1>> maybe();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        Single<List<Entity1>> single();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        List<Entity1WithChildren> withRelation();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        LivePagedListProvider<Integer, Entity1> pagedList();
+
+        @Override
+        @Transaction
+        @Query(SELECT_ALL)
+        TiledDataSource<Entity1> dataSource();
+    }
+
+    @Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false)
+    abstract static class TransactionDb extends RoomDatabase {
+        abstract EntityDao dao();
+
+        abstract TransactionDao transactionDao();
+
+        @Override
+        public void beginTransaction() {
+            super.beginTransaction();
+            sStartedTransactionCount.incrementAndGet();
+        }
+    }
+}
diff --git a/room/integration-tests/testapp/src/test/java/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java b/room/integration-tests/testapp/src/test/java/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java
deleted file mode 100644
index 3cbffc8..0000000
--- a/room/integration-tests/testapp/src/test/java/android/arch/persistence/room/integration/testapp/db/JDBCOpenHelper.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 android.arch.persistence.room.integration.testapp.db;
-
-import android.arch.persistence.db.SupportSQLiteDatabase;
-import android.arch.persistence.db.SupportSQLiteOpenHelper;
-
-public class JDBCOpenHelper implements SupportSQLiteOpenHelper {
-    @Override
-    public String getDatabaseName() {
-        return null;
-    }
-
-    @Override
-    public void setWriteAheadLoggingEnabled(boolean enabled) {
-
-    }
-
-    @Override
-    public SupportSQLiteDatabase getWritableDatabase() {
-        return null;
-    }
-
-    @Override
-    public SupportSQLiteDatabase getReadableDatabase() {
-        return null;
-    }
-
-    @Override
-    public void close() {
-
-    }
-}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
index 800514c..2f9a888 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
@@ -49,10 +49,13 @@
     private final RoomDatabase mDb;
     @SuppressWarnings("FieldCanBeLocal")
     private final InvalidationTracker.Observer mObserver;
+    private final boolean mInTransaction;
 
-    protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query, String... tables) {
+    protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
+            boolean inTransaction, String... tables) {
         mDb = db;
         mSourceQuery = query;
+        mInTransaction = inTransaction;
         mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
         mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
         mObserver = new InvalidationTracker.Observer(tables) {
@@ -98,13 +101,30 @@
         sqLiteQuery.copyArgumentsFrom(mSourceQuery);
         sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
         sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
-        Cursor cursor = mDb.query(sqLiteQuery);
-
-        try {
-            return convertRows(cursor);
-        } finally {
-            cursor.close();
-            sqLiteQuery.release();
+        if (mInTransaction) {
+            mDb.beginTransaction();
+            Cursor cursor = null;
+            try {
+                cursor = mDb.query(sqLiteQuery);
+                List<T> rows = convertRows(cursor);
+                mDb.setTransactionSuccessful();
+                return rows;
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+                mDb.endTransaction();
+                sqLiteQuery.release();
+            }
+        } else {
+            Cursor cursor = mDb.query(sqLiteQuery);
+            //noinspection TryFinallyCanBeTryWithResources
+            try {
+                return convertRows(cursor);
+            } finally {
+                cursor.close();
+                sqLiteQuery.release();
+            }
         }
     }
 }
diff --git a/tv-provider/api/current.txt b/tv-provider/api/current.txt
index 42cad9f..80421e9 100644
--- a/tv-provider/api/current.txt
+++ b/tv-provider/api/current.txt
@@ -531,6 +531,7 @@
     method public int getWatchNextType();
     method public android.content.ContentValues toContentValues();
     method public java.lang.String toString();
+    field public static final int WATCH_NEXT_TYPE_UNKNOWN = -1; // 0xffffffff
   }
 
   public static final class WatchNextProgram.Builder {
diff --git a/tv-provider/lint-baseline.xml b/tv-provider/lint-baseline.xml
index 9814796..4387a5a 100644
--- a/tv-provider/lint-baseline.xml
+++ b/tv-provider/lint-baseline.xml
@@ -1,92 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="4" by="lint 3.0.0-beta6">
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.TYPE_MOVIE, PreviewProgramColumns.TYPE_TV_SERIES, PreviewProgramColumns.TYPE_TV_SEASON, PreviewProgramColumns.TYPE_TV_EPISODE, PreviewProgramColumns.TYPE_CLIP, PreviewProgramColumns.TYPE_EVENT, PreviewProgramColumns.TYPE_CHANNEL, PreviewProgramColumns.TYPE_TRACK, PreviewProgramColumns.TYPE_ALBUM, PreviewProgramColumns.TYPE_ARTIST, PreviewProgramColumns.TYPE_PLAYLIST, PreviewProgramColumns.TYPE_STATION, PreviewProgramColumns.TYPE_GAME"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="130"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.ASPECT_RATIO_16_9, PreviewProgramColumns.ASPECT_RATIO_3_2, PreviewProgramColumns.ASPECT_RATIO_4_3, PreviewProgramColumns.ASPECT_RATIO_1_1, PreviewProgramColumns.ASPECT_RATIO_2_3, PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="140"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.ASPECT_RATIO_16_9, PreviewProgramColumns.ASPECT_RATIO_3_2, PreviewProgramColumns.ASPECT_RATIO_4_3, PreviewProgramColumns.ASPECT_RATIO_1_1, PreviewProgramColumns.ASPECT_RATIO_2_3, PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="150"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.AVAILABILITY_AVAILABLE, PreviewProgramColumns.AVAILABILITY_FREE_WITH_SUBSCRIPTION, PreviewProgramColumns.AVAILABILITY_PAID_CONTENT, PreviewProgramColumns.AVAILABILITY_PURCHASED, PreviewProgramColumns.AVAILABILITY_FREE"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="168"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: PreviewProgramColumns.INTERACTION_TYPE_VIEWS, PreviewProgramColumns.INTERACTION_TYPE_LISTENS, PreviewProgramColumns.INTERACTION_TYPE_FOLLOWERS, PreviewProgramColumns.INTERACTION_TYPE_FANS, PreviewProgramColumns.INTERACTION_TYPE_LIKES, PreviewProgramColumns.INTERACTION_TYPE_THUMBS, PreviewProgramColumns.INTERACTION_TYPE_VIEWERS"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BasePreviewProgram.java"
-            line="219"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: ProgramColumns.REVIEW_RATING_STYLE_STARS, ProgramColumns.REVIEW_RATING_STYLE_THUMBS_UP_DOWN, ProgramColumns.REVIEW_RATING_STYLE_PERCENTAGE"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/BaseProgram.java"
-            line="257"
-            column="28"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: Genres.FAMILY_KIDS, Genres.SPORTS, Genres.SHOPPING, Genres.MOVIES, Genres.COMEDY, Genres.TRAVEL, Genres.DRAMA, Genres.EDUCATION, Genres.ANIMAL_WILDLIFE, Genres.NEWS, Genres.GAMING, Genres.ARTS, Genres.ENTERTAINMENT, Genres.LIFE_STYLE, Genres.MUSIC, Genres.PREMIER, Genres.TECH_SCIENCE"
-        errorLine1="            mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres));"
-        errorLine2="                                                                                ~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/Program.java"
-            line="286"
-            column="81"/>
-    </issue>
-
-    <issue
-        id="WrongConstant"
-        message="Must be one of: WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE, WatchNextPrograms.WATCH_NEXT_TYPE_NEXT, WatchNextPrograms.WATCH_NEXT_TYPE_NEW, WatchNextPrograms.WATCH_NEXT_TYPE_WATCHLIST"
-        errorLine1="        return i == null ? INVALID_INT_VALUE : i;"
-        errorLine2="                           ~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/android/support/media/tv/WatchNextProgram.java"
-            line="99"
-            column="28"/>
-    </issue>
+<issues format="4" by="lint 3.0.0-beta7">
 
 </issues>
diff --git a/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java b/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
index 1423d9d..39c3014 100644
--- a/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/BasePreviewProgram.java
@@ -23,14 +23,13 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build;
+import android.support.annotation.IntDef;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.PreviewProgramColumns;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.AspectRatio;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Availability;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.InteractionType;
-import android.support.media.tv.TvContractCompat.PreviewProgramColumns.Type;
 import android.support.media.tv.TvContractCompat.PreviewPrograms;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.net.URISyntaxException;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -55,6 +54,89 @@
     private static final int IS_LIVE = 1;
     private static final int IS_BROWSABLE = 1;
 
+    /** @hide */
+    @IntDef({
+            TYPE_UNKNOWN,
+            PreviewProgramColumns.TYPE_MOVIE,
+            PreviewProgramColumns.TYPE_TV_SERIES,
+            PreviewProgramColumns.TYPE_TV_SEASON,
+            PreviewProgramColumns.TYPE_TV_EPISODE,
+            PreviewProgramColumns.TYPE_CLIP,
+            PreviewProgramColumns.TYPE_EVENT,
+            PreviewProgramColumns.TYPE_CHANNEL,
+            PreviewProgramColumns.TYPE_TRACK,
+            PreviewProgramColumns.TYPE_ALBUM,
+            PreviewProgramColumns.TYPE_ARTIST,
+            PreviewProgramColumns.TYPE_PLAYLIST,
+            PreviewProgramColumns.TYPE_STATION,
+            PreviewProgramColumns.TYPE_GAME
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface Type {}
+
+    /**
+     * The unknown program type.
+     */
+    private static final int TYPE_UNKNOWN = -1;
+
+    /** @hide */
+    @IntDef({
+            ASPECT_RATIO_UNKNOWN,
+            PreviewProgramColumns.ASPECT_RATIO_16_9,
+            PreviewProgramColumns.ASPECT_RATIO_3_2,
+            PreviewProgramColumns.ASPECT_RATIO_4_3,
+            PreviewProgramColumns.ASPECT_RATIO_1_1,
+            PreviewProgramColumns.ASPECT_RATIO_2_3,
+            PreviewProgramColumns.ASPECT_RATIO_MOVIE_POSTER
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface AspectRatio {}
+
+    /**
+     * The aspect ratio for unknown aspect ratios.
+     */
+    private static final int ASPECT_RATIO_UNKNOWN = -1;
+
+    /** @hide */
+    @IntDef({
+            AVAILABILITY_UNKNOWN,
+            PreviewProgramColumns.AVAILABILITY_AVAILABLE,
+            PreviewProgramColumns.AVAILABILITY_FREE_WITH_SUBSCRIPTION,
+            PreviewProgramColumns.AVAILABILITY_PAID_CONTENT,
+            PreviewProgramColumns.AVAILABILITY_PURCHASED,
+            PreviewProgramColumns.AVAILABILITY_FREE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface Availability {}
+
+    /**
+     * The unknown availability.
+     */
+    private static final int AVAILABILITY_UNKNOWN = -1;
+
+    /** @hide */
+    @IntDef({
+            INTERACTION_TYPE_UNKNOWN,
+            PreviewProgramColumns.INTERACTION_TYPE_VIEWS,
+            PreviewProgramColumns.INTERACTION_TYPE_LISTENS,
+            PreviewProgramColumns.INTERACTION_TYPE_FOLLOWERS,
+            PreviewProgramColumns.INTERACTION_TYPE_FANS,
+            PreviewProgramColumns.INTERACTION_TYPE_LIKES,
+            PreviewProgramColumns.INTERACTION_TYPE_THUMBS,
+            PreviewProgramColumns.INTERACTION_TYPE_VIEWERS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface InteractionType {}
+
+    /**
+     * The unknown interaction type.
+     */
+    private static final int INTERACTION_TYPE_UNKNOWN = -1;
+
     BasePreviewProgram(Builder builder) {
         super(builder);
     }
@@ -127,7 +209,7 @@
      */
     public @Type int getType() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_TYPE);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? TYPE_UNKNOWN : i;
     }
 
     /**
@@ -137,7 +219,7 @@
      */
     public @AspectRatio int getPosterArtAspectRatio() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? ASPECT_RATIO_UNKNOWN : i;
     }
 
     /**
@@ -147,7 +229,7 @@
      */
     public @AspectRatio int getThumbnailAspectRatio() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_THUMBNAIL_ASPECT_RATIO);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? ASPECT_RATIO_UNKNOWN : i;
     }
 
     /**
@@ -165,7 +247,7 @@
      */
     public @Availability int getAvailability() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_AVAILABILITY);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? AVAILABILITY_UNKNOWN : i;
     }
 
     /**
@@ -216,7 +298,7 @@
      */
     public @InteractionType int getInteractionType() {
         Integer i = mValues.getAsInteger(PreviewPrograms.COLUMN_INTERACTION_TYPE);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? INTERACTION_TYPE_UNKNOWN : i;
     }
 
     /**
diff --git a/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java b/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
index e4ce9d1..23b5cf9 100644
--- a/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/BaseProgram.java
@@ -22,13 +22,16 @@
 import android.media.tv.TvContentRating;
 import android.net.Uri;
 import android.os.Build;
+import android.support.annotation.IntDef;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.BaseTvColumns;
 import android.support.media.tv.TvContractCompat.ProgramColumns;
-import android.support.media.tv.TvContractCompat.ProgramColumns.ReviewRatingStyle;
 import android.support.media.tv.TvContractCompat.Programs;
 import android.support.media.tv.TvContractCompat.Programs.Genres.Genre;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Base class for derived classes that want to have common fields for programs defined in
  * {@link TvContractCompat}.
@@ -46,6 +49,22 @@
     private static final int IS_SEARCHABLE = 1;
 
     /** @hide */
+    @IntDef({
+            REVIEW_RATING_STYLE_UNKNOWN,
+            ProgramColumns.REVIEW_RATING_STYLE_STARS,
+            ProgramColumns.REVIEW_RATING_STYLE_THUMBS_UP_DOWN,
+            ProgramColumns.REVIEW_RATING_STYLE_PERCENTAGE,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    @interface ReviewRatingStyle {}
+
+    /**
+     * The unknown review rating style.
+     */
+    private static final int REVIEW_RATING_STYLE_UNKNOWN = -1;
+
+    /** @hide */
     @RestrictTo(LIBRARY_GROUP)
     protected ContentValues mValues;
 
@@ -254,7 +273,7 @@
      */
     public @ReviewRatingStyle int getReviewRatingStyle() {
         Integer i = mValues.getAsInteger(Programs.COLUMN_REVIEW_RATING_STYLE);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? REVIEW_RATING_STYLE_UNKNOWN : i;
     }
 
     /**
diff --git a/tv-provider/src/main/java/android/support/media/tv/Program.java b/tv-provider/src/main/java/android/support/media/tv/Program.java
index 4e3bd7a..233f1ba 100644
--- a/tv-provider/src/main/java/android/support/media/tv/Program.java
+++ b/tv-provider/src/main/java/android/support/media/tv/Program.java
@@ -25,6 +25,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.Programs;
+import android.support.media.tv.TvContractCompat.Programs.Genres.Genre;
 
 /**
  * A convenience class to access {@link TvContractCompat.Programs} entries in the system content
@@ -282,7 +283,7 @@
          * @return This Builder object to allow for chaining of calls to builder methods.
          * @see Programs#COLUMN_BROADCAST_GENRE
          */
-        public Builder setBroadcastGenres(String[] genres) {
+        public Builder setBroadcastGenres(@Genre String[] genres) {
             mValues.put(Programs.COLUMN_BROADCAST_GENRE, Programs.Genres.encode(genres));
             return this;
         }
diff --git a/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java b/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
index 5a46e79..de4fd04 100644
--- a/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
+++ b/tv-provider/src/main/java/android/support/media/tv/TvContractCompat.java
@@ -30,7 +30,6 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.provider.BaseColumns;
-import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.RequiresApi;
@@ -606,16 +605,6 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     interface ProgramColumns {
-        /** @hide */
-        @IntDef({
-                REVIEW_RATING_STYLE_STARS,
-                REVIEW_RATING_STYLE_THUMBS_UP_DOWN,
-                REVIEW_RATING_STYLE_PERCENTAGE,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        @interface ReviewRatingStyle {}
-
         /**
          * The review rating style for five star rating.
          *
@@ -934,27 +923,6 @@
      */
     @RestrictTo(LIBRARY_GROUP)
     public interface PreviewProgramColumns {
-
-        /** @hide */
-        @IntDef({
-                TYPE_MOVIE,
-                TYPE_TV_SERIES,
-                TYPE_TV_SEASON,
-                TYPE_TV_EPISODE,
-                TYPE_CLIP,
-                TYPE_EVENT,
-                TYPE_CHANNEL,
-                TYPE_TRACK,
-                TYPE_ALBUM,
-                TYPE_ARTIST,
-                TYPE_PLAYLIST,
-                TYPE_STATION,
-                TYPE_GAME
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface Type {}
-
         /**
          * The program type for movie.
          *
@@ -1046,19 +1014,6 @@
          */
         int TYPE_GAME = 12;
 
-        /** @hide */
-        @IntDef({
-                ASPECT_RATIO_16_9,
-                ASPECT_RATIO_3_2,
-                ASPECT_RATIO_4_3,
-                ASPECT_RATIO_1_1,
-                ASPECT_RATIO_2_3,
-                ASPECT_RATIO_MOVIE_POSTER,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface AspectRatio {}
-
         /**
          * The aspect ratio for 16:9.
          *
@@ -1107,18 +1062,6 @@
          */
         int ASPECT_RATIO_MOVIE_POSTER = 5;
 
-        /** @hide */
-        @IntDef({
-                AVAILABILITY_AVAILABLE,
-                AVAILABILITY_FREE_WITH_SUBSCRIPTION,
-                AVAILABILITY_PAID_CONTENT,
-                AVAILABILITY_PURCHASED,
-                AVAILABILITY_FREE,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface Availability {}
-
         /**
          * The availability for "available to this user".
          *
@@ -1155,20 +1098,6 @@
          */
         int AVAILABILITY_FREE = 4;
 
-        /** @hide */
-        @IntDef({
-                INTERACTION_TYPE_VIEWS,
-                INTERACTION_TYPE_LISTENS,
-                INTERACTION_TYPE_FOLLOWERS,
-                INTERACTION_TYPE_FANS,
-                INTERACTION_TYPE_LIKES,
-                INTERACTION_TYPE_THUMBS,
-                INTERACTION_TYPE_VIEWERS,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface InteractionType {}
-
         /**
          * The interaction type for "views".
          *
@@ -2895,17 +2824,6 @@
         /** The MIME type of a single preview TV program. */
         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/watch_next_program";
 
-        /** @hide */
-        @IntDef({
-                WATCH_NEXT_TYPE_CONTINUE,
-                WATCH_NEXT_TYPE_NEXT,
-                WATCH_NEXT_TYPE_NEW,
-                WATCH_NEXT_TYPE_WATCHLIST,
-        })
-        @Retention(RetentionPolicy.SOURCE)
-        @RestrictTo(LIBRARY_GROUP)
-        public @interface WatchNextType {}
-
         /**
          * The watch next type for CONTINUE. Use this type when the user has already watched more
          * than 1 minute of this content.
diff --git a/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java b/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
index f466584..c192745 100644
--- a/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
+++ b/tv-provider/src/main/java/android/support/media/tv/WatchNextProgram.java
@@ -22,12 +22,15 @@
 import android.database.Cursor;
 import android.media.tv.TvContentRating;  // For javadoc gen of super class
 import android.os.Build;
+import android.support.annotation.IntDef;
 import android.support.annotation.RestrictTo;
 import android.support.media.tv.TvContractCompat.PreviewPrograms;  // For javadoc gen of super class
 import android.support.media.tv.TvContractCompat.Programs;  // For javadoc gen of super class
 import android.support.media.tv.TvContractCompat.Programs.Genres;  // For javadoc gen of super class
 import android.support.media.tv.TvContractCompat.WatchNextPrograms;
-import android.support.media.tv.TvContractCompat.WatchNextPrograms.WatchNextType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 
 /**
  * A convenience class to access {@link WatchNextPrograms} entries in the system content
@@ -87,16 +90,34 @@
     private static final long INVALID_LONG_VALUE = -1;
     private static final int INVALID_INT_VALUE = -1;
 
+    /** @hide */
+    @IntDef({
+            WATCH_NEXT_TYPE_UNKNOWN,
+            WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE,
+            WatchNextPrograms.WATCH_NEXT_TYPE_NEXT,
+            WatchNextPrograms.WATCH_NEXT_TYPE_NEW,
+            WatchNextPrograms.WATCH_NEXT_TYPE_WATCHLIST,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(LIBRARY_GROUP)
+    public @interface WatchNextType {}
+
+    /**
+     * The unknown watch next type. Use this type when the actual type is not known.
+     */
+    public static final int WATCH_NEXT_TYPE_UNKNOWN = -1;
+
     private WatchNextProgram(Builder builder) {
         super(builder);
     }
 
     /**
-     * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program.
+     * @return The value of {@link WatchNextPrograms#COLUMN_WATCH_NEXT_TYPE} for the program,
+     * or {@link #WATCH_NEXT_TYPE_UNKNOWN} if it's unknown.
      */
     public @WatchNextType int getWatchNextType() {
         Integer i = mValues.getAsInteger(WatchNextPrograms.COLUMN_WATCH_NEXT_TYPE);
-        return i == null ? INVALID_INT_VALUE : i;
+        return i == null ? WATCH_NEXT_TYPE_UNKNOWN : i;
     }
 
     /**