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 android.support.v17.leanback.app; 17 18import android.content.Context; 19import android.graphics.drawable.Drawable; 20import android.os.Bundle; 21import android.os.Handler; 22import android.support.v17.leanback.test.R; 23import android.support.v17.leanback.widget.Action; 24import android.support.v17.leanback.widget.ArrayObjectAdapter; 25import android.support.v17.leanback.widget.ClassPresenterSelector; 26import android.support.v17.leanback.widget.ControlButtonPresenterSelector; 27import android.support.v17.leanback.widget.HeaderItem; 28import android.support.v17.leanback.widget.ListRow; 29import android.support.v17.leanback.widget.ListRowPresenter; 30import android.support.v17.leanback.widget.OnItemViewClickedListener; 31import android.support.v17.leanback.widget.OnItemViewSelectedListener; 32import android.support.v17.leanback.widget.PlaybackControlsRow; 33import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; 34import android.support.v17.leanback.widget.Presenter; 35import android.support.v17.leanback.widget.PresenterSelector; 36import android.support.v17.leanback.widget.Row; 37import android.support.v17.leanback.widget.RowPresenter; 38import android.support.v17.leanback.widget.SparseArrayObjectAdapter; 39import android.util.Log; 40import android.view.KeyEvent; 41import android.view.View; 42import android.widget.Toast; 43 44public class PlaybackOverlayTestFragment extends PlaybackOverlayFragment { 45 private static final String TAG = "leanback.PlaybackControlsFragment"; 46 47 /** 48 * Change this to choose a different overlay background. 49 */ 50 private static final int BACKGROUND_TYPE = PlaybackOverlayFragment.BG_LIGHT; 51 52 /** 53 * Change the number of related content rows. 54 */ 55 private static final int RELATED_CONTENT_ROWS = 3; 56 57 /** 58 * Change this to select hidden 59 */ 60 private static final boolean SECONDARY_HIDDEN = false; 61 62 private static final int ROW_CONTROLS = 0; 63 64 private PlaybackControlHelper mGlue; 65 private PlaybackControlsRowPresenter mPlaybackControlsRowPresenter; 66 67 private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() { 68 @Override 69 public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, 70 RowPresenter.ViewHolder rowViewHolder, Row row) { 71 Log.i(TAG, "onItemClicked: " + item + " row " + row); 72 if (item instanceof Action) { 73 mGlue.onActionClicked((Action) item); 74 } 75 } 76 }; 77 78 private OnItemViewSelectedListener mOnItemViewSelectedListener = 79 new OnItemViewSelectedListener() { 80 @Override 81 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 82 RowPresenter.ViewHolder rowViewHolder, Row row) { 83 Log.i(TAG, "onItemSelected: " + item + " row " + row); 84 } 85 }; 86 87 @Override 88 public SparseArrayObjectAdapter getAdapter() { 89 return (SparseArrayObjectAdapter) super.getAdapter(); 90 } 91 92 @Override 93 public void onCreate(Bundle savedInstanceState) { 94 Log.i(TAG, "onCreate"); 95 super.onCreate(savedInstanceState); 96 97 setBackgroundType(BACKGROUND_TYPE); 98 setOnItemViewSelectedListener(mOnItemViewSelectedListener); 99 100 createComponents(getActivity()); 101 } 102 103 private void createComponents(Context context) { 104 mGlue = new PlaybackControlHelper(context, this) { 105 @Override 106 public int getUpdatePeriod() { 107 long totalTime = getControlsRow().getDuration(); 108 if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) { 109 return 1000; 110 } 111 return 16; 112 } 113 114 @Override 115 protected void onRowChanged(PlaybackControlsRow row) { 116 if (getAdapter() == null) { 117 return; 118 } 119 int index = getAdapter().indexOf(row); 120 if (index >= 0) { 121 getAdapter().notifyArrayItemRangeChanged(index, 1); 122 } 123 } 124 125 @Override 126 public void onActionClicked(Action action) { 127 if (action.getId() == R.id.lb_control_picture_in_picture) { 128 getActivity().enterPictureInPictureMode(); 129 return; 130 } 131 super.onActionClicked(action); 132 } 133 }; 134 135 mGlue.setOnItemViewClickedListener(mOnItemViewClickedListener); 136 137 mPlaybackControlsRowPresenter = mGlue.createControlsRowAndPresenter(); 138 mPlaybackControlsRowPresenter.setSecondaryActionsHidden(SECONDARY_HIDDEN); 139 ClassPresenterSelector selector = new ClassPresenterSelector(); 140 selector.addClassPresenter(ListRow.class, new ListRowPresenter()); 141 selector.addClassPresenter(PlaybackControlsRow.class, mPlaybackControlsRowPresenter); 142 143 setAdapter(new SparseArrayObjectAdapter(selector)); 144 145 // Add the controls row 146 getAdapter().set(ROW_CONTROLS, mGlue.getControlsRow()); 147 148 // Add related content rows 149 for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) { 150 ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter()); 151 listRowAdapter.add("Some related content"); 152 listRowAdapter.add("Other related content"); 153 HeaderItem header = new HeaderItem(i, "Row " + i); 154 getAdapter().set(ROW_CONTROLS + 1 + i, new ListRow(header, listRowAdapter)); 155 } 156 157 } 158 159 @Override 160 public void onStart() { 161 super.onStart(); 162 mGlue.setFadingEnabled(true); 163 } 164 165 abstract static class PlaybackControlHelper extends PlaybackControlGlue { 166 /** 167 * Change the location of the thumbs up/down controls 168 */ 169 private static final boolean THUMBS_PRIMARY = true; 170 171 private static final String FAUX_TITLE = "A short song of silence"; 172 private static final String FAUX_SUBTITLE = "2014"; 173 private static final int FAUX_DURATION = 33 * 1000; 174 175 // These should match the playback service FF behavior 176 private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 }; 177 178 private boolean mIsPlaying; 179 private int mSpeed = PlaybackControlGlue.PLAYBACK_SPEED_PAUSED; 180 private long mStartTime; 181 private long mStartPosition = 0; 182 183 private PlaybackControlsRow.RepeatAction mRepeatAction; 184 private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction; 185 private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction; 186 private PlaybackControlsRow.PictureInPictureAction mPipAction; 187 private static Handler mHandler = new Handler(); 188 189 private final Runnable mUpdateProgressRunnable = new Runnable() { 190 @Override 191 public void run() { 192 updateProgress(); 193 mHandler.postDelayed(this, getUpdatePeriod()); 194 } 195 }; 196 197 PlaybackControlHelper(Context context, PlaybackOverlayFragment fragment) { 198 super(context, fragment, sFastForwardSpeeds); 199 mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context); 200 mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE); 201 mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context); 202 mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE); 203 mRepeatAction = new PlaybackControlsRow.RepeatAction(context); 204 mPipAction = new PlaybackControlsRow.PictureInPictureAction(context); 205 } 206 207 @Override 208 public PlaybackControlsRowPresenter createControlsRowAndPresenter() { 209 PlaybackControlsRowPresenter presenter = super.createControlsRowAndPresenter(); 210 211 ArrayObjectAdapter adapter = new ArrayObjectAdapter( 212 new ControlButtonPresenterSelector()); 213 getControlsRow().setSecondaryActionsAdapter(adapter); 214 if (!THUMBS_PRIMARY) { 215 adapter.add(mThumbsDownAction); 216 } 217 if (android.os.Build.VERSION.SDK_INT > 23) { 218 adapter.add(mPipAction); 219 } 220 adapter.add(mRepeatAction); 221 if (!THUMBS_PRIMARY) { 222 adapter.add(mThumbsUpAction); 223 } 224 225 return presenter; 226 } 227 228 @Override 229 protected SparseArrayObjectAdapter createPrimaryActionsAdapter( 230 PresenterSelector presenterSelector) { 231 SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector); 232 if (THUMBS_PRIMARY) { 233 adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction); 234 adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction); 235 } 236 return adapter; 237 } 238 239 @Override 240 public void onActionClicked(Action action) { 241 if (shouldDispatchAction(action)) { 242 dispatchAction(action); 243 return; 244 } 245 super.onActionClicked(action); 246 } 247 248 @Override 249 public boolean onKey(View view, int keyCode, KeyEvent keyEvent) { 250 if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) { 251 Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode()); 252 if (shouldDispatchAction(action)) { 253 dispatchAction(action); 254 return true; 255 } 256 } 257 return super.onKey(view, keyCode, keyEvent); 258 } 259 260 private boolean shouldDispatchAction(Action action) { 261 return action == mRepeatAction || action == mThumbsUpAction 262 || action == mThumbsDownAction; 263 } 264 265 private void dispatchAction(Action action) { 266 Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show(); 267 PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action; 268 multiAction.nextIndex(); 269 notifyActionChanged(multiAction); 270 } 271 272 private void notifyActionChanged(PlaybackControlsRow.MultiAction action) { 273 int index; 274 index = getPrimaryActionsAdapter().indexOf(action); 275 if (index >= 0) { 276 getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1); 277 } else { 278 index = getSecondaryActionsAdapter().indexOf(action); 279 if (index >= 0) { 280 getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1); 281 } 282 } 283 } 284 285 private SparseArrayObjectAdapter getPrimaryActionsAdapter() { 286 return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter(); 287 } 288 289 private ArrayObjectAdapter getSecondaryActionsAdapter() { 290 return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter(); 291 } 292 293 @Override 294 public boolean hasValidMedia() { 295 return true; 296 } 297 298 @Override 299 public boolean isMediaPlaying() { 300 return mIsPlaying; 301 } 302 303 @Override 304 public CharSequence getMediaTitle() { 305 return FAUX_TITLE; 306 } 307 308 @Override 309 public CharSequence getMediaSubtitle() { 310 return FAUX_SUBTITLE; 311 } 312 313 @Override 314 public int getMediaDuration() { 315 return FAUX_DURATION; 316 } 317 318 @Override 319 public Drawable getMediaArt() { 320 return null; 321 } 322 323 @Override 324 public long getSupportedActions() { 325 return PlaybackControlGlue.ACTION_PLAY_PAUSE 326 | PlaybackControlGlue.ACTION_FAST_FORWARD 327 | PlaybackControlGlue.ACTION_REWIND; 328 } 329 330 @Override 331 public int getCurrentSpeedId() { 332 return mSpeed; 333 } 334 335 @Override 336 public int getCurrentPosition() { 337 int speed; 338 if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) { 339 speed = 0; 340 } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) { 341 speed = 1; 342 } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) { 343 int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0; 344 speed = getFastForwardSpeeds()[index]; 345 } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) { 346 int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0; 347 speed = -getRewindSpeeds()[index]; 348 } else { 349 return -1; 350 } 351 long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed; 352 if (position > getMediaDuration()) { 353 position = getMediaDuration(); 354 onPlaybackComplete(true); 355 } else if (position < 0) { 356 position = 0; 357 onPlaybackComplete(false); 358 } 359 return (int) position; 360 } 361 362 void onPlaybackComplete(final boolean ended) { 363 mHandler.post(new Runnable() { 364 @Override 365 public void run() { 366 if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.INDEX_NONE) { 367 pausePlayback(); 368 } else { 369 startPlayback(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL); 370 } 371 mStartPosition = 0; 372 onStateChanged(); 373 } 374 }); 375 } 376 377 @Override 378 protected void startPlayback(int speed) { 379 if (speed == mSpeed) { 380 return; 381 } 382 mStartPosition = getCurrentPosition(); 383 mSpeed = speed; 384 mIsPlaying = true; 385 mStartTime = System.currentTimeMillis(); 386 } 387 388 @Override 389 protected void pausePlayback() { 390 if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) { 391 return; 392 } 393 mStartPosition = getCurrentPosition(); 394 mSpeed = PlaybackControlGlue.PLAYBACK_SPEED_PAUSED; 395 mIsPlaying = false; 396 } 397 398 @Override 399 protected void skipToNext() { 400 // Not supported 401 } 402 403 @Override 404 protected void skipToPrevious() { 405 // Not supported 406 } 407 408 @Override 409 public void enableProgressUpdating(boolean enable) { 410 mHandler.removeCallbacks(mUpdateProgressRunnable); 411 if (enable) { 412 mUpdateProgressRunnable.run(); 413 } 414 } 415 } 416} 417