1/* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package androidx.leanback.app; 17 18import static org.junit.Assert.assertEquals; 19import static org.junit.Assert.assertNull; 20import static org.junit.Assert.assertSame; 21import static org.junit.Assert.assertTrue; 22import static org.mockito.Matchers.any; 23import static org.mockito.Mockito.times; 24import static org.mockito.Mockito.verify; 25 26import android.graphics.drawable.Drawable; 27import android.os.Bundle; 28import android.support.test.filters.FlakyTest; 29import android.support.test.filters.MediumTest; 30import android.support.test.filters.Suppress; 31import android.support.test.runner.AndroidJUnit4; 32import android.view.KeyEvent; 33import android.view.View; 34 35import androidx.leanback.media.PlaybackControlGlue; 36import androidx.leanback.media.PlaybackGlue; 37import androidx.leanback.testutils.PollingCheck; 38import androidx.leanback.widget.ControlButtonPresenterSelector; 39import androidx.leanback.widget.ListRow; 40import androidx.leanback.widget.OnItemViewClickedListener; 41import androidx.leanback.widget.OnItemViewSelectedListener; 42import androidx.leanback.widget.PlaybackControlsRow; 43import androidx.leanback.widget.PlaybackControlsRowPresenter; 44import androidx.leanback.widget.Presenter; 45import androidx.leanback.widget.Row; 46import androidx.leanback.widget.RowPresenter; 47import androidx.leanback.widget.SparseArrayObjectAdapter; 48 49import org.junit.Test; 50import org.junit.runner.RunWith; 51import org.mockito.ArgumentCaptor; 52import org.mockito.Mockito; 53 54@MediumTest 55@RunWith(AndroidJUnit4.class) 56public class PlaybackSupportFragmentTest extends SingleSupportFragmentTestBase { 57 58 private static final String TAG = "PlaybackSupportFragmentTest"; 59 private static final long TRANSITION_LENGTH = 1000; 60 61 @Test 62 public void testDetachCalledWhenDestroyFragment() throws Throwable { 63 final SingleSupportFragmentTestActivity activity = 64 launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000); 65 final PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment(); 66 PlaybackGlue glue = fragment.getGlue(); 67 activityTestRule.runOnUiThread(new Runnable() { 68 @Override 69 public void run() { 70 activity.finish(); 71 } 72 }); 73 PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() { 74 @Override 75 public boolean canProceed() { 76 return fragment.mDestroyCalled; 77 } 78 }); 79 assertNull(glue.getHost()); 80 } 81 82 @Test 83 public void testSelectedListener() throws Throwable { 84 SingleSupportFragmentTestActivity activity = 85 launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000); 86 PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment(); 87 88 assertTrue(fragment.getView().hasFocus()); 89 90 OnItemViewSelectedListener selectedListener = Mockito.mock( 91 OnItemViewSelectedListener.class); 92 fragment.setOnItemViewSelectedListener(selectedListener); 93 94 95 PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow(); 96 SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter) 97 controlsRow.getPrimaryActionsAdapter(); 98 99 PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction) 100 primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE); 101 102 PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction) 103 primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND); 104 105 PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction) 106 primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST); 107 108 ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor = 109 ArgumentCaptor.forClass(Presenter.ViewHolder.class); 110 ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class); 111 ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor = 112 ArgumentCaptor.forClass(RowPresenter.ViewHolder.class); 113 ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class); 114 115 116 // First navigate left within PlaybackControlsRow items. 117 verify(selectedListener, times(0)).onItemSelected(any(Presenter.ViewHolder.class), 118 any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class)); 119 sendKeys(KeyEvent.KEYCODE_DPAD_LEFT); 120 verify(selectedListener, times(1)).onItemSelected(itemVHCaptor.capture(), 121 itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture()); 122 assertSame("Same controls row should be passed to the listener", controlsRow, 123 rowCaptor.getValue()); 124 assertSame("The selected action should be rewind", rewind, itemCaptor.getValue()); 125 126 sendKeys(KeyEvent.KEYCODE_DPAD_LEFT); 127 verify(selectedListener, times(2)).onItemSelected(itemVHCaptor.capture(), 128 itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture()); 129 assertSame("Same controls row should be passed to the listener", controlsRow, 130 rowCaptor.getValue()); 131 assertSame("The selected action should be thumbsUp", thumbsUp, itemCaptor.getValue()); 132 133 // Now navigate down to a ListRow item. 134 ListRow listRow0 = (ListRow) fragment.getAdapter().get(1); 135 136 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 137 waitForScrollIdle(fragment.getVerticalGridView()); 138 verify(selectedListener, times(3)).onItemSelected(itemVHCaptor.capture(), 139 itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture()); 140 assertSame("Same list row should be passed to the listener", listRow0, 141 rowCaptor.getValue()); 142 // Depending on the focusSearch algorithm, one of the items in the first ListRow must be 143 // selected. 144 boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0) 145 || itemCaptor.getValue() == listRow0.getAdapter().get(1)); 146 assertTrue("None of the items in the first ListRow are passed to the selected listener.", 147 listRowItemPassed); 148 } 149 150 @Test 151 public void testClickedListener() throws Throwable { 152 SingleSupportFragmentTestActivity activity = 153 launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000); 154 PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment(); 155 156 assertTrue(fragment.getView().hasFocus()); 157 158 OnItemViewClickedListener clickedListener = Mockito.mock(OnItemViewClickedListener.class); 159 fragment.setOnItemViewClickedListener(clickedListener); 160 161 162 PlaybackControlsRow controlsRow = fragment.getGlue().getControlsRow(); 163 SparseArrayObjectAdapter primaryActionsAdapter = (SparseArrayObjectAdapter) 164 controlsRow.getPrimaryActionsAdapter(); 165 166 PlaybackControlsRow.MultiAction playPause = (PlaybackControlsRow.MultiAction) 167 primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_PLAY_PAUSE); 168 169 PlaybackControlsRow.MultiAction rewind = (PlaybackControlsRow.MultiAction) 170 primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_REWIND); 171 172 PlaybackControlsRow.MultiAction thumbsUp = (PlaybackControlsRow.MultiAction) 173 primaryActionsAdapter.lookup(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST); 174 175 ArgumentCaptor<Presenter.ViewHolder> itemVHCaptor = 176 ArgumentCaptor.forClass(Presenter.ViewHolder.class); 177 ArgumentCaptor<Object> itemCaptor = ArgumentCaptor.forClass(Object.class); 178 ArgumentCaptor<RowPresenter.ViewHolder> rowVHCaptor = 179 ArgumentCaptor.forClass(RowPresenter.ViewHolder.class); 180 ArgumentCaptor<Row> rowCaptor = ArgumentCaptor.forClass(Row.class); 181 182 183 // First navigate left within PlaybackControlsRow items. 184 verify(clickedListener, times(0)).onItemClicked(any(Presenter.ViewHolder.class), 185 any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class)); 186 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 187 verify(clickedListener, times(1)).onItemClicked(itemVHCaptor.capture(), 188 itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture()); 189 assertSame("Same controls row should be passed to the listener", controlsRow, 190 rowCaptor.getValue()); 191 assertSame("The clicked action should be playPause", playPause, itemCaptor.getValue()); 192 193 sendKeys(KeyEvent.KEYCODE_DPAD_LEFT); 194 verify(clickedListener, times(1)).onItemClicked(any(Presenter.ViewHolder.class), 195 any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class)); 196 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 197 verify(clickedListener, times(2)).onItemClicked(itemVHCaptor.capture(), 198 itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture()); 199 assertSame("Same controls row should be passed to the listener", controlsRow, 200 rowCaptor.getValue()); 201 assertSame("The clicked action should be rewind", rewind, itemCaptor.getValue()); 202 203 sendKeys(KeyEvent.KEYCODE_DPAD_LEFT); 204 verify(clickedListener, times(2)).onItemClicked(any(Presenter.ViewHolder.class), 205 any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class)); 206 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 207 verify(clickedListener, times(3)).onItemClicked(itemVHCaptor.capture(), 208 itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture()); 209 assertSame("Same controls row should be passed to the listener", controlsRow, 210 rowCaptor.getValue()); 211 assertSame("The clicked action should be thumbsUp", thumbsUp, itemCaptor.getValue()); 212 213 // Now navigate down to a ListRow item. 214 ListRow listRow0 = (ListRow) fragment.getAdapter().get(1); 215 216 sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); 217 waitForScrollIdle(fragment.getVerticalGridView()); 218 verify(clickedListener, times(3)).onItemClicked(any(Presenter.ViewHolder.class), 219 any(Object.class), any(RowPresenter.ViewHolder.class), any(Row.class)); 220 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); 221 verify(clickedListener, times(4)).onItemClicked(itemVHCaptor.capture(), 222 itemCaptor.capture(), rowVHCaptor.capture(), rowCaptor.capture()); 223 assertSame("Same list row should be passed to the listener", listRow0, 224 rowCaptor.getValue()); 225 boolean listRowItemPassed = (itemCaptor.getValue() == listRow0.getAdapter().get(0) 226 || itemCaptor.getValue() == listRow0.getAdapter().get(1)); 227 assertTrue("None of the items in the first ListRow are passed to the click listener.", 228 listRowItemPassed); 229 } 230 231 @FlakyTest 232 @Suppress 233 @Test 234 public void alignmentRowToBottom() throws Throwable { 235 SingleSupportFragmentTestActivity activity = 236 launchAndWaitActivity(PlaybackTestSupportFragment.class, 1000); 237 final PlaybackTestSupportFragment fragment = (PlaybackTestSupportFragment) activity.getTestFragment(); 238 239 assertTrue(fragment.getAdapter().size() > 2); 240 241 View playRow = fragment.getVerticalGridView().getChildAt(0); 242 assertTrue(playRow.hasFocus()); 243 assertEquals(playRow.getResources().getDimensionPixelSize( 244 androidx.leanback.test.R.dimen.lb_playback_controls_padding_bottom), 245 fragment.getVerticalGridView().getHeight() - playRow.getBottom()); 246 247 activityTestRule.runOnUiThread(new Runnable() { 248 @Override 249 public void run() { 250 fragment.getVerticalGridView().setSelectedPositionSmooth( 251 fragment.getAdapter().size() - 1); 252 } 253 }); 254 waitForScrollIdle(fragment.getVerticalGridView()); 255 256 View lastRow = fragment.getVerticalGridView().getChildAt( 257 fragment.getVerticalGridView().getChildCount() - 1); 258 assertEquals(fragment.getAdapter().size() - 1, 259 fragment.getVerticalGridView().getChildAdapterPosition(lastRow)); 260 assertTrue(lastRow.hasFocus()); 261 assertEquals(lastRow.getResources().getDimensionPixelSize( 262 androidx.leanback.test.R.dimen.lb_playback_controls_padding_bottom), 263 fragment.getVerticalGridView().getHeight() - lastRow.getBottom()); 264 } 265 266 public static class PurePlaybackSupportFragment extends PlaybackSupportFragment { 267 @Override 268 public void onCreate(Bundle savedInstanceState) { 269 super.onCreate(savedInstanceState); 270 setFadingEnabled(false); 271 PlaybackControlsRow row = new PlaybackControlsRow(); 272 SparseArrayObjectAdapter primaryAdapter = new SparseArrayObjectAdapter( 273 new ControlButtonPresenterSelector()); 274 primaryAdapter.set(0, new PlaybackControlsRow.SkipPreviousAction(getActivity())); 275 primaryAdapter.set(1, new PlaybackControlsRow.PlayPauseAction(getActivity())); 276 primaryAdapter.set(2, new PlaybackControlsRow.SkipNextAction(getActivity())); 277 row.setPrimaryActionsAdapter(primaryAdapter); 278 row.setSecondaryActionsAdapter(null); 279 setPlaybackRow(row); 280 setPlaybackRowPresenter(new PlaybackControlsRowPresenter()); 281 } 282 } 283 284 @Test 285 public void setupRowAndPresenterWithoutGlue() { 286 SingleSupportFragmentTestActivity activity = 287 launchAndWaitActivity(PurePlaybackSupportFragment.class, 1000); 288 final PurePlaybackSupportFragment fragment = (PurePlaybackSupportFragment) 289 activity.getTestFragment(); 290 291 assertTrue(fragment.getAdapter().size() == 1); 292 View playRow = fragment.getVerticalGridView().getChildAt(0); 293 assertTrue(playRow.hasFocus()); 294 assertEquals(playRow.getResources().getDimensionPixelSize( 295 androidx.leanback.test.R.dimen.lb_playback_controls_padding_bottom), 296 fragment.getVerticalGridView().getHeight() - playRow.getBottom()); 297 } 298 299 public static class ControlGlueFragment extends PlaybackSupportFragment { 300 @Override 301 public void onCreate(Bundle savedInstanceState) { 302 super.onCreate(savedInstanceState); 303 int[] ffspeeds = new int[] {PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0, 304 PlaybackControlGlue.PLAYBACK_SPEED_FAST_L1}; 305 PlaybackGlue glue = new PlaybackControlGlue( 306 getActivity(), ffspeeds) { 307 @Override 308 public boolean hasValidMedia() { 309 return true; 310 } 311 312 @Override 313 public boolean isMediaPlaying() { 314 return false; 315 } 316 317 @Override 318 public CharSequence getMediaTitle() { 319 return "Title"; 320 } 321 322 @Override 323 public CharSequence getMediaSubtitle() { 324 return "SubTitle"; 325 } 326 327 @Override 328 public int getMediaDuration() { 329 return 100; 330 } 331 332 @Override 333 public Drawable getMediaArt() { 334 return null; 335 } 336 337 @Override 338 public long getSupportedActions() { 339 return PlaybackControlGlue.ACTION_PLAY_PAUSE; 340 } 341 342 @Override 343 public int getCurrentSpeedId() { 344 return PlaybackControlGlue.PLAYBACK_SPEED_PAUSED; 345 } 346 347 @Override 348 public int getCurrentPosition() { 349 return 50; 350 } 351 }; 352 glue.setHost(new PlaybackSupportFragmentGlueHost(this)); 353 } 354 } 355 356 @Test 357 public void setupWithControlGlue() throws Throwable { 358 SingleSupportFragmentTestActivity activity = 359 launchAndWaitActivity(ControlGlueFragment.class, 1000); 360 final ControlGlueFragment fragment = (ControlGlueFragment) 361 activity.getTestFragment(); 362 363 assertTrue(fragment.getAdapter().size() == 1); 364 365 View playRow = fragment.getVerticalGridView().getChildAt(0); 366 assertTrue(playRow.hasFocus()); 367 assertEquals(playRow.getResources().getDimensionPixelSize( 368 androidx.leanback.test.R.dimen.lb_playback_controls_padding_bottom), 369 fragment.getVerticalGridView().getHeight() - playRow.getBottom()); 370 } 371} 372