Merge "appcompat: Fix testDrawerHeight without status bar" into nyc-support-25.1-dev
diff --git a/annotations/src/android/support/annotation/IntDef.java b/annotations/src/android/support/annotation/IntDef.java
index be2e2b8..f621b7f 100644
--- a/annotations/src/android/support/annotation/IntDef.java
+++ b/annotations/src/android/support/annotation/IntDef.java
@@ -15,16 +15,12 @@
*/
package android.support.annotation;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.ElementType.METHOD;
-import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
/**
* Denotes that the annotated element of integer type, represents
* a logical type and that its value should be one of the explicitly
@@ -47,7 +43,7 @@
* For a flag, set the flag attribute:
* <pre><code>
* @IntDef(
- * flag = true
+ * flag = true,
* value = {NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
* </code></pre>
*/
diff --git a/build.gradle b/build.gradle
index d7cced1..340984f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -41,8 +41,8 @@
}
gradle.ext.currentSdk = 25
-ext.supportVersion = '25.1.0'
-ext.extraVersion = 41
+ext.supportVersion = '25.1.1'
+ext.extraVersion = 42
ext.supportRepoOut = ''
ext.buildToolsVersion = '24.0.1'
ext.buildNumber = Integer.toString(ext.extraVersion)
diff --git a/design/res/layout/design_bottom_navigation_item.xml b/design/res/layout/design_bottom_navigation_item.xml
index 67df838..f6212cf 100644
--- a/design/res/layout/design_bottom_navigation_item.xml
+++ b/design/res/layout/design_bottom_navigation_item.xml
@@ -26,8 +26,9 @@
<android.support.design.internal.BaselineLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/design_bottom_navigation_margin"
android:layout_gravity="bottom|center_horizontal"
+ android:clipToPadding="false"
+ android:paddingBottom="10dp"
android:duplicateParentState="true">
<TextView
android:id="@+id/smallLabel"
diff --git a/design/src/android/support/design/internal/BaselineLayout.java b/design/src/android/support/design/internal/BaselineLayout.java
index eac3542..23a04cd 100644
--- a/design/src/android/support/design/internal/BaselineLayout.java
+++ b/design/src/android/support/design/internal/BaselineLayout.java
@@ -24,7 +24,8 @@
import android.view.ViewGroup;
/**
- * A simple ViewGroup that aligns all the views inside on a baseline.
+ * A simple ViewGroup that aligns all the views inside on a baseline. Note: bottom padding for this
+ * view will be measured starting from the baseline.
*
* @hide
*/
@@ -69,6 +70,7 @@
ViewCompat.getMeasuredState(child));
}
if (maxChildBaseline != -1) {
+ maxChildDescent = Math.max(maxChildDescent, getPaddingBottom());
maxHeight = Math.max(maxHeight, maxChildBaseline + maxChildDescent);
mBaseline = maxChildBaseline;
}
diff --git a/design/src/android/support/design/internal/BottomNavigationMenuView.java b/design/src/android/support/design/internal/BottomNavigationMenuView.java
index bc73970..82d983e 100644
--- a/design/src/android/support/design/internal/BottomNavigationMenuView.java
+++ b/design/src/android/support/design/internal/BottomNavigationMenuView.java
@@ -255,6 +255,7 @@
}
removeAllViews();
if (mMenu.size() == 0) {
+ mButtons = null;
return;
}
mButtons = new BottomNavigationItemView[mMenu.size()];
diff --git a/design/src/android/support/design/widget/TextInputLayout.java b/design/src/android/support/design/widget/TextInputLayout.java
index 92c1506..22f4546 100644
--- a/design/src/android/support/design/widget/TextInputLayout.java
+++ b/design/src/android/support/design/widget/TextInputLayout.java
@@ -1076,6 +1076,13 @@
});
}
+ if (mEditText != null && ViewCompat.getMinimumHeight(mEditText) <= 0) {
+ // We should make sure that the EditText has the same min-height as the password
+ // toggle view. This ensure focus works properly, and there is no visual jump
+ // if the password toggle is enabled/disabled.
+ mEditText.setMinimumHeight(ViewCompat.getMinimumHeight(mPasswordToggleView));
+ }
+
mPasswordToggleView.setVisibility(VISIBLE);
mPasswordToggleView.setChecked(mPasswordToggledVisible);
diff --git a/design/tests/res/layout/design_navigation_view.xml b/design/tests/res/layout/design_navigation_view.xml
index 1789843..4f7147d 100644
--- a/design/tests/res/layout/design_navigation_view.xml
+++ b/design/tests/res/layout/design_navigation_view.xml
@@ -37,7 +37,7 @@
android:fitsSystemWindows="true" tells the system to have
DrawerLayout span the full height of the screen, including the
system status bar on Lollipop+ versions of the plaform. -->
- <android.support.design.widget.NavigationView
+ <android.support.design.widget.NavigationTestView
android:id="@+id/start_drawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
diff --git a/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java b/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
index 37a58a6..f06a85a 100644
--- a/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
+++ b/design/tests/src/android/support/design/widget/BottomNavigationViewTest.java
@@ -223,6 +223,16 @@
checkAndVerifyExclusiveItem(menu, R.id.destination_people);
}
+ @UiThreadTest
+ @Test
+ @SmallTest
+ public void testClearingMenu() throws Throwable {
+ mBottomNavigation.getMenu().clear();
+ assertEquals(0, mBottomNavigation.getMenu().size());
+ mBottomNavigation.inflateMenu(R.menu.bottom_navigation_view_content);
+ assertEquals(3, mBottomNavigation.getMenu().size());
+ }
+
private void checkAndVerifyExclusiveItem(final Menu menu, final int id) throws Throwable {
menu.findItem(id).setChecked(true);
for (int i = 0; i < menu.size(); i++) {
diff --git a/design/tests/src/android/support/design/widget/NavigationTestView.java b/design/tests/src/android/support/design/widget/NavigationTestView.java
new file mode 100644
index 0000000..af3d69e
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/NavigationTestView.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.design.widget;
+
+import android.content.Context;
+import android.support.v4.view.WindowInsetsCompat;
+import android.util.AttributeSet;
+
+/**
+ * Expose hasSystemWindowInsets() for testing.
+ */
+public class NavigationTestView extends NavigationView {
+
+ boolean mHashSystemWindowInsets;
+
+ public NavigationTestView(Context context) {
+ this(context, null);
+ }
+
+ public NavigationTestView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NavigationTestView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onInsetsChanged(WindowInsetsCompat insets) {
+ super.onInsetsChanged(insets);
+ mHashSystemWindowInsets = insets.hasSystemWindowInsets();
+ }
+
+ public boolean hasSystemWindowInsets() {
+ return mHashSystemWindowInsets;
+ }
+}
diff --git a/design/tests/src/android/support/design/widget/NavigationViewTest.java b/design/tests/src/android/support/design/widget/NavigationViewTest.java
index 7031281..f0c064e 100755
--- a/design/tests/src/android/support/design/widget/NavigationViewTest.java
+++ b/design/tests/src/android/support/design/widget/NavigationViewTest.java
@@ -94,7 +94,7 @@
private DrawerLayout mDrawerLayout;
- private NavigationView mNavigationView;
+ private NavigationTestView mNavigationView;
public NavigationViewTest() {
super(NavigationViewActivity.class);
@@ -104,7 +104,7 @@
public void setUp() throws Exception {
final NavigationViewActivity activity = mActivityTestRule.getActivity();
mDrawerLayout = (DrawerLayout) activity.findViewById(R.id.drawer_layout);
- mNavigationView = (NavigationView) mDrawerLayout.findViewById(R.id.start_drawer);
+ mNavigationView = (NavigationTestView) mDrawerLayout.findViewById(R.id.start_drawer);
// Close the drawer to reset the state for the next test
onView(withId(R.id.drawer_layout)).perform(closeDrawer(GravityCompat.START));
@@ -146,7 +146,11 @@
onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START));
if (Build.VERSION.SDK_INT >= 21) {
- assertFalse(mNavigationView.willNotDraw());
+ if (mNavigationView.hasSystemWindowInsets()) {
+ assertFalse(mNavigationView.willNotDraw());
+ } else {
+ assertTrue(mNavigationView.willNotDraw());
+ }
} else {
assertTrue(mNavigationView.willNotDraw());
}
diff --git a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
index 30c15b5..4944170 100755
--- a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
@@ -31,6 +31,7 @@
import static android.support.design.testutils.TextInputLayoutActions.setTypeface;
import static android.support.design.testutils.TextInputLayoutMatchers
.hasPasswordToggleContentDescription;
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.typeText;
@@ -38,6 +39,7 @@
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.contrib.AccessibilityChecks.accessibilityAssertion;
import static android.support.test.espresso.matcher.ViewMatchers.hasContentDescription;
+import static android.support.test.espresso.matcher.ViewMatchers.hasFocus;
import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
@@ -66,6 +68,7 @@
import android.support.v4.widget.TextViewCompat;
import android.util.AttributeSet;
import android.util.SparseArray;
+import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
@@ -456,6 +459,21 @@
.check(matches(withTextColor(textColor)));
}
+ @Test
+ public void testFocusMovesToEditTextWithPasswordEnabled() {
+ // Focus the preceding EditText
+ onView(withId(R.id.textinput_edittext))
+ .perform(click())
+ .check(matches(hasFocus()));
+
+ // Then send a TAB to focus the next view
+ getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
+
+ // And check that the EditText is focused
+ onView(withId(R.id.textinput_edittext_pwd))
+ .check(matches(hasFocus()));
+ }
+
static ViewAssertion isHintExpanded(final boolean expanded) {
return new ViewAssertion() {
@Override
diff --git a/media-compat/build.gradle b/media-compat/build.gradle
index e26a1bc..9bec2a3 100644
--- a/media-compat/build.gradle
+++ b/media-compat/build.gradle
@@ -23,6 +23,8 @@
defaultConfig {
minSdkVersion 9
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
sourceSets {
@@ -38,6 +40,11 @@
'java'
]
main.aidl.srcDirs = ['java']
+
+ androidTest.setRoot('tests')
+ androidTest.java.srcDir 'tests/src'
+ androidTest.res.srcDir 'tests/res'
+ androidTest.manifest.srcFile 'tests/AndroidManifest.xml'
}
compileOptions {
@@ -113,4 +120,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
new file mode 100644
index 0000000..1216194
--- /dev/null
+++ b/media-compat/tests/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="android.support.mediacompat.test">
+
+ <uses-sdk
+ android:minSdkVersion="9"
+ android:targetSdkVersion="23"
+ tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
+ android.support.test.espresso, android.support.test.espresso.idling"/>
+
+ <application android:supportsRtl="true">
+ <uses-library android:name="android.test.runner"/>
+ <activity android:name="android.support.v4.media.session.TestActivity" />
+ <receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
+ <intent-filter>
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
+ </intent-filter>
+ </receiver>
+ </application>
+
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="android.support.mediacompat.test"/>
+
+</manifest>
diff --git a/media-compat/tests/NO_DOCS b/media-compat/tests/NO_DOCS
new file mode 100644
index 0000000..092a39c
--- /dev/null
+++ b/media-compat/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
new file mode 100644
index 0000000..455a706
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/MediaSessionCompatTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.session;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Looper;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static junit.framework.Assert.fail;
+
+public class MediaSessionCompatTest {
+ @Rule
+ public ActivityTestRule<TestActivity> mActivityRule =
+ new ActivityTestRule<>(TestActivity.class);
+ Context mContext;
+ Map<String, LockedObject> results = new HashMap<>();
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getContext();
+ }
+
+ @Test
+ public void testSetNullCallback() throws Throwable {
+ initWait("testSetNullCallback");
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ MediaSessionCompat session = new MediaSessionCompat(mContext, "TEST");
+ session.setCallback(null);
+ } catch (Exception e) {
+ fail("Fail with an exception: " + e);
+ } finally {
+ setResultData("testSetNullCallback", true);
+ }
+ }
+ });
+ waitFor("testSetNullCallback");
+ }
+
+ private void initWait(String key) throws InterruptedException {
+ results.put(key, new LockedObject());
+ }
+
+ private Object[] waitFor(String key) throws InterruptedException {
+ return results.get(key).waitFor();
+ }
+
+ private void setResultData(String key, Object... args) {
+ if (results.containsKey(key)) {
+ results.get(key).set(args);
+ }
+ }
+
+ private class LockedObject {
+ private Semaphore mLock = new Semaphore(1);
+ private volatile Object[] mArgs;
+
+ public LockedObject() {
+ mLock.drainPermits();
+ }
+
+ public void set(Object... args) {
+ mArgs = args;
+ mLock.release(1);
+ }
+
+ public Object[] waitFor() throws InterruptedException {
+ mLock.tryAcquire(1, 2, TimeUnit.SECONDS);
+ return mArgs;
+ }
+ }
+}
diff --git a/media-compat/tests/src/android/support/v4/media/session/TestActivity.java b/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
new file mode 100644
index 0000000..dd56467
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/session/TestActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.session;
+
+import android.app.Activity;
+
+public class TestActivity extends Activity {
+}
diff --git a/samples/SupportLeanbackDemos/AndroidManifest.xml b/samples/SupportLeanbackDemos/AndroidManifest.xml
index 1dba568..ea3c1ee 100644
--- a/samples/SupportLeanbackDemos/AndroidManifest.xml
+++ b/samples/SupportLeanbackDemos/AndroidManifest.xml
@@ -43,6 +43,22 @@
android:theme="@style/Theme.Example.Leanback.Details"
android:exported="true" />
+ <activity android:name="DetailsVideoActivity"
+ android:theme="@style/Theme.Example.Leanback.Details"
+ android:exported="true" />
+
+ <activity android:name="DetailsVideoSupportActivity"
+ android:theme="@style/Theme.Example.Leanback.Details"
+ android:exported="true" />
+
+ <activity android:name="DetailsCustomTitleActivity"
+ android:theme="@style/Theme.Example.Leanback.Details.CustomTitle"
+ android:exported="true" />
+
+ <activity android:name="DetailsCustomTitleSupportActivity"
+ android:theme="@style/Theme.Example.Leanback.Details.CustomTitle"
+ android:exported="true" />
+
<activity android:name="SearchDetailsActivity"
android:theme="@style/Theme.Example.Leanback.SearchDetails"
android:exported="true" />
diff --git a/samples/SupportLeanbackDemos/generatev4.py b/samples/SupportLeanbackDemos/generatev4.py
index d4b3f22..c79f1b1 100755
--- a/samples/SupportLeanbackDemos/generatev4.py
+++ b/samples/SupportLeanbackDemos/generatev4.py
@@ -134,8 +134,6 @@
line = line.replace('DetailsActivity', 'DetailsSupportActivity')
line = line.replace('android.app.Activity', 'android.support.v4.app.FragmentActivity')
line = line.replace('extends Activity', 'extends FragmentActivity')
- line = line.replace('R.layout.details', 'R.layout.details_support')
- line = line.replace('R.layout.legacy_details', 'R.layout.legacy_details_support')
line = line.replace('getFragmentManager()', 'getSupportFragmentManager()')
line = line.replace('DetailsFragment', 'DetailsSupportFragment')
line = line.replace('NewDetailsFragment', 'NewDetailsSupportFragment')
@@ -143,26 +141,6 @@
file.close()
outfile.close()
-file = open('res/layout/details.xml', 'r')
-outfile = open('res/layout/details_support.xml', 'w')
-for line in file:
- line = replace_xml_head(line, "details")
- line = line.replace('com.example.android.leanback.NewDetailsFragment', 'com.example.android.leanback.NewDetailsSupportFragment')
- outfile.write(line)
-file.close()
-outfile.close()
-
-
-file = open('res/layout/legacy_details.xml', 'r')
-outfile = open('res/layout/legacy_details_support.xml', 'w')
-for line in file:
- line = replace_xml_head(line, "legacy_details")
- line = line.replace('com.example.android.leanback.DetailsFragment', 'com.example.android.leanback.DetailsSupportFragment')
- outfile.write(line)
-file.close()
-outfile.close()
-
-
file = open('src/com/example/android/leanback/SearchDetailsActivity.java', 'r')
outfile = open('src/com/example/android/leanback/SearchDetailsSupportActivity.java', 'w')
write_java_head(outfile, "SearchDetailsActivity")
diff --git a/samples/SupportLeanbackDemos/res/layout/custom_title.xml b/samples/SupportLeanbackDemos/res/layout/custom_title.xml
new file mode 100644
index 0000000..4a88021
--- /dev/null
+++ b/samples/SupportLeanbackDemos/res/layout/custom_title.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.example.android.leanback.CustomTitleView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/browse_title_group"
+ android:orientation="horizontal" android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TextView
+ android:text="@string/custom_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+</com.example.android.leanback.CustomTitleView>
\ No newline at end of file
diff --git a/samples/SupportLeanbackDemos/res/layout/details.xml b/samples/SupportLeanbackDemos/res/layout/details.xml
deleted file mode 100644
index 3159f54..0000000
--- a/samples/SupportLeanbackDemos/res/layout/details.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- 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.
--->
-
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
- android:name="com.example.android.leanback.NewDetailsFragment"
- android:id="@+id/details_fragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
-/>
diff --git a/samples/SupportLeanbackDemos/res/layout/legacy_details.xml b/samples/SupportLeanbackDemos/res/layout/details_activity.xml
similarity index 85%
rename from samples/SupportLeanbackDemos/res/layout/legacy_details.xml
rename to samples/SupportLeanbackDemos/res/layout/details_activity.xml
index 4af4e6a..7fc1400 100644
--- a/samples/SupportLeanbackDemos/res/layout/legacy_details.xml
+++ b/samples/SupportLeanbackDemos/res/layout/details_activity.xml
@@ -15,8 +15,7 @@
limitations under the License.
-->
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
- android:name="com.example.android.leanback.DetailsFragment"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/details_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/samples/SupportLeanbackDemos/res/layout/details_support.xml b/samples/SupportLeanbackDemos/res/layout/details_support.xml
deleted file mode 100644
index 103dc25..0000000
--- a/samples/SupportLeanbackDemos/res/layout/details_support.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This file is auto-generated from details.xml. DO NOT MODIFY. -->
-
-<!--
- 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.
--->
-
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
- android:name="com.example.android.leanback.NewDetailsSupportFragment"
- android:id="@+id/details_fragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
-/>
diff --git a/samples/SupportLeanbackDemos/res/layout/legacy_details_support.xml b/samples/SupportLeanbackDemos/res/layout/legacy_details_support.xml
deleted file mode 100644
index 8146f9b..0000000
--- a/samples/SupportLeanbackDemos/res/layout/legacy_details_support.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This file is auto-generated from legacy_details.xml. DO NOT MODIFY. -->
-
-<!--
- 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.
--->
-
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
- android:name="com.example.android.leanback.DetailsSupportFragment"
- android:id="@+id/details_fragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
-/>
diff --git a/samples/SupportLeanbackDemos/res/values/strings.xml b/samples/SupportLeanbackDemos/res/values/strings.xml
index 3774ebf..6de7d7b 100644
--- a/samples/SupportLeanbackDemos/res/values/strings.xml
+++ b/samples/SupportLeanbackDemos/res/values/strings.xml
@@ -29,8 +29,17 @@
<string name="search_support_description">SearchSupportFragment test</string>
<string name="details">Details</string>
<string name="details_description">DetailsFragment test</string>
+ <string name="details_video">Details with Video</string>
+ <string name="details_video_description">DetailsFragment with Video test</string>
+ <string name="details_custom_title">Details with custom title</string>
+ <string name="details_custom_title_description">DetailsFragment with custom title test</string>
<string name="details_support">Details(support version)</string>
<string name="details_support_description">DetailsSupportFragment test</string>
+ <string name="details_video_support">Details with Video(support version)</string>
+ <string name="details_video_support_description">DetailsFragment with Video test(support version)</string>
+ <string name="details_custom_title_support">Details with custom title(support version)</string>
+ <string name="details_custom_title_support_description">DetailsSupportFragment with custom title test</string>
+ <string name="custom_title">Custom Title</string>
<string name="search_details">Search Details</string>
<string name="search_details_description">Search style DetailsFragment test</string>
<string name="search_details_support">Search Details(support version)</string>
diff --git a/samples/SupportLeanbackDemos/res/values/themes.xml b/samples/SupportLeanbackDemos/res/values/themes.xml
index 458c5c0..58ae373 100644
--- a/samples/SupportLeanbackDemos/res/values/themes.xml
+++ b/samples/SupportLeanbackDemos/res/values/themes.xml
@@ -23,6 +23,9 @@
</style>
<style name="Theme.Example.Leanback.Details" parent="Theme.Leanback.Details">
</style>
+ <style name="Theme.Example.Leanback.Details.CustomTitle">
+ <item name="browseTitleViewLayout">@layout/custom_title</item>.
+ </style>
<style name="Theme.Example.Leanback.VerticalGrid" parent="Theme.Leanback.VerticalGrid">
</style>
<style name="Theme.Example.Leanback.Rows" parent="Theme.Leanback">
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/CustomTitleView.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/CustomTitleView.java
new file mode 100644
index 0000000..3ceb17d
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/CustomTitleView.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.example.android.leanback;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.v17.leanback.widget.SearchOrbView;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.support.v17.leanback.widget.TitleViewAdapter;
+
+public class CustomTitleView extends LinearLayout implements TitleViewAdapter.Provider {
+
+ private final TitleViewAdapter mTitleViewAdapter = new TitleViewAdapter() {
+ @Override
+ public View getSearchAffordanceView() {
+ return null;
+ }
+
+ @Override
+ public void setOnSearchClickedListener(View.OnClickListener listener) {
+ }
+
+ @Override
+ public void setAnimationEnabled(boolean enable) {
+ }
+
+ @Override
+ public Drawable getBadgeDrawable() {
+ return null;
+ }
+
+ @Override
+ public SearchOrbView.Colors getSearchAffordanceColors() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getTitle() {
+ return null;
+ }
+
+ @Override
+ public void setBadgeDrawable(Drawable drawable) {
+ }
+
+ @Override
+ public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
+ }
+
+ @Override
+ public void setTitle(CharSequence titleText) {
+ }
+
+ @Override
+ public void updateComponentsVisibility(int flags) {
+ }
+ };
+
+ public CustomTitleView(Context context) {
+ this(context, null);
+ }
+
+ public CustomTitleView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public TitleViewAdapter getTitleViewAdapter() {
+ return mTitleViewAdapter;
+ }
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsActivity.java
index 43c6910..6e2cbb4 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsActivity.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsActivity.java
@@ -26,22 +26,41 @@
&& !(this instanceof SearchDetailsActivity));
}
+ protected boolean hasBackgroundVideo() {
+ return false;
+ }
+
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getFragmentManager().enableDebugLogging(true);
- setContentView(useLegacyFragment() ? R.layout.legacy_details : R.layout.details);
+ setContentView(R.layout.details_activity);
if (savedInstanceState == null) {
- // Only pass object to fragment when activity is first time created,
- // later object is modified and persisted with fragment state.
if (useLegacyFragment()) {
- ((DetailsFragment)getFragmentManager().findFragmentById(R.id.details_fragment))
- .setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ DetailsFragment fragment = new DetailsFragment();
+ fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ getFragmentManager().beginTransaction()
+ .replace(R.id.details_fragment, fragment)
+ .commit();
} else {
- ((NewDetailsFragment)getFragmentManager().findFragmentById(R.id.details_fragment))
- .setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ NewDetailsFragment fragment = new NewDetailsFragment();
+ fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ fragment.setBackgroundVideo(hasBackgroundVideo());
+ getFragmentManager().beginTransaction()
+ .replace(R.id.details_fragment, fragment)
+ .commit();
+ }
+ } else {
+ if (useLegacyFragment()) {
+ DetailsFragment fragment = (DetailsFragment) getFragmentManager()
+ .findFragmentById(R.id.details_fragment);
+ fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ } else {
+ NewDetailsFragment fragment = (NewDetailsFragment) getFragmentManager()
+ .findFragmentById(R.id.details_fragment);
+ fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
}
}
}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleActivity.java
new file mode 100644
index 0000000..0796fb7
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleActivity.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.example.android.leanback;
+
+/**
+ * Same function as DetailsActivity, using different theme in AndroidManifest.
+ */
+public class DetailsCustomTitleActivity extends DetailsActivity {
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleSupportActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleSupportActivity.java
new file mode 100644
index 0000000..4ad2f7a
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsCustomTitleSupportActivity.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.example.android.leanback;
+
+/**
+ * Same function as DetailsSupportActivity, using different theme in AndroidManifest.
+ */
+public class DetailsCustomTitleSupportActivity extends DetailsSupportActivity {
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsFragment.java
index 09d9526..56acc05 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsFragment.java
@@ -126,12 +126,7 @@
ps.addClassPresenter(ListRow.class, new ListRowPresenter());
mRowsAdapter = new ArrayObjectAdapter(ps);
-
- PhotoItem item = (PhotoItem) (savedInstanceState != null ?
- savedInstanceState.getParcelable(ITEM) : null);
- if (item != null) {
- setItem(item);
- }
+ updateAdapter();
setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
@@ -179,7 +174,13 @@
public void setItem(PhotoItem photoItem) {
mPhotoItem = photoItem;
+ updateAdapter();
+ }
+ void updateAdapter() {
+ if (mRowsAdapter == null) {
+ return;
+ }
mRowsAdapter.clear();
new Handler().postDelayed(new Runnable() {
public void run() {
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportActivity.java
index 3ebf101..48d15e5 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportActivity.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportActivity.java
@@ -29,22 +29,41 @@
&& !(this instanceof SearchDetailsSupportActivity));
}
+ protected boolean hasBackgroundVideo() {
+ return false;
+ }
+
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getSupportFragmentManager().enableDebugLogging(true);
- setContentView(useLegacyFragment() ? R.layout.legacy_details_support : R.layout.details_support);
+ setContentView(R.layout.details_activity);
if (savedInstanceState == null) {
- // Only pass object to fragment when activity is first time created,
- // later object is modified and persisted with fragment state.
if (useLegacyFragment()) {
- ((DetailsSupportFragment)getSupportFragmentManager().findFragmentById(R.id.details_fragment))
- .setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ DetailsSupportFragment fragment = new DetailsSupportFragment();
+ fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.details_fragment, fragment)
+ .commit();
} else {
- ((NewDetailsSupportFragment)getSupportFragmentManager().findFragmentById(R.id.details_fragment))
- .setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ NewDetailsSupportFragment fragment = new NewDetailsSupportFragment();
+ fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ fragment.setBackgroundVideo(hasBackgroundVideo());
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.details_fragment, fragment)
+ .commit();
+ }
+ } else {
+ if (useLegacyFragment()) {
+ DetailsSupportFragment fragment = (DetailsSupportFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.details_fragment);
+ fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
+ } else {
+ NewDetailsSupportFragment fragment = (NewDetailsSupportFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.details_fragment);
+ fragment.setItem((PhotoItem) getIntent().getParcelableExtra(EXTRA_ITEM));
}
}
}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportFragment.java
index 28a61d9..e58e2e7 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsSupportFragment.java
@@ -129,12 +129,7 @@
ps.addClassPresenter(ListRow.class, new ListRowPresenter());
mRowsAdapter = new ArrayObjectAdapter(ps);
-
- PhotoItem item = (PhotoItem) (savedInstanceState != null ?
- savedInstanceState.getParcelable(ITEM) : null);
- if (item != null) {
- setItem(item);
- }
+ updateAdapter();
setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
@@ -182,7 +177,13 @@
public void setItem(PhotoItem photoItem) {
mPhotoItem = photoItem;
+ updateAdapter();
+ }
+ void updateAdapter() {
+ if (mRowsAdapter == null) {
+ return;
+ }
mRowsAdapter.clear();
new Handler().postDelayed(new Runnable() {
public void run() {
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoActivity.java
new file mode 100644
index 0000000..06b65b7
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.example.android.leanback;
+
+public class DetailsVideoActivity extends DetailsActivity {
+
+ @Override
+ protected boolean hasBackgroundVideo() {
+ return true;
+ }
+
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoSupportActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoSupportActivity.java
new file mode 100644
index 0000000..0c31d36
--- /dev/null
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/DetailsVideoSupportActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.example.android.leanback;
+
+public class DetailsVideoSupportActivity extends DetailsSupportActivity {
+
+ @Override
+ protected boolean hasBackgroundVideo() {
+ return true;
+ }
+
+}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
index 83e56d6..fcd8e65 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/MainActivity.java
@@ -61,12 +61,35 @@
R.string.browse_support_description);
addAction(actions, SearchActivity.class, R.string.search, R.string.search_description);
addAction(actions, SearchSupportActivity.class, R.string.search_support, R.string.search_support_description);
- addAction(actions, DetailsActivity.class, R.string.details, R.string.details_description);
+
+ addAction(actions, DetailsActivity.class, R.string.details,
+ R.string.details_description);
actions.get(actions.size()-1).getIntent().putExtra(DetailsActivity.EXTRA_ITEM,
new PhotoItem("Hello world", R.drawable.gallery_photo_1));
- addAction(actions, DetailsSupportActivity.class, R.string.details_support, R.string.details_support_description);
+ addAction(actions, DetailsSupportActivity.class, R.string.details_support,
+ R.string.details_support_description);
actions.get(actions.size()-1).getIntent().putExtra(DetailsSupportActivity.EXTRA_ITEM,
new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+
+ addAction(actions, DetailsVideoActivity.class, R.string.details_video,
+ R.string.details_video_description);
+ actions.get(actions.size()-1).getIntent().putExtra(DetailsActivity.EXTRA_ITEM,
+ new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+ addAction(actions, DetailsVideoSupportActivity.class, R.string.details_video_support,
+ R.string.details_video_support_description);
+ actions.get(actions.size()-1).getIntent().putExtra(DetailsSupportActivity.EXTRA_ITEM,
+ new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+
+ addAction(actions, DetailsCustomTitleActivity.class, R.string.details_custom_title,
+ R.string.details_custom_title_description);
+ actions.get(actions.size()-1).getIntent().putExtra(DetailsActivity.EXTRA_ITEM,
+ new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+ addAction(actions, DetailsCustomTitleSupportActivity.class,
+ R.string.details_custom_title_support,
+ R.string.details_custom_title_support_description);
+ actions.get(actions.size()-1).getIntent().putExtra(DetailsSupportActivity.EXTRA_ITEM,
+ new PhotoItem("Hello world", R.drawable.gallery_photo_1));
+
addAction(actions, SearchDetailsActivity.class, R.string.search_details,
R.string.search_details_description);
actions.get(actions.size()-1).getIntent().putExtra(SearchDetailsActivity.EXTRA_ITEM,
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
index 75fa941..395d557 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsFragment.java
@@ -20,7 +20,6 @@
import android.os.Handler;
import android.support.v17.leanback.app.DetailsBackgroundParallaxHelper;
import android.support.v17.leanback.app.DetailsFragmentVideoHelper;
-import android.support.v17.leanback.app.VideoFragment;
import android.support.v17.leanback.media.MediaPlayerGlue;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -63,6 +62,7 @@
private boolean TEST_OVERVIEW_ROW_ON_SECOND;
private boolean TEST_SHARED_ELEMENT_TRANSITION;
private boolean TEST_ENTRANCE_TRANSITION;
+ private boolean TEST_BACKGROUND_PLAYER;
private static final long TIME_TO_LOAD_OVERVIEW_ROW_MS = 1000;
private static final long TIME_TO_LOAD_RELATED_ROWS_MS = 2000;
@@ -77,7 +77,6 @@
private BackgroundHelper mBackgroundHelper;
private int mBitmapMinVerticalOffset = -100;
private MediaPlayerGlue mMediaPlayerGlue;
- private VideoFragment mVideoFragment;
private void initializeTest() {
TEST_SHARED_ELEMENT_TRANSITION = null != getActivity().getWindow()
@@ -97,27 +96,32 @@
getActivity(), getParallaxManager())
.setCoverImageMinVerticalOffset(mBitmapMinVerticalOffset)
.build();
- mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
- mMediaPlayerGlue.setHost(createPlaybackGlueHost());
- mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
- mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
+ if (TEST_BACKGROUND_PLAYER) {
+ mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
+ mMediaPlayerGlue.setHost(createPlaybackGlueHost());
+ mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
+ mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
- mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
- mMediaPlayerGlue.setArtist("A Googleer");
- mMediaPlayerGlue.setTitle("Diving with Sharks");
- mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
+ mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ mMediaPlayerGlue.setArtist("A Googleer");
+ mMediaPlayerGlue.setTitle("Diving with Sharks");
+ mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
+
+ }
final Context context = getActivity();
setBadgeDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_title,
context.getTheme()));
setTitle("Leanback Sample App");
- setOnSearchClickedListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Intent intent = new Intent(getActivity(), SearchActivity.class);
- startActivity(intent);
- }
- });
+ if (!TEST_BACKGROUND_PLAYER) {
+ setOnSearchClickedListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(getActivity(), SearchActivity.class);
+ startActivity(intent);
+ }
+ });
+ }
mActionPlay = new Action(ACTION_PLAY, "Play");
mActionRent = new Action(ACTION_RENT, "Rent", "$3.99", ResourcesCompat.getDrawable(
@@ -165,12 +169,7 @@
ps.addClassPresenter(ListRow.class, new ListRowPresenter());
mRowsAdapter = new ArrayObjectAdapter(ps);
-
- PhotoItem item = (PhotoItem) (savedInstanceState != null ?
- savedInstanceState.getParcelable(ITEM) : null);
- if (item != null) {
- setItem(item);
- }
+ updateAdapter();
setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
@@ -223,9 +222,19 @@
return view;
}
+ public void setBackgroundVideo(boolean backgroundVideo) {
+ TEST_BACKGROUND_PLAYER = backgroundVideo;
+ }
+
public void setItem(PhotoItem photoItem) {
mPhotoItem = photoItem;
+ updateAdapter();
+ }
+ void updateAdapter() {
+ if (mRowsAdapter == null) {
+ return;
+ }
mRowsAdapter.clear();
new Handler().postDelayed(new Runnable() {
public void run() {
@@ -295,6 +304,8 @@
@Override
public void onStop() {
super.onStop();
- mMediaPlayerGlue.pause();
+ if (TEST_BACKGROUND_PLAYER) {
+ mMediaPlayerGlue.pause();
+ }
}
}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
index 11d8b7e..ba482b1 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/NewDetailsSupportFragment.java
@@ -23,7 +23,6 @@
import android.os.Handler;
import android.support.v17.leanback.app.DetailsBackgroundParallaxHelper;
import android.support.v17.leanback.app.DetailsFragmentVideoHelper;
-import android.support.v17.leanback.app.VideoSupportFragment;
import android.support.v17.leanback.media.MediaPlayerGlue;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -66,6 +65,7 @@
private boolean TEST_OVERVIEW_ROW_ON_SECOND;
private boolean TEST_SHARED_ELEMENT_TRANSITION;
private boolean TEST_ENTRANCE_TRANSITION;
+ private boolean TEST_BACKGROUND_PLAYER;
private static final long TIME_TO_LOAD_OVERVIEW_ROW_MS = 1000;
private static final long TIME_TO_LOAD_RELATED_ROWS_MS = 2000;
@@ -80,7 +80,6 @@
private BackgroundHelper mBackgroundHelper;
private int mBitmapMinVerticalOffset = -100;
private MediaPlayerGlue mMediaPlayerGlue;
- private VideoSupportFragment mVideoSupportFragment;
private void initializeTest() {
TEST_SHARED_ELEMENT_TRANSITION = null != getActivity().getWindow()
@@ -100,27 +99,32 @@
getActivity(), getParallaxManager())
.setCoverImageMinVerticalOffset(mBitmapMinVerticalOffset)
.build();
- mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
- mMediaPlayerGlue.setHost(createPlaybackGlueHost());
- mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
- mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
+ if (TEST_BACKGROUND_PLAYER) {
+ mMediaPlayerGlue = new MediaPlayerGlue(getActivity());
+ mMediaPlayerGlue.setHost(createPlaybackGlueHost());
+ mVideoHelper = new DetailsFragmentVideoHelper(mMediaPlayerGlue, getParallaxManager());
+ mVideoHelper.setBackgroundDrawable(mParallaxHelper.getCoverImageDrawable());
- mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
- mMediaPlayerGlue.setArtist("A Googleer");
- mMediaPlayerGlue.setTitle("Diving with Sharks");
- mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
+ mMediaPlayerGlue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ mMediaPlayerGlue.setArtist("A Googleer");
+ mMediaPlayerGlue.setTitle("Diving with Sharks");
+ mMediaPlayerGlue.setVideoUrl("http://techslides.com/demos/sample-videos/small.mp4");
+
+ }
final Context context = getActivity();
setBadgeDrawable(ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_title,
context.getTheme()));
setTitle("Leanback Sample App");
- setOnSearchClickedListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Intent intent = new Intent(getActivity(), SearchSupportActivity.class);
- startActivity(intent);
- }
- });
+ if (!TEST_BACKGROUND_PLAYER) {
+ setOnSearchClickedListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent intent = new Intent(getActivity(), SearchSupportActivity.class);
+ startActivity(intent);
+ }
+ });
+ }
mActionPlay = new Action(ACTION_PLAY, "Play");
mActionRent = new Action(ACTION_RENT, "Rent", "$3.99", ResourcesCompat.getDrawable(
@@ -168,12 +172,7 @@
ps.addClassPresenter(ListRow.class, new ListRowPresenter());
mRowsAdapter = new ArrayObjectAdapter(ps);
-
- PhotoItem item = (PhotoItem) (savedInstanceState != null ?
- savedInstanceState.getParcelable(ITEM) : null);
- if (item != null) {
- setItem(item);
- }
+ updateAdapter();
setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
@@ -226,9 +225,19 @@
return view;
}
+ public void setBackgroundVideo(boolean backgroundVideo) {
+ TEST_BACKGROUND_PLAYER = backgroundVideo;
+ }
+
public void setItem(PhotoItem photoItem) {
mPhotoItem = photoItem;
+ updateAdapter();
+ }
+ void updateAdapter() {
+ if (mRowsAdapter == null) {
+ return;
+ }
mRowsAdapter.clear();
new Handler().postDelayed(new Runnable() {
public void run() {
@@ -298,6 +307,8 @@
@Override
public void onStop() {
super.onStop();
- mMediaPlayerGlue.pause();
+ if (TEST_BACKGROUND_PLAYER) {
+ mMediaPlayerGlue.pause();
+ }
}
}
diff --git a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlaySupportFragment.java b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlaySupportFragment.java
index f51513c..b806a9a 100644
--- a/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlaySupportFragment.java
+++ b/samples/SupportLeanbackDemos/src/com/example/android/leanback/PlaybackOverlaySupportFragment.java
@@ -140,7 +140,6 @@
mGlue.setInitialized(true);
}
}, MEDIA_PREPARATION_DELAY);
-
mGlue.setOnItemViewClickedListener(mOnItemViewClickedListener);
mPlaybackControlsRowPresenter = mGlue.createControlsRowAndPresenter();
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index 214ae64..fcb3fa9 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -191,6 +191,7 @@
getChildFragmentManager().beginTransaction()
.replace(R.id.details_rows_dock, mRowsFragment).commit();
}
+ installTitleView(inflater, mRootView, savedInstanceState);
mRowsFragment.setAdapter(mAdapter);
mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
@@ -378,8 +379,11 @@
void onRowSelected(int selectedPosition, int selectedSubPosition) {
ObjectAdapter adapter = getAdapter();
- if (adapter == null || adapter.size() == 0
- || (selectedPosition == 0 && selectedSubPosition == 0)) {
+ if (( mRowsFragment != null && mRowsFragment.getView() != null
+ && mRowsFragment.getView().hasFocus())
+ && (adapter == null || adapter.size() == 0
+ || (getVerticalGridView().getSelectedPosition() == 0
+ && getVerticalGridView().getSelectedSubPosition() == 0))) {
showTitle(true);
} else {
showTitle(false);
@@ -548,20 +552,31 @@
mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() {
@Override
public View onFocusSearch(View focused, int direction) {
- if (mVideoFragment == null) {
- return null;
- }
if (mRowsFragment.getVerticalGridView() != null
&& mRowsFragment.getVerticalGridView().hasFocus()) {
if (direction == View.FOCUS_UP) {
- slideOutGridView();
- return mVideoFragment.getView();
+ if (mVideoFragment != null && mVideoFragment.getView() != null) {
+ slideOutGridView();
+ showTitle(false);
+ return mVideoFragment.getView();
+ } else if (getTitleView() != null) {
+ return getTitleView();
+ }
}
- } else if (mVideoFragment.getView() != null
+ } else if (mVideoFragment != null && mVideoFragment.getView() != null
&& mVideoFragment.getView().hasFocus()) {
if (direction == View.FOCUS_DOWN) {
- slideInGridView();
- return mRowsFragment.getVerticalGridView();
+ if (mRowsFragment.getVerticalGridView() != null) {
+ showTitle(true);
+ slideInGridView();
+ return mRowsFragment.getVerticalGridView();
+ }
+ }
+ } else if (getTitleView() != null && getTitleView().hasFocus()) {
+ if (direction == View.FOCUS_DOWN) {
+ if (mRowsFragment.getVerticalGridView() != null) {
+ return mRowsFragment.getVerticalGridView();
+ }
}
}
return focused;
@@ -579,6 +594,7 @@
if (mVideoFragment != null && mVideoFragment.getView() != null
&& mVideoFragment.getView().hasFocus()) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
+ showTitle(true);
slideInGridView();
getVerticalGridView().requestFocus();
return true;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
index a969f1c..a9bbf28 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -194,6 +194,7 @@
getChildFragmentManager().beginTransaction()
.replace(R.id.details_rows_dock, mRowsSupportFragment).commit();
}
+ installTitleView(inflater, mRootView, savedInstanceState);
mRowsSupportFragment.setAdapter(mAdapter);
mRowsSupportFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
mRowsSupportFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
@@ -381,8 +382,11 @@
void onRowSelected(int selectedPosition, int selectedSubPosition) {
ObjectAdapter adapter = getAdapter();
- if (adapter == null || adapter.size() == 0
- || (selectedPosition == 0 && selectedSubPosition == 0)) {
+ if (( mRowsSupportFragment != null && mRowsSupportFragment.getView() != null
+ && mRowsSupportFragment.getView().hasFocus())
+ && (adapter == null || adapter.size() == 0
+ || (getVerticalGridView().getSelectedPosition() == 0
+ && getVerticalGridView().getSelectedSubPosition() == 0))) {
showTitle(true);
} else {
showTitle(false);
@@ -551,20 +555,31 @@
mRootView.setOnFocusSearchListener(new BrowseFrameLayout.OnFocusSearchListener() {
@Override
public View onFocusSearch(View focused, int direction) {
- if (mVideoSupportFragment == null) {
- return null;
- }
if (mRowsSupportFragment.getVerticalGridView() != null
&& mRowsSupportFragment.getVerticalGridView().hasFocus()) {
if (direction == View.FOCUS_UP) {
- slideOutGridView();
- return mVideoSupportFragment.getView();
+ if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null) {
+ slideOutGridView();
+ showTitle(false);
+ return mVideoSupportFragment.getView();
+ } else if (getTitleView() != null) {
+ return getTitleView();
+ }
}
- } else if (mVideoSupportFragment.getView() != null
+ } else if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null
&& mVideoSupportFragment.getView().hasFocus()) {
if (direction == View.FOCUS_DOWN) {
- slideInGridView();
- return mRowsSupportFragment.getVerticalGridView();
+ if (mRowsSupportFragment.getVerticalGridView() != null) {
+ showTitle(true);
+ slideInGridView();
+ return mRowsSupportFragment.getVerticalGridView();
+ }
+ }
+ } else if (getTitleView() != null && getTitleView().hasFocus()) {
+ if (direction == View.FOCUS_DOWN) {
+ if (mRowsSupportFragment.getVerticalGridView() != null) {
+ return mRowsSupportFragment.getVerticalGridView();
+ }
}
}
return focused;
@@ -582,6 +597,7 @@
if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null
&& mVideoSupportFragment.getView().hasFocus()) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
+ showTitle(true);
slideInGridView();
getVerticalGridView().requestFocus();
return true;
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
index a0027ec..02a0257 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackFragment.java
@@ -31,6 +31,7 @@
import android.support.v17.leanback.media.PlaybackGlueHost;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
+import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.ItemBridgeAdapter;
import android.support.v17.leanback.widget.ObjectAdapter;
@@ -111,25 +112,43 @@
private ObjectAdapter mAdapter;
private PlaybackRowPresenter mPresenter;
private Row mRow;
+ private BaseOnItemViewSelectedListener mExternalItemSelectedListener;
private BaseOnItemViewClickedListener mExternalItemClickedListener;
private BaseOnItemViewClickedListener mPlaybackItemClickedListener;
- private BaseOnItemViewClickedListener mOnItemViewClickedListener = new BaseOnItemViewClickedListener() {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder,
- Object item,
- RowPresenter.ViewHolder rowViewHolder,
- Object row) {
- if (mPlaybackItemClickedListener != null
- && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
- mPlaybackItemClickedListener.onItemClicked(
- itemViewHolder, item, rowViewHolder, row);
- }
- if (mExternalItemClickedListener != null) {
- mExternalItemClickedListener.onItemClicked(
- itemViewHolder, item, rowViewHolder, row);
- }
- }
- };
+
+ private final BaseOnItemViewClickedListener mOnItemViewClickedListener =
+ new BaseOnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder,
+ Object item,
+ RowPresenter.ViewHolder rowViewHolder,
+ Object row) {
+ if (mPlaybackItemClickedListener != null
+ && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
+ mPlaybackItemClickedListener.onItemClicked(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ if (mExternalItemClickedListener != null) {
+ mExternalItemClickedListener.onItemClicked(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ }
+ };
+
+ private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener =
+ new BaseOnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder,
+ Object item,
+ RowPresenter.ViewHolder rowViewHolder,
+ Object row) {
+ if (mExternalItemSelectedListener != null) {
+ mExternalItemSelectedListener.onItemSelected(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ }
+ };
+
private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
public ObjectAdapter getAdapter() {
@@ -743,6 +762,7 @@
} else {
mRowsFragment.setAdapter(mAdapter);
}
+ mRowsFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
mBgAlpha = 255;
@@ -786,6 +806,15 @@
}
/**
+ * This listener is called every time there is a selection in {@link RowsFragment}. This can
+ * be used by users to take additional actions such as animations.
+ * @hide
+ */
+ public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
+ mExternalItemSelectedListener = listener;
+ }
+
+ /**
* This listener is called every time there is a click in {@link RowsFragment}. This can
* be used by users to take additional actions such as animations.
*/
diff --git a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
index f21bd4e..07701f9 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -34,6 +34,7 @@
import android.support.v17.leanback.media.PlaybackGlueHost;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
+import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.support.v17.leanback.widget.ItemBridgeAdapter;
import android.support.v17.leanback.widget.ObjectAdapter;
@@ -114,25 +115,43 @@
private ObjectAdapter mAdapter;
private PlaybackRowPresenter mPresenter;
private Row mRow;
+ private BaseOnItemViewSelectedListener mExternalItemSelectedListener;
private BaseOnItemViewClickedListener mExternalItemClickedListener;
private BaseOnItemViewClickedListener mPlaybackItemClickedListener;
- private BaseOnItemViewClickedListener mOnItemViewClickedListener = new BaseOnItemViewClickedListener() {
- @Override
- public void onItemClicked(Presenter.ViewHolder itemViewHolder,
- Object item,
- RowPresenter.ViewHolder rowViewHolder,
- Object row) {
- if (mPlaybackItemClickedListener != null
- && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
- mPlaybackItemClickedListener.onItemClicked(
- itemViewHolder, item, rowViewHolder, row);
- }
- if (mExternalItemClickedListener != null) {
- mExternalItemClickedListener.onItemClicked(
- itemViewHolder, item, rowViewHolder, row);
- }
- }
- };
+
+ private final BaseOnItemViewClickedListener mOnItemViewClickedListener =
+ new BaseOnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder,
+ Object item,
+ RowPresenter.ViewHolder rowViewHolder,
+ Object row) {
+ if (mPlaybackItemClickedListener != null
+ && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
+ mPlaybackItemClickedListener.onItemClicked(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ if (mExternalItemClickedListener != null) {
+ mExternalItemClickedListener.onItemClicked(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ }
+ };
+
+ private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener =
+ new BaseOnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder,
+ Object item,
+ RowPresenter.ViewHolder rowViewHolder,
+ Object row) {
+ if (mExternalItemSelectedListener != null) {
+ mExternalItemSelectedListener.onItemSelected(
+ itemViewHolder, item, rowViewHolder, row);
+ }
+ }
+ };
+
private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
public ObjectAdapter getAdapter() {
@@ -746,6 +765,7 @@
} else {
mRowsSupportFragment.setAdapter(mAdapter);
}
+ mRowsSupportFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
mRowsSupportFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
mBgAlpha = 255;
@@ -789,6 +809,15 @@
}
/**
+ * This listener is called every time there is a selection in {@link RowsSupportFragment}. This can
+ * be used by users to take additional actions such as animations.
+ * @hide
+ */
+ public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
+ mExternalItemSelectedListener = listener;
+ }
+
+ /**
* This listener is called every time there is a click in {@link RowsSupportFragment}. This can
* be used by users to take additional actions such as animations.
*/
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
index ca486ed..d3a45a0 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
@@ -133,12 +133,13 @@
static final String TAG = "RowsFragment";
static final boolean DEBUG = false;
+ static final int ALIGN_TOP_NOT_SET = Integer.MIN_VALUE;
ItemBridgeAdapter.ViewHolder mSelectedViewHolder;
private int mSubPosition;
boolean mExpand = true;
boolean mViewsCreated;
- private int mAlignedTop;
+ private int mAlignedTop = ALIGN_TOP_NOT_SET;
boolean mAfterEntranceTransition = true;
BaseOnItemViewSelectedListener mOnItemViewSelectedListener;
@@ -540,6 +541,9 @@
@Override
public void setAlignment(int windowAlignOffsetFromTop) {
+ if (windowAlignOffsetFromTop == ALIGN_TOP_NOT_SET) {
+ return;
+ }
mAlignedTop = windowAlignOffsetFromTop;
final VerticalGridView gridView = getVerticalGridView();
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
index d6c8d3f..5582644 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
@@ -136,12 +136,13 @@
static final String TAG = "RowsSupportFragment";
static final boolean DEBUG = false;
+ static final int ALIGN_TOP_NOT_SET = Integer.MIN_VALUE;
ItemBridgeAdapter.ViewHolder mSelectedViewHolder;
private int mSubPosition;
boolean mExpand = true;
boolean mViewsCreated;
- private int mAlignedTop;
+ private int mAlignedTop = ALIGN_TOP_NOT_SET;
boolean mAfterEntranceTransition = true;
BaseOnItemViewSelectedListener mOnItemViewSelectedListener;
@@ -543,6 +544,9 @@
@Override
public void setAlignment(int windowAlignOffsetFromTop) {
+ if (windowAlignOffsetFromTop == ALIGN_TOP_NOT_SET) {
+ return;
+ }
mAlignedTop = windowAlignOffsetFromTop;
final VerticalGridView gridView = getVerticalGridView();
diff --git a/v17/leanback/tests/AndroidManifest.xml b/v17/leanback/tests/AndroidManifest.xml
index 16b78cb..6e6b157 100644
--- a/v17/leanback/tests/AndroidManifest.xml
+++ b/v17/leanback/tests/AndroidManifest.xml
@@ -38,6 +38,10 @@
android:theme="@style/Theme.Leanback.GuidedStep"
android:exported="true" />
+ <activity android:name="android.support.v17.leanback.app.RowsFragmentTestActivity"
+ android:theme="@style/Theme.Leanback"
+ android:exported="true" />
+
<activity android:name="android.support.v17.leanback.app.BrowseFragmentTestActivity"
android:theme="@style/Theme.Leanback.Browse"
android:exported="true" />
@@ -46,6 +50,10 @@
android:theme="@style/Theme.Leanback"
android:exported="true" />
+ <activity android:name="android.support.v17.leanback.app.RowsSupportFragmentTestActivity"
+ android:theme="@style/Theme.Leanback"
+ android:exported="true" />
+
<activity android:name="android.support.v17.leanback.app.BrowseSupportFragmentTestActivity"
android:theme="@style/Theme.Leanback.Browse"
android:exported="true" />
@@ -58,6 +66,14 @@
android:theme="@style/Theme.Leanback.GuidedStep"
android:exported="true" />
+ <activity android:name="android.support.v17.leanback.app.PlaybackTestActivity"
+ android:theme="@style/Theme.Leanback"
+ android:exported="true" />
+
+ <activity android:name="android.support.v17.leanback.app.PlaybackSupportTestActivity"
+ android:theme="@style/Theme.Leanback"
+ android:exported="true" />
+
<activity android:name="android.support.v17.leanback.app.PlaybackOverlayTestActivity"
android:theme="@style/Theme.Leanback"
android:exported="true" />
diff --git a/v17/leanback/tests/generatev4.py b/v17/leanback/tests/generatev4.py
index 3e25503..ecae656 100755
--- a/v17/leanback/tests/generatev4.py
+++ b/v17/leanback/tests/generatev4.py
@@ -19,11 +19,11 @@
print "Generate v4 fragment related code for leanback"
-files = ['BrowseTest', 'GuidedStepTest']
+files = ['BrowseTest', 'GuidedStepTest', 'RowsTest']
cls = ['BrowseTest', 'Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
'PlaybackOverlay', 'Rows', 'Search', 'VerticalGrid', 'Branded',
- 'GuidedStepTest', 'GuidedStep']
+ 'GuidedStepTest', 'GuidedStep', 'RowsTest']
for w in files:
print "copy {}Fragment to {}SupportFragment".format(w, w)
@@ -68,7 +68,7 @@
file.close()
outfile.close()
-testcls = ['Browse', 'GuidedStep', 'VerticalGrid']
+testcls = ['Browse', 'GuidedStep', 'VerticalGrid', 'Rows']
for w in testcls:
print "copy {}FrgamentTest to {}SupportFragmentTest".format(w, w)
@@ -95,7 +95,7 @@
file.close()
outfile.close()
-testcls = ['Browse', 'GuidedStep']
+testcls = ['Browse', 'GuidedStep', 'Rows']
for w in testcls:
print "copy {}FragmentTestActivity to {}SupportFragmentTestActivity".format(w, w)
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
index c6662ab..c24a7e5 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
@@ -16,6 +16,7 @@
package android.support.v17.leanback.app;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import android.content.Intent;
import android.graphics.Rect;
@@ -87,6 +88,9 @@
bitmapDrawable.getBounds().height());
assertEquals(0, bitmapDrawable.getVerticalOffset());
+ assertTrue("TitleView is visible", detailsFragment.getView()
+ .findViewById(R.id.browse_title_group).getVisibility() == View.VISIBLE);
+
activityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -97,7 +101,9 @@
PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
@Override
public boolean canProceed() {
- return bitmapDrawable.getVerticalOffset() == mDefaultVerticalOffset;
+ return bitmapDrawable.getVerticalOffset() == mDefaultVerticalOffset
+ && detailsFragment.getView()
+ .findViewById(R.id.browse_title_group).getVisibility() != View.VISIBLE;
}
});
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
new file mode 100644
index 0000000..bf7077b
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackFragmentTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.view.KeyEvent;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PlaybackFragmentTest {
+
+ private static final String TAG = "PlaybackFragmentTest";
+ private static final long TRANSITION_LENGTH = 1000;
+
+ @Rule
+ public ActivityTestRule<PlaybackTestActivity> activityTestRule =
+ new ActivityTestRule<>(PlaybackTestActivity.class, false, false);
+ private PlaybackTestActivity mActivity;
+
+ @Test
+ public void testSelectedListener() throws Throwable {
+ Intent intent = new Intent();
+ mActivity = activityTestRule.launchActivity(intent);
+ PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+ assertTrue(fragment.getView().hasFocus());
+
+ OnItemViewSelectedListener selectedListener = Mockito.mock(
+ OnItemViewSelectedListener.class);
+ fragment.setOnItemViewSelectedListener(selectedListener);
+
+
+ PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+ SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+ controlsRow.getPrimaryActionsAdapter();
+
+ PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+ PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+ PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+ ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+ ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+ ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+ ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+ ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+ // First navigate left within PlaybackControlsRow items.
+ verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The selected action should be rewind", rewind, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+ // Now navigate down to a ListRow item.
+ ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same list row should be passed to the listener", listRow0,
+ rowCaptor.getValue());
+ // Depending on the focusSearch algorithm, one of the items in the first ListRow must be
+ // selected.
+ boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+ || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+ assertTrue("None of the items in the first ListRow are passed to the selected listener.",
+ listRowItemPassed);
+ }
+
+ @Test
+ public void testClickedListener() throws Throwable {
+ Intent intent = new Intent();
+ mActivity = activityTestRule.launchActivity(intent);
+ PlaybackTestFragment fragment = mActivity.getPlaybackFragment();
+ assertTrue(fragment.getView().hasFocus());
+
+ OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
+ fragment.setOnItemViewClickedListener(clickedListener);
+
+
+ PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+ SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+ controlsRow.getPrimaryActionsAdapter();
+
+ PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+ PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+ PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+ ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+ ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+ ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+ ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+ ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+ // First navigate left within PlaybackControlsRow items.
+ verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+ // Now navigate down to a ListRow item.
+ ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same list row should be passed to the listener", listRow0,
+ rowCaptor.getValue());
+ boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+ || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+ assertTrue("None of the items in the first ListRow are passed to the click listener.",
+ listRowItemPassed);
+ }
+
+ private void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
new file mode 100644
index 0000000..fdba125
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportFragmentTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.view.KeyEvent;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class PlaybackSupportFragmentTest {
+
+ private static final String TAG = "PlaybackSupportFragmentTest";
+ private static final long TRANSITION_LENGTH = 1000;
+
+ @Rule
+ public ActivityTestRule<PlaybackSupportTestActivity> activityTestRule =
+ new ActivityTestRule<>(PlaybackSupportTestActivity.class, false, false);
+ private PlaybackSupportTestActivity mActivity;
+
+ @Test
+ public void testSelectedListener() throws Throwable {
+ Intent intent = new Intent();
+ mActivity = activityTestRule.launchActivity(intent);
+ PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+ assertTrue(fragment.getView().hasFocus());
+
+ OnItemViewSelectedListener selectedListener = Mockito.mock(
+ OnItemViewSelectedListener.class);
+ fragment.setOnItemViewSelectedListener(selectedListener);
+
+
+ PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+ SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+ controlsRow.getPrimaryActionsAdapter();
+
+ PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+ PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+ PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+ ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+ ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+ ArgumentCaptor<Object> itemCaptor =
+ ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+ ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+ ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+ // First navigate left within PlaybackControlsRow items.
+ verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The selected action should be rewind", rewind, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+ // Now navigate down to a ListRow item.
+ ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same list row should be passed to the listener", listRow0,
+ rowCaptor.getValue());
+ // Depending on the focusSearch algorithm, one of the items in the first ListRow must be
+ // selected.
+ boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+ || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+ assertTrue("None of the items in the first ListRow are passed to the selected listener.",
+ listRowItemPassed);
+ }
+
+ @Test
+ public void testClickedListener() throws Throwable {
+ Intent intent = new Intent();
+ mActivity = activityTestRule.launchActivity(intent);
+ PlaybackSupportTestFragment fragment = mActivity.getPlaybackFragment();
+ assertTrue(fragment.getView().hasFocus());
+
+ OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class);
+ fragment.setOnItemViewClickedListener(clickedListener);
+
+
+ PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow();
+ SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter)
+ controlsRow.getPrimaryActionsAdapter();
+
+ PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE);
+
+ PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND);
+
+ PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction)
+ primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST);
+
+ ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor =
+ ArgumentCaptor.forClass(Presenter.ViewHolder.class);
+ ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class);
+ ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor =
+ ArgumentCaptor.forClass(RowPresenter.ViewHolder.class);
+ ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class);
+
+
+ // First navigate left within PlaybackControlsRow items.
+ verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue());
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_LEFT);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same controls row should be passed to the listener", controlsRow,
+ rowCaptor.getValue());
+ assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue());
+
+ // Now navigate down to a ListRow item.
+ ListRow listRow0 = (ListRow) fragment.getAdapter().get(1);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class),
+ any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class));
+ sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
+ Thread.sleep(TRANSITION_LENGTH);
+ verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(),
+ itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture());
+ assertSame("Same list row should be passed to the listener", listRow0,
+ rowCaptor.getValue());
+ boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0)
+ || itemCaptor.getValue() == listRow0.getAdapter().get(1));
+ assertTrue("None of the items in the first ListRow are passed to the click listener.",
+ listRowItemPassed);
+ }
+
+ private void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
new file mode 100644
index 0000000..c85fe83
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestActivity.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+import android.support.v4.app.FragmentActivity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PlaybackSupportTestActivity extends FragmentActivity {
+ private List<PictureInPictureListener> mListeners = new ArrayList<>();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.playback_support_controls);
+ }
+
+ @Override
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ for (PictureInPictureListener listener : mListeners) {
+ listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
+ }
+ }
+
+ public void registerPictureInPictureListener(PictureInPictureListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
+ mListeners.remove(listener);
+ }
+
+ public interface PictureInPictureListener {
+ void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
+ }
+
+ public PlaybackSupportTestFragment getPlaybackFragment() {
+ return (PlaybackSupportTestFragment) getSupportFragmentManager().findFragmentById(
+ R.id.playback_controls_fragment);
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
new file mode 100644
index 0000000..4a07a60
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackSupportTestFragment.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Toast;
+
+public class PlaybackSupportTestFragment extends PlaybackSupportFragment {
+ private static final String TAG = "PlaybackTestFragment";
+
+ /**
+ * Change this to choose a different overlay background.
+ */
+ private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
+
+ private static final int ROW_CONTROLS = 0;
+
+ /**
+ * Change this to select hidden
+ */
+ private static final boolean SECONDARY_HIDDEN = false;
+
+ /**
+ * Change the number of related content rows.
+ */
+ private static final int RELATED_CONTENT_ROWS = 3;
+
+ private android.support.v17.leanback.media.PlaybackControlGlue mGlue;
+ private ListRowPresenter mListRowPresenter;
+
+ public SparseArrayObjectAdapter getAdapter() {
+ return (SparseArrayObjectAdapter) super.getAdapter();
+ }
+
+ private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.d(TAG, "onItemClicked: " + item + " row " + row);
+ }
+ };
+
+ private OnItemViewSelectedListener mOnItemViewSelectedListener =
+ new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.d(TAG, "onItemSelected: " + item + " row " + row);
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+
+ setBackgroundType(BACKGROUND_TYPE);
+ // setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+
+ createComponents(getActivity());
+ setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+
+ private void createComponents(Context context) {
+ mGlue = new PlaybackControlHelper(context) {
+ @Override
+ public int getUpdatePeriod() {
+ int totalTime = getControlsRow().getTotalTime();
+ if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
+ return 1000;
+ }
+ return Math.max(16, totalTime / getView().getWidth());
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == R.id.lb_control_picture_in_picture) {
+ getActivity().enterPictureInPictureMode();
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ protected void onCreateControlsRowAndPresenter() {
+ super.onCreateControlsRowAndPresenter();
+ getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN);
+ }
+ };
+
+ mGlue.setHost(new PlaybackSupportFragmentGlueHost(this));
+ // mGlue.setOnI
+ mListRowPresenter = new ListRowPresenter();
+
+ setAdapter(new SparseArrayObjectAdapter(new PresenterSelector() {
+ @Override
+ public Presenter getPresenter(Object object) {
+ if (object instanceof PlaybackControlsRow) {
+ return mGlue.getControlsRowPresenter();
+ } else if (object instanceof ListRow) {
+ return mListRowPresenter;
+ }
+ throw new IllegalArgumentException("Unhandled object: " + object);
+ }
+ }));
+
+ // Add the controls row
+ getAdapter().set(ROW_CONTROLS, mGlue.getControlsRow());
+
+ // Add related content rows
+ for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
+ listRowAdapter.add("Some related content");
+ listRowAdapter.add("Other related content");
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ getAdapter().set(ROW_CONTROLS + 1 + i, new ListRow(header, listRowAdapter));
+ }
+ }
+
+ public PlaybackControlGlue getGlue() {
+ return mGlue;
+ }
+
+ abstract static class PlaybackControlHelper extends PlaybackControlGlue {
+ /**
+ * Change the location of the thumbs up/down controls
+ */
+ private static final boolean THUMBS_PRIMARY = true;
+
+ private static final String FAUX_TITLE = "A short song of silence";
+ private static final String FAUX_SUBTITLE = "2014";
+ private static final int FAUX_DURATION = 33 * 1000;
+
+ // These should match the playback service FF behavior
+ private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
+
+ private boolean mIsPlaying;
+ private int mSpeed = PLAYBACK_SPEED_PAUSED;
+ private long mStartTime;
+ private long mStartPosition = 0;
+
+ private PlaybackControlsRow.RepeatAction mRepeatAction;
+ private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
+ private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
+ private PlaybackControlsRow.PictureInPictureAction mPipAction;
+ private static Handler sProgressHandler = new Handler();
+
+ private final Runnable mUpdateProgressRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateProgress();
+ sProgressHandler.postDelayed(this, getUpdatePeriod());
+ }
+ };
+
+ PlaybackControlHelper(Context context) {
+ super(context, sFastForwardSpeeds);
+ mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
+ mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.OUTLINE);
+ mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
+ mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.OUTLINE);
+ mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
+ mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
+ PresenterSelector presenterSelector) {
+ SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
+ if (THUMBS_PRIMARY) {
+ adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
+ adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
+ }
+ return adapter;
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action);
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action);
+ return true;
+ }
+ }
+ return super.onKey(view, keyCode, keyEvent);
+ }
+
+ private boolean shouldDispatchAction(Action action) {
+ return action == mRepeatAction || action == mThumbsUpAction
+ || action == mThumbsDownAction;
+ }
+
+ private void dispatchAction(Action action) {
+ Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
+ PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
+ multiAction.nextIndex();
+ notifyActionChanged(multiAction);
+ }
+
+ private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
+ int index;
+ index = getPrimaryActionsAdapter().indexOf(action);
+ if (index >= 0) {
+ getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+ } else {
+ index = getSecondaryActionsAdapter().indexOf(action);
+ if (index >= 0) {
+ getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+ }
+ }
+ }
+
+ private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
+ return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
+ }
+
+ private ArrayObjectAdapter getSecondaryActionsAdapter() {
+ return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
+ }
+
+ @Override
+ public boolean hasValidMedia() {
+ return true;
+ }
+
+ @Override
+ public boolean isMediaPlaying() {
+ return mIsPlaying;
+ }
+
+ @Override
+ public CharSequence getMediaTitle() {
+ return FAUX_TITLE;
+ }
+
+ @Override
+ public CharSequence getMediaSubtitle() {
+ return FAUX_SUBTITLE;
+ }
+
+ @Override
+ public int getMediaDuration() {
+ return FAUX_DURATION;
+ }
+
+ @Override
+ public Drawable getMediaArt() {
+ return null;
+ }
+
+ @Override
+ public long getSupportedActions() {
+ return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
+ }
+
+ @Override
+ public int getCurrentSpeedId() {
+ return mSpeed;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ int speed;
+ if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
+ speed = 0;
+ } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
+ speed = 1;
+ } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+ speed = getFastForwardSpeeds()[index];
+ } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+ speed = -getRewindSpeeds()[index];
+ } else {
+ return -1;
+ }
+ long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
+ if (position > getMediaDuration()) {
+ position = getMediaDuration();
+ onPlaybackComplete(true);
+ } else if (position < 0) {
+ position = 0;
+ onPlaybackComplete(false);
+ }
+ return (int) position;
+ }
+
+ void onPlaybackComplete(final boolean ended) {
+ sProgressHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.NONE) {
+ pause();
+ } else {
+ play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
+ }
+ mStartPosition = 0;
+ onStateChanged();
+ }
+ });
+ }
+
+ @Override
+ public void play(int speed) {
+ if (speed == mSpeed) {
+ return;
+ }
+ mStartPosition = getCurrentPosition();
+ mSpeed = speed;
+ mIsPlaying = true;
+ mStartTime = System.currentTimeMillis();
+ }
+
+ @Override
+ public void pause() {
+ if (mSpeed == PLAYBACK_SPEED_PAUSED) {
+ return;
+ }
+ mStartPosition = getCurrentPosition();
+ mSpeed = PLAYBACK_SPEED_PAUSED;
+ mIsPlaying = false;
+ }
+
+ @Override
+ public void next() {
+ // Not supported
+ }
+
+ @Override
+ public void previous() {
+ // Not supported
+ }
+
+ @Override
+ public void enableProgressUpdating(boolean enable) {
+ sProgressHandler.removeCallbacks(mUpdateProgressRunnable);
+ if (enable) {
+ mUpdateProgressRunnable.run();
+ }
+ }
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
new file mode 100644
index 0000000..ff840ec
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestActivity.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PlaybackTestActivity extends Activity {
+ private List<PictureInPictureListener> mListeners = new ArrayList<>();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.playback_controls);
+ }
+
+ @Override
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ for (PictureInPictureListener listener : mListeners) {
+ listener.onPictureInPictureModeChanged(isInPictureInPictureMode);
+ }
+ }
+
+ public void registerPictureInPictureListener(PictureInPictureListener listener) {
+ mListeners.add(listener);
+ }
+
+ public void unregisterPictureInPictureListener(PictureInPictureListener listener) {
+ mListeners.remove(listener);
+ }
+
+ public interface PictureInPictureListener {
+ void onPictureInPictureModeChanged(boolean isInPictureInPictureMode);
+ }
+
+ public PlaybackTestFragment getPlaybackFragment() {
+ return (PlaybackTestFragment) getFragmentManager().findFragmentById(
+ R.id.playback_controls_fragment);
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
new file mode 100644
index 0000000..043d73e
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/PlaybackTestFragment.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+
+import android.support.v17.leanback.media.PlaybackControlGlue;
+import android.support.v17.leanback.test.R;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Toast;
+
+public class PlaybackTestFragment extends PlaybackFragment {
+ private static final String TAG = "PlaybackTestFragment";
+
+ /**
+ * Change this to choose a different overlay background.
+ */
+ private static final int BACKGROUND_TYPE = PlaybackFragment.BG_LIGHT;
+
+ private static final int ROW_CONTROLS = 0;
+
+ /**
+ * Change this to select hidden
+ */
+ private static final boolean SECONDARY_HIDDEN = false;
+
+ /**
+ * Change the number of related content rows.
+ */
+ private static final int RELATED_CONTENT_ROWS = 3;
+
+ private android.support.v17.leanback.media.PlaybackControlGlue mGlue;
+ private ListRowPresenter mListRowPresenter;
+
+ public SparseArrayObjectAdapter getAdapter() {
+ return (SparseArrayObjectAdapter) super.getAdapter();
+ }
+
+ private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.d(TAG, "onItemClicked: " + item + " row " + row);
+ }
+ };
+
+ private OnItemViewSelectedListener mOnItemViewSelectedListener =
+ new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.d(TAG, "onItemSelected: " + item + " row " + row);
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+
+ setBackgroundType(BACKGROUND_TYPE);
+ // setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+
+ createComponents(getActivity());
+ setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+
+ private void createComponents(Context context) {
+ mGlue = new PlaybackControlHelper(context) {
+ @Override
+ public int getUpdatePeriod() {
+ int totalTime = getControlsRow().getTotalTime();
+ if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
+ return 1000;
+ }
+ return Math.max(16, totalTime / getView().getWidth());
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == R.id.lb_control_picture_in_picture) {
+ getActivity().enterPictureInPictureMode();
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ protected void onCreateControlsRowAndPresenter() {
+ super.onCreateControlsRowAndPresenter();
+ getControlsRowPresenter().setSecondaryActionsHidden(SECONDARY_HIDDEN);
+ }
+ };
+
+ mGlue.setHost(new PlaybackFragmentGlueHost(this));
+ // mGlue.setOnI
+ mListRowPresenter = new ListRowPresenter();
+
+ setAdapter(new SparseArrayObjectAdapter(new PresenterSelector() {
+ @Override
+ public Presenter getPresenter(Object object) {
+ if (object instanceof PlaybackControlsRow) {
+ return mGlue.getControlsRowPresenter();
+ } else if (object instanceof ListRow) {
+ return mListRowPresenter;
+ }
+ throw new IllegalArgumentException("Unhandled object: " + object);
+ }
+ }));
+
+ // Add the controls row
+ getAdapter().set(ROW_CONTROLS, mGlue.getControlsRow());
+
+ // Add related content rows
+ for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
+ listRowAdapter.add("Some related content");
+ listRowAdapter.add("Other related content");
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ getAdapter().set(ROW_CONTROLS + 1 + i, new ListRow(header, listRowAdapter));
+ }
+ }
+
+ public PlaybackControlGlue getGlue() {
+ return mGlue;
+ }
+
+ abstract static class PlaybackControlHelper extends PlaybackControlGlue {
+ /**
+ * Change the location of the thumbs up/down controls
+ */
+ private static final boolean THUMBS_PRIMARY = true;
+
+ private static final String FAUX_TITLE = "A short song of silence";
+ private static final String FAUX_SUBTITLE = "2014";
+ private static final int FAUX_DURATION = 33 * 1000;
+
+ // These should match the playback service FF behavior
+ private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
+
+ private boolean mIsPlaying;
+ private int mSpeed = PLAYBACK_SPEED_PAUSED;
+ private long mStartTime;
+ private long mStartPosition = 0;
+
+ private PlaybackControlsRow.RepeatAction mRepeatAction;
+ private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
+ private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
+ private PlaybackControlsRow.PictureInPictureAction mPipAction;
+ private static Handler sProgressHandler = new Handler();
+
+ private final Runnable mUpdateProgressRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateProgress();
+ sProgressHandler.postDelayed(this, getUpdatePeriod());
+ }
+ };
+
+ PlaybackControlHelper(Context context) {
+ super(context, sFastForwardSpeeds);
+ mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
+ mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.OUTLINE);
+ mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
+ mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.OUTLINE);
+ mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
+ mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
+ PresenterSelector presenterSelector) {
+ SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
+ if (THUMBS_PRIMARY) {
+ adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
+ adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
+ }
+ return adapter;
+ }
+
+ @Override
+ public void onActionClicked(Action action) {
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action);
+ return;
+ }
+ super.onActionClicked(action);
+ }
+
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
+ if (shouldDispatchAction(action)) {
+ dispatchAction(action);
+ return true;
+ }
+ }
+ return super.onKey(view, keyCode, keyEvent);
+ }
+
+ private boolean shouldDispatchAction(Action action) {
+ return action == mRepeatAction || action == mThumbsUpAction
+ || action == mThumbsDownAction;
+ }
+
+ private void dispatchAction(Action action) {
+ Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
+ PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
+ multiAction.nextIndex();
+ notifyActionChanged(multiAction);
+ }
+
+ private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
+ int index;
+ index = getPrimaryActionsAdapter().indexOf(action);
+ if (index >= 0) {
+ getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+ } else {
+ index = getSecondaryActionsAdapter().indexOf(action);
+ if (index >= 0) {
+ getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
+ }
+ }
+ }
+
+ private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
+ return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
+ }
+
+ private ArrayObjectAdapter getSecondaryActionsAdapter() {
+ return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
+ }
+
+ @Override
+ public boolean hasValidMedia() {
+ return true;
+ }
+
+ @Override
+ public boolean isMediaPlaying() {
+ return mIsPlaying;
+ }
+
+ @Override
+ public CharSequence getMediaTitle() {
+ return FAUX_TITLE;
+ }
+
+ @Override
+ public CharSequence getMediaSubtitle() {
+ return FAUX_SUBTITLE;
+ }
+
+ @Override
+ public int getMediaDuration() {
+ return FAUX_DURATION;
+ }
+
+ @Override
+ public Drawable getMediaArt() {
+ return null;
+ }
+
+ @Override
+ public long getSupportedActions() {
+ return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND;
+ }
+
+ @Override
+ public int getCurrentSpeedId() {
+ return mSpeed;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ int speed;
+ if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
+ speed = 0;
+ } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
+ speed = 1;
+ } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+ speed = getFastForwardSpeeds()[index];
+ } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
+ int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
+ speed = -getRewindSpeeds()[index];
+ } else {
+ return -1;
+ }
+ long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
+ if (position > getMediaDuration()) {
+ position = getMediaDuration();
+ onPlaybackComplete(true);
+ } else if (position < 0) {
+ position = 0;
+ onPlaybackComplete(false);
+ }
+ return (int) position;
+ }
+
+ void onPlaybackComplete(final boolean ended) {
+ sProgressHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.NONE) {
+ pause();
+ } else {
+ play(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
+ }
+ mStartPosition = 0;
+ onStateChanged();
+ }
+ });
+ }
+
+ @Override
+ public void play(int speed) {
+ if (speed == mSpeed) {
+ return;
+ }
+ mStartPosition = getCurrentPosition();
+ mSpeed = speed;
+ mIsPlaying = true;
+ mStartTime = System.currentTimeMillis();
+ }
+
+ @Override
+ public void pause() {
+ if (mSpeed == PLAYBACK_SPEED_PAUSED) {
+ return;
+ }
+ mStartPosition = getCurrentPosition();
+ mSpeed = PLAYBACK_SPEED_PAUSED;
+ mIsPlaying = false;
+ }
+
+ @Override
+ public void next() {
+ // Not supported
+ }
+
+ @Override
+ public void previous() {
+ // Not supported
+ }
+
+ @Override
+ public void enableProgressUpdating(boolean enable) {
+ sProgressHandler.removeCallbacks(mUpdateProgressRunnable);
+ if (enable) {
+ mUpdateProgressRunnable.run();
+ }
+ }
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
new file mode 100644
index 0000000..50d5f24
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.support.test.filters.MediumTest;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class RowsFragmentTest {
+
+ static final long ACTIVITY_LOAD_DELAY = 2000;
+
+ @Rule
+ public ActivityTestRule<RowsFragmentTestActivity> activityTestRule =
+ new ActivityTestRule<>(RowsFragmentTestActivity.class, false, false);
+ private RowsFragmentTestActivity mActivity;
+
+ @After
+ public void afterTest() throws Throwable {
+ activityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ if (mActivity != null) {
+ mActivity.finish();
+ mActivity = null;
+ }
+ }
+ });
+ }
+
+ private void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+
+ void launchAndWaitActivity(Intent intent) {
+ mActivity = activityTestRule.launchActivity(intent);
+ SystemClock.sleep(ACTIVITY_LOAD_DELAY);
+ }
+
+ @Test
+ public void defaultAlignment() throws InterruptedException {
+ Intent intent = new Intent();
+ intent.putExtra(RowsFragmentTestActivity.EXTRA_NUM_ROWS, 10);
+ intent.putExtra(RowsFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, 1l);
+ launchAndWaitActivity(intent);
+
+ final Rect rect = new Rect();
+
+ final VerticalGridView gridView = mActivity.getRowsTestFragment().getVerticalGridView();
+ View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
+ rect.set(0, 0, row0.getWidth(), row0.getHeight());
+ gridView.offsetDescendantRectToMyCoords(row0, rect);
+ assertEquals("First row is initially aligned to top of screen", 0, rect.top);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
+
+ rect.set(0, 0, row1.getWidth(), row1.getHeight());
+ gridView.offsetDescendantRectToMyCoords(row1, rect);
+ assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTestActivity.java
new file mode 100644
index 0000000..fe2dd25
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTestActivity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.app.Activity;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+public class RowsFragmentTestActivity extends Activity {
+
+ public static final String EXTRA_NUM_ROWS = "numRows";
+ public static final String EXTRA_REPEAT_PER_ROW = "repeatPerRow";
+ public static final String EXTRA_LOAD_DATA_DELAY = "loadDataDelay";
+ public final static String EXTRA_SET_ADAPTER_AFTER_DATA_LOAD = "set_adapter_after_data_load";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+
+ setContentView(R.layout.rows);
+ if (savedInstanceState == null) {
+ RowsTestFragment fragment = new RowsTestFragment();
+ Bundle arguments = new Bundle();
+ if (intent.getExtras() != null) {
+ arguments.putAll(intent.getExtras());
+ }
+ fragment.setArguments(arguments);
+ FragmentTransaction ft = getFragmentManager().beginTransaction();
+ ft.replace(R.id.main_frame, fragment);
+ ft.commit();
+ }
+ }
+
+ public RowsTestFragment getRowsTestFragment() {
+ return (RowsTestFragment) getFragmentManager().findFragmentById(R.id.main_frame);
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
new file mode 100644
index 0000000..c024b6c
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
@@ -0,0 +1,99 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from RowsFragmentTest.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.support.test.filters.MediumTest;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class RowsSupportFragmentTest {
+
+ static final long ACTIVITY_LOAD_DELAY = 2000;
+
+ @Rule
+ public ActivityTestRule<RowsSupportFragmentTestActivity> activityTestRule =
+ new ActivityTestRule<>(RowsSupportFragmentTestActivity.class, false, false);
+ private RowsSupportFragmentTestActivity mActivity;
+
+ @After
+ public void afterTest() throws Throwable {
+ activityTestRule.runOnUiThread(new Runnable() {
+ public void run() {
+ if (mActivity != null) {
+ mActivity.finish();
+ mActivity = null;
+ }
+ }
+ });
+ }
+
+ private void sendKeys(int ...keys) {
+ for (int i = 0; i < keys.length; i++) {
+ InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keys[i]);
+ }
+ }
+
+ void launchAndWaitActivity(Intent intent) {
+ mActivity = activityTestRule.launchActivity(intent);
+ SystemClock.sleep(ACTIVITY_LOAD_DELAY);
+ }
+
+ @Test
+ public void defaultAlignment() throws InterruptedException {
+ Intent intent = new Intent();
+ intent.putExtra(RowsSupportFragmentTestActivity.EXTRA_NUM_ROWS, 10);
+ intent.putExtra(RowsSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY, 1l);
+ launchAndWaitActivity(intent);
+
+ final Rect rect = new Rect();
+
+ final VerticalGridView gridView = mActivity.getRowsTestSupportFragment().getVerticalGridView();
+ View row0 = gridView.findViewHolderForAdapterPosition(0).itemView;
+ rect.set(0, 0, row0.getWidth(), row0.getHeight());
+ gridView.offsetDescendantRectToMyCoords(row0, rect);
+ assertEquals("First row is initially aligned to top of screen", 0, rect.top);
+
+ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
+ View row1 = gridView.findViewHolderForAdapterPosition(1).itemView;
+ PollingCheck.waitFor(new PollingCheck.ViewStableOnScreen(row1));
+
+ rect.set(0, 0, row1.getWidth(), row1.getHeight());
+ gridView.offsetDescendantRectToMyCoords(row1, rect);
+ assertTrue("Second row should not be aligned to top of screen", rect.top > 0);
+ }
+
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTestActivity.java
new file mode 100644
index 0000000..d736458
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTestActivity.java
@@ -0,0 +1,56 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from RowsFragmentTestActivity.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v17.leanback.test.R;
+
+public class RowsSupportFragmentTestActivity extends FragmentActivity {
+
+ public static final String EXTRA_NUM_ROWS = "numRows";
+ public static final String EXTRA_REPEAT_PER_ROW = "repeatPerRow";
+ public static final String EXTRA_LOAD_DATA_DELAY = "loadDataDelay";
+ public final static String EXTRA_SET_ADAPTER_AFTER_DATA_LOAD = "set_adapter_after_data_load";
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+
+ setContentView(R.layout.rows);
+ if (savedInstanceState == null) {
+ RowsTestSupportFragment fragment = new RowsTestSupportFragment();
+ Bundle arguments = new Bundle();
+ if (intent.getExtras() != null) {
+ arguments.putAll(intent.getExtras());
+ }
+ fragment.setArguments(arguments);
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ ft.replace(R.id.main_frame, fragment);
+ ft.commit();
+ }
+ }
+
+ public RowsTestSupportFragment getRowsTestSupportFragment() {
+ return (RowsTestSupportFragment) getSupportFragmentManager().findFragmentById(R.id.main_frame);
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestFragment.java
new file mode 100644
index 0000000..d1f71db
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestFragment.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.util.Log;
+
+import static android.support.v17.leanback.app.RowsFragmentTestActivity.EXTRA_LOAD_DATA_DELAY;
+import static android.support.v17.leanback.app.RowsFragmentTestActivity.EXTRA_NUM_ROWS;
+import static android.support.v17.leanback.app.RowsFragmentTestActivity.EXTRA_REPEAT_PER_ROW;
+import static android.support.v17.leanback.app.RowsFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD;
+
+public class RowsTestFragment extends RowsFragment {
+ private static final String TAG = "RowsTestFragment";
+
+ final static int DEFAULT_NUM_ROWS = 100;
+ final static int DEFAULT_REPEAT_PER_ROW = 20;
+ final static long DEFAULT_LOAD_DATA_DELAY = 2000;
+ final static boolean DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD = false;
+
+ private ArrayObjectAdapter mRowsAdapter;
+
+ // For good performance, it's important to use a single instance of
+ // a card presenter for all rows using that presenter.
+ final static StringPresenter sCardPresenter = new StringPresenter();
+
+ int NUM_ROWS;
+ int REPEAT_PER_ROW;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+
+ Bundle arguments = getArguments();
+ NUM_ROWS = arguments.getInt(EXTRA_NUM_ROWS, RowsTestFragment.DEFAULT_NUM_ROWS);
+ REPEAT_PER_ROW = arguments.getInt(EXTRA_REPEAT_PER_ROW,
+ DEFAULT_REPEAT_PER_ROW);
+ long LOAD_DATA_DELAY = arguments.getLong(EXTRA_LOAD_DATA_DELAY,
+ DEFAULT_LOAD_DATA_DELAY);
+ final boolean SET_ADAPTER_AFTER_DATA_LOAD = arguments.getBoolean(
+ EXTRA_SET_ADAPTER_AFTER_DATA_LOAD,
+ DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD);
+
+ if (!SET_ADAPTER_AFTER_DATA_LOAD) {
+ setupRows();
+ }
+
+ setOnItemViewClickedListener(new ItemViewClickedListener());
+ setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.i(TAG, "onItemSelected: " + item + " row " + row.getHeaderItem().getName()
+ + " " + rowViewHolder
+ + " " + ((ListRowPresenter.ViewHolder) rowViewHolder).getGridView());
+ }
+ });
+ // simulates in a real world use case data being loaded two seconds later
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null || getActivity().isDestroyed()) {
+ return;
+ }
+ if (SET_ADAPTER_AFTER_DATA_LOAD) {
+ setupRows();
+ }
+ loadData();
+ }
+ }, LOAD_DATA_DELAY);
+ }
+
+ private void setupRows() {
+ ListRowPresenter lrp = new ListRowPresenter();
+
+ mRowsAdapter = new ArrayObjectAdapter(lrp);
+
+ setAdapter(mRowsAdapter);
+ }
+
+ private void loadData() {
+ for (int i = 0; i < NUM_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+ int index = 0;
+ for (int j = 0; j < REPEAT_PER_ROW; ++j) {
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("This is a test-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("GuidedStepFragment-" + (index++));
+ }
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ mRowsAdapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ private final class ItemViewClickedListener implements OnItemViewClickedListener {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.i(TAG, "onItemClicked: " + item + " row " + row);
+ }
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestSupportFragment.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestSupportFragment.java
new file mode 100644
index 0000000..e095f94
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsTestSupportFragment.java
@@ -0,0 +1,132 @@
+// CHECKSTYLE:OFF Generated code
+/* This file is auto-generated from RowsTestFragment.java. DO NOT MODIFY. */
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.util.Log;
+
+import static android.support.v17.leanback.app.RowsSupportFragmentTestActivity.EXTRA_LOAD_DATA_DELAY;
+import static android.support.v17.leanback.app.RowsSupportFragmentTestActivity.EXTRA_NUM_ROWS;
+import static android.support.v17.leanback.app.RowsSupportFragmentTestActivity.EXTRA_REPEAT_PER_ROW;
+import static android.support.v17.leanback.app.RowsSupportFragmentTestActivity.EXTRA_SET_ADAPTER_AFTER_DATA_LOAD;
+
+public class RowsTestSupportFragment extends RowsSupportFragment {
+ private static final String TAG = "RowsTestSupportFragment";
+
+ final static int DEFAULT_NUM_ROWS = 100;
+ final static int DEFAULT_REPEAT_PER_ROW = 20;
+ final static long DEFAULT_LOAD_DATA_DELAY = 2000;
+ final static boolean DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD = false;
+
+ private ArrayObjectAdapter mRowsAdapter;
+
+ // For good performance, it's important to use a single instance of
+ // a card presenter for all rows using that presenter.
+ final static StringPresenter sCardPresenter = new StringPresenter();
+
+ int NUM_ROWS;
+ int REPEAT_PER_ROW;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+
+ Bundle arguments = getArguments();
+ NUM_ROWS = arguments.getInt(EXTRA_NUM_ROWS, RowsTestSupportFragment.DEFAULT_NUM_ROWS);
+ REPEAT_PER_ROW = arguments.getInt(EXTRA_REPEAT_PER_ROW,
+ DEFAULT_REPEAT_PER_ROW);
+ long LOAD_DATA_DELAY = arguments.getLong(EXTRA_LOAD_DATA_DELAY,
+ DEFAULT_LOAD_DATA_DELAY);
+ final boolean SET_ADAPTER_AFTER_DATA_LOAD = arguments.getBoolean(
+ EXTRA_SET_ADAPTER_AFTER_DATA_LOAD,
+ DEFAULT_SET_ADAPTER_AFTER_DATA_LOAD);
+
+ if (!SET_ADAPTER_AFTER_DATA_LOAD) {
+ setupRows();
+ }
+
+ setOnItemViewClickedListener(new ItemViewClickedListener());
+ setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.i(TAG, "onItemSelected: " + item + " row " + row.getHeaderItem().getName()
+ + " " + rowViewHolder
+ + " " + ((ListRowPresenter.ViewHolder) rowViewHolder).getGridView());
+ }
+ });
+ // simulates in a real world use case data being loaded two seconds later
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (getActivity() == null || getActivity().isDestroyed()) {
+ return;
+ }
+ if (SET_ADAPTER_AFTER_DATA_LOAD) {
+ setupRows();
+ }
+ loadData();
+ }
+ }, LOAD_DATA_DELAY);
+ }
+
+ private void setupRows() {
+ ListRowPresenter lrp = new ListRowPresenter();
+
+ mRowsAdapter = new ArrayObjectAdapter(lrp);
+
+ setAdapter(mRowsAdapter);
+ }
+
+ private void loadData() {
+ for (int i = 0; i < NUM_ROWS; ++i) {
+ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(sCardPresenter);
+ int index = 0;
+ for (int j = 0; j < REPEAT_PER_ROW; ++j) {
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("This is a test-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("Hello world-" + (index++));
+ listRowAdapter.add("Android TV-" + (index++));
+ listRowAdapter.add("Leanback-" + (index++));
+ listRowAdapter.add("GuidedStepSupportFragment-" + (index++));
+ }
+ HeaderItem header = new HeaderItem(i, "Row " + i);
+ mRowsAdapter.add(new ListRow(header, listRowAdapter));
+ }
+ }
+
+ private final class ItemViewClickedListener implements OnItemViewClickedListener {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ Log.i(TAG, "onItemClicked: " + item + " row " + row);
+ }
+ }
+}
diff --git a/v17/leanback/tests/res/layout/playback_controls.xml b/v17/leanback/tests/res/layout/playback_controls.xml
new file mode 100644
index 0000000..7f8910f
--- /dev/null
+++ b/v17/leanback/tests/res/layout/playback_controls.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <fragment
+ android:id="@+id/playback_controls_fragment"
+ android:name="android.support.v17.leanback.app.PlaybackTestFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/v17/leanback/tests/res/layout/playback_support_controls.xml b/v17/leanback/tests/res/layout/playback_support_controls.xml
new file mode 100644
index 0000000..9e0e092
--- /dev/null
+++ b/v17/leanback/tests/res/layout/playback_support_controls.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <fragment
+ android:id="@+id/playback_controls_fragment"
+ android:name="android.support.v17.leanback.app.PlaybackSupportTestFragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/samples/SupportLeanbackDemos/res/layout/legacy_details.xml b/v17/leanback/tests/res/layout/rows.xml
similarity index 70%
copy from samples/SupportLeanbackDemos/res/layout/legacy_details.xml
copy to v17/leanback/tests/res/layout/rows.xml
index 4af4e6a..7a7c0b0 100644
--- a/samples/SupportLeanbackDemos/res/layout/legacy_details.xml
+++ b/v17/leanback/tests/res/layout/rows.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright (C) 2014 The Android Open Source Project
+ Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,9 +15,9 @@
limitations under the License.
-->
-<fragment xmlns:android="http://schemas.android.com/apk/res/android"
- android:name="com.example.android.leanback.DetailsFragment"
- android:id="@+id/details_fragment"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/main_frame"
android:layout_width="match_parent"
- android:layout_height="match_parent"
-/>
+ android:layout_height="match_parent">
+
+</FrameLayout>
diff --git a/v7/appcompat/tests/res/layout/toolbar_decor_content.xml b/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
index db9935d..c271f68 100644
--- a/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
+++ b/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
@@ -29,11 +29,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
- <EditText
+ <View
android:id="@+id/editText"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:imeOptions="flagNoExtractUi"/>
+ android:layout_height="20dip"
+ android:focusable="true" />
</android.support.v7.custom.FitWindowsContentLayout>
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 066054c..7027acb 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -189,6 +189,16 @@
*/
private static final boolean FORCE_ABS_FOCUS_SEARCH_DIRECTION = Build.VERSION.SDK_INT <= 15;
+ /**
+ * on API 15-, a focused child can still be considered a focused child of RV even after
+ * it's being removed or its focusable flag is set to false. This is because when this focused
+ * child is detached, the reference to this child is not removed in clearFocus. API 16 and above
+ * properly handle this case by calling ensureInputFocusOnFirstFocusable or rootViewRequestFocus
+ * to request focus on a new child, which will clear the focus on the old (detached) child as a
+ * side-effect.
+ */
+ private static final boolean IGNORE_DETACHED_FOCUSED_CHILD = Build.VERSION.SDK_INT <= 15;
+
static final boolean DISPATCH_TEMP_DETACH = false;
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
@@ -3341,9 +3351,28 @@
// only recover focus if RV itself has the focus or the focused view is hidden
if (!isFocused()) {
final View focusedChild = getFocusedChild();
- if (!mChildHelper.isHidden(focusedChild)
- // on API 15, this happens :/.
- && focusedChild.getParent() == this && focusedChild.hasFocus()) {
+ if (IGNORE_DETACHED_FOCUSED_CHILD
+ && (focusedChild.getParent() == null || !focusedChild.hasFocus())) {
+ // Special handling of API 15-. A focused child can be invalid because mFocus is not
+ // cleared when the child is detached (mParent = null),
+ // This happens because clearFocus on API 15- does not invalidate mFocus of its
+ // parent when this child is detached.
+ // For API 16+, this is not an issue because requestFocus takes care of clearing the
+ // prior detached focused child. For API 15- the problem happens in 2 cases because
+ // clearChild does not call clearChildFocus on RV: 1. setFocusable(false) is called
+ // for the current focused item which calls clearChild or 2. when the prior focused
+ // child is removed, removeDetachedView called in layout step 3 which calls
+ // clearChild. We should ignore this invalid focused child in all our calculations
+ // for the next view to receive focus, and apply the focus recovery logic instead.
+ if (mChildHelper.getChildCount() == 0) {
+ // No children left. Request focus on the RV itself since one of its children
+ // was holding focus previously.
+ requestFocus();
+ return;
+ }
+ } else if (!mChildHelper.isHidden(focusedChild)) {
+ // If the currently focused child is hidden, apply the focus recovery logic.
+ // Otherwise return, i.e. the currently (unhidden) focused child is good enough :/.
return;
}
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewFocusRecoveryTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewFocusRecoveryTest.java
index 0c324a9..0354d53 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewFocusRecoveryTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewFocusRecoveryTest.java
@@ -421,6 +421,11 @@
*/
private void assertFocusAfterLayout(int focusedChildIndexWhenRecoveryEnabled,
int focusedChildIndexWhenRecoveryDisabled) {
+ if (mDisableAnimation && mDisableRecovery) {
+ // This case is not quite handled properly at the moment. For now, RV may become focused
+ // without re-delivering the focus down to the children. Skip the checks for now.
+ return;
+ }
if (mRecyclerView.getChildCount() == 0) {
assertThat("RV should have focus when it has no children",
mRecyclerView.hasFocus(), is(true));
@@ -428,11 +433,6 @@
mRecyclerView.isFocused(), is(true));
return;
}
- if (mDisableAnimation && mDisableRecovery) {
- // This case is not quite handled properly at the moment. For now, RV may become focused
- // without re-delivering the focus down to the children. Skip the checks for now.
- return;
- }
assertThat("RV should still have focus after layout", mRecyclerView.hasFocus(), is(true));
if ((mDisableRecovery && focusedChildIndexWhenRecoveryDisabled == -1)