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