Merge "Remove dependency on Multidex by design and leanback libs." into oc-support-26.0-dev
diff --git a/app-toolkit/dependencies.gradle b/app-toolkit/dependencies.gradle
index 6b99d77..c683e14 100644
--- a/app-toolkit/dependencies.gradle
+++ b/app-toolkit/dependencies.gradle
@@ -27,7 +27,7 @@
 ffVersions.javapoet = "1.8.0"
 ffVersions.compile_testing = "0.11"
 ffVersions.localize_maven = "1.2"
-ffVersions.support_lib = "25.3.1"
+ffVersions.support_lib = "26.0.0"
 ffVersions.intellij_annotations = "12.0"
 ffVersions.rxjava2 = "2.0.6"
 ffVersions.reactivestreams = "1.0.0"
@@ -84,5 +84,5 @@
 ext.tools.current_sdk = gradle.ext.currentSdk
 ext.tools.build_tools_version = rootProject.ext.buildToolsVersion
 ext.flatfoot = [:]
-ext.flatfoot.release_version = "1.0.0-alpha5"
+ext.flatfoot.release_version = "1.0.0-alpha6"
 ext.flatfoot.min_sdk = 14
diff --git a/room/integration-tests/testapp/build.gradle b/room/integration-tests/testapp/build.gradle
index e85d90e..9873138 100644
--- a/room/integration-tests/testapp/build.gradle
+++ b/room/integration-tests/testapp/build.gradle
@@ -81,7 +81,8 @@
     androidTestCompile project(':room:rxjava2')
     androidTestCompile project(':arch:core-testing')
     androidTestCompile libs.rx_java
-    testCompile libs.mockito_core
+    androidTestCompile libs.mockito_core
+    androidTestImplementation libs.dexmaker_mockito
 }
 
 createAndroidCheckstyle(project)
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
index 6fb1186..7b03e43 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
@@ -28,6 +28,7 @@
 import android.arch.persistence.room.migration.Migration;
 import android.arch.persistence.room.testing.MigrationTestHelper;
 import android.arch.persistence.room.util.TableInfo;
+import android.content.Context;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -221,6 +222,36 @@
         assertThat(info.foreignKeys.size(), is(1));
     }
 
+    @Test
+    public void missingMigration() throws IOException {
+        SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1);
+        database.close();
+        try {
+            Context targetContext = InstrumentationRegistry.getTargetContext();
+            MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+                    .build();
+            db.dao().loadAllEntity1s();
+            throw new AssertionError("Should've failed :/");
+        } catch (IllegalStateException ignored) {
+        }
+    }
+
+    @Test
+    public void missingMigrationNuke() throws IOException {
+        SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 1);
+        final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database);
+        dao.insertIntoEntity1(2, "foo");
+        dao.insertIntoEntity1(3, "bar");
+        database.close();
+
+        Context targetContext = InstrumentationRegistry.getTargetContext();
+        MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+                .fallbackToDestructiveMigration()
+                .build();
+        assertThat(db.dao().loadAllEntity1s().size(), is(0));
+        db.close();
+    }
+
     private void testFailure(int startVersion, int endVersion) throws IOException {
         final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
         db.close();
@@ -232,10 +263,11 @@
             throwable = t;
         }
         assertThat(throwable, instanceOf(IllegalStateException.class));
+        //noinspection ConstantConditions
         assertThat(throwable.getMessage(), containsString("Migration failed"));
     }
 
