Added missing selected listener to the new playback fragments
am: 4c9758b250

Change-Id: I0d4822396814d05c218ccda52d14a0c99e6a8c03
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..ed4cc1f 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/tests/AndroidManifest.xml b/v17/leanback/tests/AndroidManifest.xml
index 16b78cb..a30e5c9 100644
--- a/v17/leanback/tests/AndroidManifest.xml
+++ b/v17/leanback/tests/AndroidManifest.xml
@@ -58,6 +58,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/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/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