-    static final Migration MIGRATION_1_2 = new Migration(1, 2) {
+    private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
         @Override
         public void migrate(SupportSQLiteDatabase database) {
             database.execSQL("CREATE TABLE IF NOT EXISTS `Entity2` ("
@@ -244,7 +276,7 @@
         }
     };
 
-    static final Migration MIGRATION_2_3 = new Migration(2, 3) {
+    private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
         @Override
         public void migrate(SupportSQLiteDatabase database) {
             database.execSQL("ALTER TABLE " + MigrationDb.Entity2.TABLE_NAME
@@ -252,7 +284,7 @@
         }
     };
 
-    static final Migration MIGRATION_3_4 = new Migration(3, 4) {
+    private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
         @Override
         public void migrate(SupportSQLiteDatabase database) {
             database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3` (`id` INTEGER,"
@@ -260,7 +292,7 @@
         }
     };
 
-    static final Migration MIGRATION_4_5 = new Migration(4, 5) {
+    private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
         @Override
         public void migrate(SupportSQLiteDatabase database) {
             database.execSQL("CREATE TABLE IF NOT EXISTS `Entity3_New` (`id` INTEGER,"
@@ -272,14 +304,14 @@
         }
     };
 
-    static final Migration MIGRATION_5_6 = new Migration(5, 6) {
+    private static final Migration MIGRATION_5_6 = new Migration(5, 6) {
         @Override
         public void migrate(SupportSQLiteDatabase database) {
             database.execSQL("DROP TABLE " + MigrationDb.Entity3.TABLE_NAME);
         }
     };
 
-    static final Migration MIGRATION_6_7 = new Migration(6, 7) {
+    private static final Migration MIGRATION_6_7 = new Migration(6, 7) {
         @Override
         public void migrate(SupportSQLiteDatabase database) {
             database.execSQL("CREATE TABLE IF NOT EXISTS " + MigrationDb.Entity4.TABLE_NAME
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java
new file mode 100644
index 0000000..353c2e3
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/test/CustomDatabaseTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import android.arch.persistence.db.SupportSQLiteDatabase;
+import android.arch.persistence.db.SupportSQLiteOpenHelper;
+import android.arch.persistence.db.SupportSQLiteQuery;
+import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
+import android.arch.persistence.room.Room;
+import android.arch.persistence.room.RoomDatabase;
+import android.arch.persistence.room.integration.testapp.database.Customer;
+import android.arch.persistence.room.integration.testapp.database.SampleDatabase;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class CustomDatabaseTest {
+
+    @Test
+    public void invalidationTrackerAfterClose() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        RoomDatabase.Builder<SampleDatabase> builder =
+                Room.databaseBuilder(context, SampleDatabase.class, "db")
+                        .openHelperFactory(new RethrowExceptionFactory());
+        Customer customer = new Customer();
+        for (int i = 0; i < 100; i++) {
+            SampleDatabase db = builder.build();
+            customer.setId(i);
+            db.getCustomerDao().insert(customer);
+            // Give InvalidationTracker enough time to start #mRefreshRunnable and pass the
+            // initialization check.
+            SystemClock.sleep(1);
+            // InvalidationTracker#mRefreshRunnable will cause race condition if its database query
+            // happens after close.
+            db.close();
+        }
+    }
+
+    /**
+     * This is mostly {@link FrameworkSQLiteOpenHelperFactory}, but the returned {@link
+     * SupportSQLiteDatabase} fails with {@link RuntimeException} instead of {@link
+     * IllegalStateException} or {@link SQLiteException}. This way, we can simulate custom database
+     * implementation that throws its own exception types.
+     */
+    private static class RethrowExceptionFactory implements SupportSQLiteOpenHelper.Factory {
+
+        @Override
+        public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
+            final FrameworkSQLiteOpenHelperFactory factory = new FrameworkSQLiteOpenHelperFactory();
+            final SupportSQLiteOpenHelper helper = factory.create(configuration);
+            SupportSQLiteOpenHelper helperMock = mock(SupportSQLiteOpenHelper.class,
+                    delegatesTo(helper));
+            // Inject mocks to the object hierarchy.
+            doAnswer(new Answer() {
+                @Override
+                public SupportSQLiteDatabase answer(InvocationOnMock invocation)
+                        throws Throwable {
+                    final SupportSQLiteDatabase db = helper.getWritableDatabase();
+                    SupportSQLiteDatabase dbMock = mock(SupportSQLiteDatabase.class,
+                            delegatesTo(db));
+                    doAnswer(new Answer() {
+                        @Override
+                        public Cursor answer(InvocationOnMock invocation) throws Throwable {
+                            SupportSQLiteQuery query = invocation.getArgument(0);
+                            try {
+                                return db.query(query);
+                            } catch (IllegalStateException | SQLiteException e) {
+                                // Rethrow the exception in order to simulate the way custom
+                                // database implementation throws its own exception types.
+                                throw new RuntimeException("closed", e);
+                            }
+                        }
+                    }).when(dbMock).query(any(SupportSQLiteQuery.class));
+                    return dbMock;
+                }
+            }).when(helperMock).getWritableDatabase();
+            return helperMock;
+        }
+    }
+}
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java b/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java
index e3d0c63..adf5d4d 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/DatabaseConfiguration.java
@@ -60,6 +60,11 @@
     public final boolean allowMainThreadQueries;
 
     /**
+     * If true, Room should crash if a migration is missing.
+     */
+    public final boolean requireMigration;
+
+    /**
      * Creates a database configuration with the given values.
      *
      * @param context The application context.
@@ -68,6 +73,8 @@
      * @param migrationContainer The migration container for migrations.
      * @param callbacks The list of callbacks for database events.
      * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
+     * @param requireMigration True if Room should require a valid migration if version changes,
+     *                        instead of recreating the tables.
      *
      * @hide
      */
@@ -76,12 +83,14 @@
             @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
             @NonNull RoomDatabase.MigrationContainer migrationContainer,
             @Nullable List<RoomDatabase.Callback> callbacks,
-            boolean allowMainThreadQueries) {
+            boolean allowMainThreadQueries,
+            boolean requireMigration) {
         this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
         this.context = context;
         this.name = name;
         this.migrationContainer = migrationContainer;
         this.callbacks = callbacks;
         this.allowMainThreadQueries = allowMainThreadQueries;
+        this.requireMigration = requireMigration;
     }
 }
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java b/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java
index 3db5a86..619c53d 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/InvalidationTracker.java
@@ -38,6 +38,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
 
 /**
  * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
@@ -340,16 +341,20 @@
     Runnable mRefreshRunnable = new Runnable() {
         @Override
         public void run() {
-            if (!ensureInitialization()) {
-                return;
-            }
-            if (mDatabase.inTransaction()
-                    || !mPendingRefresh.compareAndSet(true, false)) {
-                // no pending refresh
-                return;
-            }
+            final Lock closeLock = mDatabase.getCloseLock();
             boolean hasUpdatedTable = false;
             try {
+                closeLock.lock();
+
+                if (!ensureInitialization()) {
+                    return;
+                }
+
+                if (mDatabase.inTransaction()
+                        || !mPendingRefresh.compareAndSet(true, false)) {
+                    // no pending refresh
+                    return;
+                }
                 mCleanupStatement.executeUpdateDelete();
                 mQueryArgs[0] = mMaxVersion;
                 Cursor cursor = mDatabase.query(SELECT_UPDATED_TABLES_SQL, mQueryArgs);
@@ -371,6 +376,8 @@
                 // may happen if db is closed. just log.
                 Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
                         exception);
+            } finally {
+                closeLock.unlock();
             }
             if (hasUpdatedTable) {
                 synchronized (mObserverMap) {
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
index cc265c0..e64f2d6 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/RoomDatabase.java
@@ -37,6 +37,8 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.Callable;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Base class for all Room databases. All classes that are annotated with {@link Database} must
@@ -59,6 +61,18 @@
     @Nullable
     protected List<Callback> mCallbacks;
 
+    private final ReentrantLock mCloseLock = new ReentrantLock();
+
+    /**
+     * {@link InvalidationTracker} uses this lock to prevent the database from closing while it is
+     * querying database updates.
+     *
+     * @return The lock for {@link #close()}.
+     */
+    Lock getCloseLock() {
+        return mCloseLock;
+    }
+
     /**
      * Creates a RoomDatabase.
      * <p>
@@ -125,7 +139,12 @@
      */
     public void close() {
         if (isOpen()) {
-            mOpenHelper.close();
+            try {
+                mCloseLock.lock();
+                mOpenHelper.close();
+            } finally {
+                mCloseLock.unlock();
+            }
         }
     }
 
@@ -294,6 +313,7 @@
         private SupportSQLiteOpenHelper.Factory mFactory;
         private boolean mInMemory;
         private boolean mAllowMainThreadQueries;
+        private boolean mRequireMigration;
         /**
          * Migrations, mapped by from-to pairs.
          */
@@ -303,6 +323,7 @@
             mContext = context;
             mDatabaseClass = klass;
             mName = name;
+            mRequireMigration = true;
             mMigrationContainer = new MigrationContainer();
         }
 
@@ -360,6 +381,25 @@
         }
 
         /**
+         * When the database version on the device does not match the latest schema version, Room
+         * runs necessary {@link Migration}s on the database.
+         * <p>
+         * If it cannot find the set of {@link Migration}s that will bring the database to the
+         * current version, it will throw an {@link IllegalStateException}.
+         * <p>
+         * You can call this method to change this behavior to re-create the database instead of
+         * crashing.
+         * <p>
+         * Note that this will delete all of the data in the database tables managed by Room.
+         *
+         * @return this
+         */
+        public Builder<T> fallbackToDestructiveMigration() {
+            mRequireMigration = false;
+            return this;
+        }
+
+        /**
          * Adds a {@link Callback} to this database.
          *
          * @param callback The callback.
@@ -396,7 +436,7 @@
             }
             DatabaseConfiguration configuration =
                     new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
-                            mCallbacks, mAllowMainThreadQueries);
+                            mCallbacks, mAllowMainThreadQueries, mRequireMigration);
             T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
             db.init(configuration);
             return db;
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
index a5b8150..8767f06 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/RoomOpenHelper.java
@@ -77,6 +77,12 @@
             }
         }
         if (!migrated) {
+            if (mConfiguration == null || mConfiguration.requireMigration) {
+                throw new IllegalStateException("A migration from " + oldVersion + " to "
+                + newVersion + " is necessary. Please provide a Migration in the builder or call"
+                        + " fallbackToDestructiveMigration in the builder in which case Room will"
+                        + " re-create all of the tables.");
+            }
             mDelegate.dropAllTables(db);
             mDelegate.createAllTables(db);
         }
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/migration/Migration.java b/room/runtime/src/main/java/android/arch/persistence/room/migration/Migration.java
index 0023a2e..907e624 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/migration/Migration.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/migration/Migration.java
@@ -52,6 +52,9 @@
      * Should run the necessary migrations.
      * <p>
      * This class cannot access any generated Dao in this method.
+     * <p>
+     * This method is already called inside a transaction and that transaction might actually be a
+     * composite transaction of all necessary {@code Migration}s.
      *
      * @param database The database instance
      */
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java b/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java
index ecf6cc4..0728cca 100644
--- a/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java
+++ b/room/runtime/src/test/java/android/arch/persistence/room/BuilderTest.java
@@ -108,6 +108,15 @@
     }
 
     @Test
+    public void skipMigration() {
+        Context context = mock(Context.class);
+        TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+                .fallbackToDestructiveMigration().build();
+        DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+        assertThat(config.requireMigration, is(false));
+    }
+
+    @Test
     public void createBasic() {
         Context context = mock(Context.class);
         TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
diff --git a/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java b/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java
index cd2375e..f0b730a 100644
--- a/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java
+++ b/room/runtime/src/test/java/android/arch/persistence/room/InvalidationTrackerTest.java
@@ -56,6 +56,7 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
 
 @RunWith(JUnit4.class)
 public class InvalidationTrackerTest {
@@ -75,6 +76,8 @@
         doReturn(statement).when(sqliteDb).compileStatement(eq(InvalidationTracker.CLEANUP_SQL));
         doReturn(sqliteDb).when(mOpenHelper).getWritableDatabase();
         doReturn(true).when(mRoomDatabase).isOpen();
+        ReentrantLock closeLock = new ReentrantLock();
+        doReturn(closeLock).when(mRoomDatabase).getCloseLock();
         //noinspection ResultOfMethodCallIgnored
         doReturn(mOpenHelper).when(mRoomDatabase).getOpenHelper();
 
diff --git a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
index 1c5edd0..16df7f1 100644
--- a/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
+++ b/room/testing/src/main/java/android/arch/persistence/room/testing/MigrationTestHelper.java
@@ -141,7 +141,8 @@
         SchemaBundle schemaBundle = loadSchema(version);
         RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
         DatabaseConfiguration configuration = new DatabaseConfiguration(
-                mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true);
+                mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
+                true);
         RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
                 new CreatingDelegate(schemaBundle.getDatabase()),
                 schemaBundle.getDatabase().getIdentityHash());
@@ -183,7 +184,8 @@
         RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
         container.addMigrations(migrations);
         DatabaseConfiguration configuration = new DatabaseConfiguration(
-                mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true);
+                mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
+                true);
         RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
                 new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
                 schemaBundle.getDatabase().getIdentityHash());
diff --git a/v17/leanback/res/layout/lb_playback_transport_controls_row.xml b/v17/leanback/res/layout/lb_playback_transport_controls_row.xml
index c8852e8..8b692f3 100644
--- a/v17/leanback/res/layout/lb_playback_transport_controls_row.xml
+++ b/v17/leanback/res/layout/lb_playback_transport_controls_row.xml
@@ -18,7 +18,7 @@
 <!-- Note: clipChildren/clipToPadding false are needed to apply shadows to child
      views with no padding of their own. Also to allow for negative margin on description. -->
 
-<android.support.v17.leanback.widget.PlaybackTransportRowView
+<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -68,9 +68,10 @@
             android:layout_marginBottom="@dimen/lb_playback_transport_thumbs_bottom_margin" />
     </FrameLayout>
 
-    <LinearLayout
+    <android.support.v17.leanback.widget.PlaybackTransportRowView
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:id="@+id/transport_row"
         android:orientation="vertical"
         android:paddingStart="?attr/browsePaddingStart"
         android:paddingEnd="?attr/browsePaddingEnd"
@@ -134,5 +135,5 @@
         </RelativeLayout>
 
 
-    </LinearLayout>
-</android.support.v17.leanback.widget.PlaybackTransportRowView>
+    </android.support.v17.leanback.widget.PlaybackTransportRowView>
+</LinearLayout>
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
index e6db33b..4505944 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackTransportRowPresenter.java
@@ -666,8 +666,8 @@
         vh.mSecondaryControlsVh = (ControlBarPresenter.ViewHolder) mSecondaryControlsPresenter
                 .onCreateViewHolder(vh.mSecondaryControlsDock);
         vh.mSecondaryControlsDock.addView(vh.mSecondaryControlsVh.view);
-        ((PlaybackTransportRowView) vh.view).setOnUnhandledKeyListener(
-                new PlaybackTransportRowView.OnUnhandledKeyListener() {
+        ((PlaybackTransportRowView) vh.view.findViewById(R.id.transport_row))
+                .setOnUnhandledKeyListener(new PlaybackTransportRowView.OnUnhandledKeyListener() {
                 @Override
                 public boolean onUnhandledKey(KeyEvent event) {
                     if (vh.getOnKeyListener() != null) {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
index d6a1f86..2cee649 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackGlueHostImplWithViewHolder.java
@@ -56,12 +56,13 @@
         if (mViewHolder == null && mPlaybackRowPresenter != null && mRow != null) {
             mViewHolder = (PlaybackRowPresenter.ViewHolder)
                     mPlaybackRowPresenter.onCreateViewHolder(mRootView = new FrameLayout(mContext));
+            // Bind ViewHolder before measure/layout so child views will get proper size
+            mPlaybackRowPresenter.onBindViewHolder(mViewHolder, mRow);
             mRootView.addView(mViewHolder.view, mLayoutWidth, mLayoutHeight);
             mRootView.measure(
                     View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.AT_MOST),
                     View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
             mRootView.layout(0, 0, mRootView.getMeasuredWidth(), mRootView.getMeasuredHeight());
-            mPlaybackRowPresenter.onBindViewHolder(mViewHolder, mRow);
             if (mViewHolder instanceof PlaybackSeekUi) {
                 ((PlaybackSeekUi) mViewHolder).setPlaybackSeekUiClient(mChainedClient);
             }
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
index 54d27a3..db55725 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/PlaybackTransportRowPresenterTest.java
@@ -37,6 +37,8 @@
 import android.support.v17.leanback.widget.PlaybackSeekDataProvider.ResultCallback;
 import android.view.ContextThemeWrapper;
 import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewParent;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -65,7 +67,24 @@
         InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
             @Override
             public void run() {
-                mGlue = new PlaybackTransportControlGlue(mContext, mImpl);
+                mGlue = new PlaybackTransportControlGlue(mContext, mImpl) {
+                    @Override
+                    protected void onCreatePrimaryActions(ArrayObjectAdapter
+                            primaryActionsAdapter) {
+                        super.onCreatePrimaryActions(primaryActionsAdapter);
+                        primaryActionsAdapter.add(
+                                new PlaybackControlsRow.ClosedCaptioningAction(mContext));
+                    }
+
+                    @Override
+                    protected void onCreateSecondaryActions(ArrayObjectAdapter
+                            secondaryActionsAdapter) {
+                        secondaryActionsAdapter.add(
+                                new PlaybackControlsRow.HighQualityAction(mContext));
+                        secondaryActionsAdapter.add(
+                                new PlaybackControlsRow.PictureInPictureAction(mContext));
+                    }
+                };
                 mGlue.setHost(mHost);
 
             }
@@ -195,6 +214,107 @@
         assertSame(art, mViewHolder.mImageView.getDrawable());
     }
 
+    static boolean isDescendant(View view, View descendant) {
+        while (descendant != view) {
+            ViewParent p = descendant.getParent();
+            if (!(p instanceof View)) {
+                return false;
+            }
+            descendant = (View) p;
+        }
+        return true;
+    }
+
+    @Test
+    public void navigateRightInPrimary() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mViewHolder.mControlsVh.mControlBar.getChildAt(0).requestFocus();
+            }
+        });
+        View view = mViewHolder.view.findFocus();
+        assertTrue(isDescendant(mViewHolder.mControlsVh.mControlBar.getChildAt(0), view));
+        assertTrue(isDescendant(mViewHolder.mControlsVh.mControlBar.getChildAt(1),
+                view.focusSearch(View.FOCUS_RIGHT)));
+    }
+
+    @Test
+    public void navigateRightInSecondary() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0).requestFocus();
+            }
+        });
+        View view = mViewHolder.view.findFocus();
+        assertTrue(isDescendant(mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0), view));
+        assertTrue(isDescendant(mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(1),
+                view.focusSearch(View.FOCUS_RIGHT)));
+    }
+
+    @Test
+    public void navigatePrimaryDownToProgress() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mViewHolder.mControlsVh.mControlBar.getChildAt(0).requestFocus();
+            }
+        });
+        View view = mViewHolder.view.findFocus();
+        assertTrue(isDescendant(mViewHolder.mControlsVh.mControlBar.getChildAt(0), view));
+        assertSame(mViewHolder.mProgressBar, view.focusSearch(View.FOCUS_DOWN));
+    }
+
+    @Test
+    public void navigateProgressUpToPrimary() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mViewHolder.mProgressBar.requestFocus();
+            }
+        });
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mViewHolder.mProgressBar.focusSearch(View.FOCUS_UP).requestFocus();
+            }
+        });
+        View view = mViewHolder.view.findFocus();
+        assertTrue(isDescendant(mViewHolder.mControlsVh.mControlBar.getChildAt(0), view));
+    }
+
+    @Test
+    public void navigateProgressDownToSecondary() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mViewHolder.mProgressBar.requestFocus();
+            }
+        });
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mViewHolder.mProgressBar.focusSearch(View.FOCUS_DOWN).requestFocus();
+            }
+        });
+        View view = mViewHolder.view.findFocus();
+        assertTrue(isDescendant(mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0), view));
+    }
+
+    @Test
+    public void navigateSecondaryUpToProgress() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0).requestFocus();
+            }
+        });
+        View view = mViewHolder.view.findFocus();
+        assertTrue(isDescendant(mViewHolder.mSecondaryControlsVh.mControlBar.getChildAt(0), view));
+        assertSame(mViewHolder.mProgressBar, view.focusSearch(View.FOCUS_UP));
+    }
+
     @Test
     public void seekAndConfirm() {
         when(mImpl.isPrepared()).thenReturn(true);