1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package com.example.android.tvleanback.ui; 15 16 import android.app.Activity; 17 import android.content.Context; 18 import android.content.Intent; 19 import android.media.MediaMetadataRetriever; 20 import android.os.Build; 21 import android.os.Bundle; 22 import android.os.Handler; 23 import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; 24 import android.support.v17.leanback.widget.Action; 25 import android.support.v17.leanback.widget.ArrayObjectAdapter; 26 import android.support.v17.leanback.widget.ClassPresenterSelector; 27 import android.support.v17.leanback.widget.ControlButtonPresenterSelector; 28 import android.support.v17.leanback.widget.HeaderItem; 29 import android.support.v17.leanback.widget.ImageCardView; 30 import android.support.v17.leanback.widget.ListRow; 31 import android.support.v17.leanback.widget.ListRowPresenter; 32 import android.support.v17.leanback.widget.OnActionClickedListener; 33 import android.support.v17.leanback.widget.OnItemViewClickedListener; 34 import android.support.v17.leanback.widget.OnItemViewSelectedListener; 35 import android.support.v17.leanback.widget.PlaybackControlsRow; 36 import android.support.v17.leanback.widget.PlaybackControlsRow.FastForwardAction; 37 import android.support.v17.leanback.widget.PlaybackControlsRow.PlayPauseAction; 38 import android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction; 39 import android.support.v17.leanback.widget.PlaybackControlsRow.RewindAction; 40 import android.support.v17.leanback.widget.PlaybackControlsRow.ShuffleAction; 41 import android.support.v17.leanback.widget.PlaybackControlsRow.SkipNextAction; 42 import android.support.v17.leanback.widget.PlaybackControlsRow.SkipPreviousAction; 43 import android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsDownAction; 44 import android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsUpAction; 45 import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; 46 import android.support.v17.leanback.widget.Presenter; 47 import android.support.v17.leanback.widget.Row; 48 import android.support.v17.leanback.widget.RowPresenter; 49 import android.support.v4.app.ActivityOptionsCompat; 50 import android.util.Log; 51 52 import com.bumptech.glide.Glide; 53 import com.bumptech.glide.load.resource.drawable.GlideDrawable; 54 import com.bumptech.glide.request.animation.GlideAnimation; 55 import com.bumptech.glide.request.target.SimpleTarget; 56 import com.example.android.tvleanback.R; 57 import com.example.android.tvleanback.data.VideoProvider; 58 import com.example.android.tvleanback.model.Movie; 59 import com.example.android.tvleanback.presenter.CardPresenter; 60 61 62 import java.util.ArrayList; 63 import java.util.HashMap; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Timer; 67 import java.util.TimerTask; 68 69 /* 70 * Class for video playback with media control 71 */ 72 public class PlaybackOverlayFragment extends android.support.v17.leanback.app.PlaybackOverlayFragment { 73 private static final String TAG = "PlaybackOverlayFragment"; 74 private static final boolean SHOW_DETAIL = true; 75 private static final boolean HIDE_MORE_ACTIONS = false; 76 private static final int PRIMARY_CONTROLS = 5; 77 private static final boolean SHOW_IMAGE = PRIMARY_CONTROLS <= 5; 78 private static final int BACKGROUND_TYPE = PlaybackOverlayFragment.BG_LIGHT; 79 private static final int CARD_WIDTH = 150; 80 private static final int CARD_HEIGHT = 240; 81 private static final int DEFAULT_UPDATE_PERIOD = 1000; 82 private static final int UPDATE_PERIOD = 16; 83 private static final int SIMULATED_BUFFERED_TIME = 10000; 84 private static final int CLICK_TRACKING_DELAY = 1000; 85 private static final int INITIAL_SPEED = 10000; 86 87 private static Context sContext; 88 private final Handler mClickTrackingHandler = new Handler(); 89 OnPlayPauseClickedListener mCallback; 90 private ArrayObjectAdapter mRowsAdapter; 91 private ArrayObjectAdapter mPrimaryActionsAdapter; 92 private ArrayObjectAdapter mSecondaryActionsAdapter; 93 private PlayPauseAction mPlayPauseAction; 94 private RepeatAction mRepeatAction; 95 private ThumbsUpAction mThumbsUpAction; 96 private ThumbsDownAction mThumbsDownAction; 97 private ShuffleAction mShuffleAction; 98 private FastForwardAction mFastForwardAction; 99 private RewindAction mRewindAction; 100 private SkipNextAction mSkipNextAction; 101 private SkipPreviousAction mSkipPreviousAction; 102 private PlaybackControlsRow mPlaybackControlsRow; 103 private ArrayList<Movie> mItems = new ArrayList<Movie>(); 104 private int mCurrentItem; 105 private long mDuration; 106 private Handler mHandler; 107 private Runnable mRunnable; 108 private Movie mSelectedMovie; 109 private int mFfwRwdSpeed = INITIAL_SPEED; 110 private Timer mClickTrackingTimer; 111 private int mClickCount; 112 113 @Override onCreate(Bundle savedInstanceState)114 public void onCreate(Bundle savedInstanceState) { 115 Log.i(TAG, "onCreate"); 116 super.onCreate(savedInstanceState); 117 sContext = getActivity(); 118 119 mItems = new ArrayList<Movie>(); 120 mSelectedMovie = (Movie) getActivity() 121 .getIntent().getParcelableExtra(MovieDetailsActivity.MOVIE); 122 123 HashMap<String, List<Movie>> movies = VideoProvider.getMovieList(); 124 125 if(movies != null) { 126 for (Map.Entry<String, List<Movie>> entry : movies.entrySet()) { 127 if (mSelectedMovie.getCategory().contains(entry.getKey())) { 128 List<Movie> list = entry.getValue(); 129 if(list != null && !list.isEmpty()) { 130 for (int j = 0; j < list.size(); j++) { 131 mItems.add(list.get(j)); 132 if (mSelectedMovie.getTitle().contentEquals(list.get(j).getTitle())) { 133 mCurrentItem = j; 134 } 135 } 136 } 137 } 138 } 139 } 140 141 mHandler = new Handler(); 142 143 setBackgroundType(BACKGROUND_TYPE); 144 setFadingEnabled(false); 145 146 setupRows(); 147 148 setOnItemViewSelectedListener(new OnItemViewSelectedListener() { 149 @Override 150 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 151 RowPresenter.ViewHolder rowViewHolder, Row row) { 152 Log.i(TAG, "onItemSelected: " + item + " row " + row); 153 } 154 }); 155 setOnItemViewClickedListener(new ItemViewClickedListener()); 156 } 157 158 @Override onAttach(Activity activity)159 public void onAttach(Activity activity) { 160 super.onAttach(activity); 161 162 // This makes sure that the container activity has implemented 163 // the callback interface. If not, it throws an exception 164 try { 165 mCallback = (OnPlayPauseClickedListener) activity; 166 } catch (ClassCastException e) { 167 throw new ClassCastException(activity.toString() 168 + " must implement OnPlayPauseClickedListener"); 169 } 170 } 171 172 @Override onResume()173 public void onResume(){ 174 super.onResume(); 175 } 176 setupRows()177 private void setupRows() { 178 179 ClassPresenterSelector ps = new ClassPresenterSelector(); 180 181 PlaybackControlsRowPresenter playbackControlsRowPresenter; 182 if (SHOW_DETAIL) { 183 playbackControlsRowPresenter = new PlaybackControlsRowPresenter( 184 new DescriptionPresenter()); 185 } else { 186 playbackControlsRowPresenter = new PlaybackControlsRowPresenter(); 187 } 188 playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() { 189 public void onActionClicked(Action action) { 190 if (action.getId() == mPlayPauseAction.getId()) { 191 if (mPlayPauseAction.getIndex() == PlayPauseAction.PLAY) { 192 startProgressAutomation(); 193 setFadingEnabled(true); 194 mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 195 mPlaybackControlsRow.getCurrentTime(), true); 196 } else { 197 stopProgressAutomation(); 198 setFadingEnabled(false); 199 mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 200 mPlaybackControlsRow.getCurrentTime(), false); 201 } 202 } else if (action.getId() == mSkipNextAction.getId()) { 203 next(); 204 } else if (action.getId() == mSkipPreviousAction.getId()) { 205 prev(); 206 } else if (action.getId() == mFastForwardAction.getId()) { 207 fastForward(); 208 } else if (action.getId() == mRewindAction.getId()) { 209 fastRewind(); 210 } 211 if (action instanceof PlaybackControlsRow.MultiAction) { 212 ((PlaybackControlsRow.MultiAction) action).nextIndex(); 213 notifyChanged(action); 214 } 215 } 216 }); 217 playbackControlsRowPresenter.setSecondaryActionsHidden(HIDE_MORE_ACTIONS); 218 219 ps.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter); 220 ps.addClassPresenter(ListRow.class, new ListRowPresenter()); 221 mRowsAdapter = new ArrayObjectAdapter(ps); 222 223 addPlaybackControlsRow(); 224 addOtherRows(); 225 226 setAdapter(mRowsAdapter); 227 } 228 getDuration()229 private int getDuration() { 230 Movie movie = mItems.get(mCurrentItem); 231 MediaMetadataRetriever mmr = new MediaMetadataRetriever(); 232 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 233 mmr.setDataSource(movie.getVideoUrl(), new HashMap<String, String>()); 234 } else { 235 mmr.setDataSource(movie.getVideoUrl()); 236 } 237 String time = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); 238 mDuration = Long.parseLong(time); 239 return (int) mDuration; 240 } 241 addPlaybackControlsRow()242 private void addPlaybackControlsRow() { 243 if (SHOW_DETAIL) { 244 mPlaybackControlsRow = new PlaybackControlsRow(mSelectedMovie); 245 } else { 246 mPlaybackControlsRow = new PlaybackControlsRow(); 247 } 248 mRowsAdapter.add(mPlaybackControlsRow); 249 250 updatePlaybackRow(mCurrentItem); 251 252 ControlButtonPresenterSelector presenterSelector = new ControlButtonPresenterSelector(); 253 mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); 254 mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); 255 mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter); 256 mPlaybackControlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); 257 258 mPlayPauseAction = new PlayPauseAction(sContext); 259 mRepeatAction = new RepeatAction(sContext); 260 mThumbsUpAction = new ThumbsUpAction(sContext); 261 mThumbsDownAction = new ThumbsDownAction(sContext); 262 mShuffleAction = new ShuffleAction(sContext); 263 mSkipNextAction = new PlaybackControlsRow.SkipNextAction(sContext); 264 mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(sContext); 265 mFastForwardAction = new PlaybackControlsRow.FastForwardAction(sContext); 266 mRewindAction = new PlaybackControlsRow.RewindAction(sContext); 267 268 if (PRIMARY_CONTROLS > 5) { 269 mPrimaryActionsAdapter.add(mThumbsUpAction); 270 } else { 271 mSecondaryActionsAdapter.add(mThumbsUpAction); 272 } 273 mPrimaryActionsAdapter.add(mSkipPreviousAction); 274 if (PRIMARY_CONTROLS > 3) { 275 mPrimaryActionsAdapter.add(new PlaybackControlsRow.RewindAction(sContext)); 276 } 277 mPrimaryActionsAdapter.add(mPlayPauseAction); 278 if (PRIMARY_CONTROLS > 3) { 279 mPrimaryActionsAdapter.add(new PlaybackControlsRow.FastForwardAction(sContext)); 280 } 281 mPrimaryActionsAdapter.add(mSkipNextAction); 282 283 mSecondaryActionsAdapter.add(mRepeatAction); 284 mSecondaryActionsAdapter.add(mShuffleAction); 285 if (PRIMARY_CONTROLS > 5) { 286 mPrimaryActionsAdapter.add(mThumbsDownAction); 287 } else { 288 mSecondaryActionsAdapter.add(mThumbsDownAction); 289 } 290 mSecondaryActionsAdapter.add(new PlaybackControlsRow.HighQualityAction(sContext)); 291 mSecondaryActionsAdapter.add(new PlaybackControlsRow.ClosedCaptioningAction(sContext)); 292 } 293 notifyChanged(Action action)294 private void notifyChanged(Action action) { 295 ArrayObjectAdapter adapter = mPrimaryActionsAdapter; 296 if (adapter.indexOf(action) >= 0) { 297 adapter.notifyArrayItemRangeChanged(adapter.indexOf(action), 1); 298 return; 299 } 300 adapter = mSecondaryActionsAdapter; 301 if (adapter.indexOf(action) >= 0) { 302 adapter.notifyArrayItemRangeChanged(adapter.indexOf(action), 1); 303 return; 304 } 305 } 306 updatePlaybackRow(int index)307 private void updatePlaybackRow(int index) { 308 if (mPlaybackControlsRow.getItem() != null) { 309 Movie item = (Movie) mPlaybackControlsRow.getItem(); 310 item.setTitle(mItems.get(mCurrentItem).getTitle()); 311 item.setStudio(mItems.get(mCurrentItem).getStudio()); 312 } 313 if (SHOW_IMAGE) { 314 updateVideoImage(mItems.get(mCurrentItem).getCardImageUrl()); 315 } 316 mRowsAdapter.notifyArrayItemRangeChanged(0, 1); 317 mPlaybackControlsRow.setTotalTime(getDuration()); 318 mPlaybackControlsRow.setCurrentTime(0); 319 mPlaybackControlsRow.setBufferedProgress(0); 320 } 321 addOtherRows()322 private void addOtherRows() { 323 ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); 324 for (Movie movie : mItems) { 325 listRowAdapter.add(movie); 326 } 327 HeaderItem header = new HeaderItem(0, getString(R.string.related_movies)); 328 mRowsAdapter.add(new ListRow(header, listRowAdapter)); 329 330 } 331 getUpdatePeriod()332 private int getUpdatePeriod() { 333 if (getView() == null || mPlaybackControlsRow.getTotalTime() <= 0) { 334 return DEFAULT_UPDATE_PERIOD; 335 } 336 return Math.max(UPDATE_PERIOD, mPlaybackControlsRow.getTotalTime() / getView().getWidth()); 337 } 338 startProgressAutomation()339 private void startProgressAutomation() { 340 mRunnable = new Runnable() { 341 @Override 342 public void run() { 343 int updatePeriod = getUpdatePeriod(); 344 int currentTime = mPlaybackControlsRow.getCurrentTime() + updatePeriod; 345 int totalTime = mPlaybackControlsRow.getTotalTime(); 346 mPlaybackControlsRow.setCurrentTime(currentTime); 347 mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME); 348 349 if (totalTime > 0 && totalTime <= currentTime) { 350 next(); 351 } 352 mHandler.postDelayed(this, updatePeriod); 353 } 354 }; 355 mHandler.postDelayed(mRunnable, getUpdatePeriod()); 356 } 357 next()358 private void next() { 359 if (++mCurrentItem >= mItems.size()) { 360 mCurrentItem = 0; 361 } 362 if (mPlayPauseAction.getIndex() == PlayPauseAction.PLAY) { 363 mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 0, false); 364 } else { 365 mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 0, true); 366 } 367 mFfwRwdSpeed = INITIAL_SPEED; 368 updatePlaybackRow(mCurrentItem); 369 } 370 prev()371 private void prev() { 372 if (--mCurrentItem < 0) { 373 mCurrentItem = mItems.size() - 1; 374 } 375 if (mPlayPauseAction.getIndex() == PlayPauseAction.PLAY) { 376 mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 0, false); 377 } else { 378 mCallback.onFragmentPlayPause(mItems.get(mCurrentItem), 0, true); 379 } 380 mFfwRwdSpeed = INITIAL_SPEED; 381 updatePlaybackRow(mCurrentItem); 382 } 383 fastForward()384 private void fastForward() { 385 Log.d(TAG, "current time: " + mPlaybackControlsRow.getCurrentTime()); 386 startClickTrackingTimer(); 387 int currentTime = mPlaybackControlsRow.getCurrentTime() + mFfwRwdSpeed; 388 if (currentTime > (int) mDuration) { 389 currentTime = (int) mDuration; 390 } 391 fastFR(currentTime); 392 } 393 fastRewind()394 private void fastRewind() { 395 startClickTrackingTimer(); 396 int currentTime = mPlaybackControlsRow.getCurrentTime() - mFfwRwdSpeed; 397 if (currentTime < 0 || currentTime > (int) mDuration) { 398 currentTime = 0; 399 } 400 fastFR(currentTime); 401 } 402 fastFR(int currentTime)403 private void fastFR(int currentTime) { 404 mCallback.onFragmentFfwRwd(mItems.get(mCurrentItem), currentTime); 405 mPlaybackControlsRow.setCurrentTime(currentTime); 406 mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME); 407 } 408 stopProgressAutomation()409 private void stopProgressAutomation() { 410 if (mHandler != null && mRunnable != null) { 411 mHandler.removeCallbacks(mRunnable); 412 } 413 } 414 415 @Override onStop()416 public void onStop() { 417 stopProgressAutomation(); 418 super.onStop(); 419 } 420 updateVideoImage(String uri)421 protected void updateVideoImage(String uri) { 422 Glide.with(sContext) 423 .load(uri) 424 .centerCrop() 425 .into(new SimpleTarget<GlideDrawable>(CARD_WIDTH, CARD_HEIGHT) { 426 @Override 427 public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) { 428 mPlaybackControlsRow.setImageDrawable(resource); 429 mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size()); 430 } 431 }); 432 } 433 startClickTrackingTimer()434 private void startClickTrackingTimer() { 435 if (null != mClickTrackingTimer) { 436 mClickCount++; 437 mClickTrackingTimer.cancel(); 438 } else { 439 mClickCount = 0; 440 mFfwRwdSpeed = INITIAL_SPEED; 441 } 442 mClickTrackingTimer = new Timer(); 443 mClickTrackingTimer.schedule(new UpdateFfwRwdSpeedTask(), CLICK_TRACKING_DELAY); 444 } 445 446 // Container Activity must implement this interface 447 public interface OnPlayPauseClickedListener { onFragmentPlayPause(Movie movie, int position, Boolean playPause)448 public void onFragmentPlayPause(Movie movie, int position, Boolean playPause); 449 onFragmentFfwRwd(Movie movie, int position)450 public void onFragmentFfwRwd(Movie movie, int position); 451 } 452 453 static class DescriptionPresenter extends AbstractDetailsDescriptionPresenter { 454 @Override onBindDescription(ViewHolder viewHolder, Object item)455 protected void onBindDescription(ViewHolder viewHolder, Object item) { 456 viewHolder.getTitle().setText(((Movie) item).getTitle()); 457 viewHolder.getSubtitle().setText(((Movie) item).getStudio()); 458 } 459 } 460 461 private class UpdateFfwRwdSpeedTask extends TimerTask { 462 463 @Override run()464 public void run() { 465 mClickTrackingHandler.post(new Runnable() { 466 @Override 467 public void run() { 468 if (mClickCount == 0) { 469 mFfwRwdSpeed = INITIAL_SPEED; 470 } else if (mClickCount == 1) { 471 mFfwRwdSpeed *= 2; 472 } else if (mClickCount >= 2) { 473 mFfwRwdSpeed *= 4; 474 } 475 mClickCount = 0; 476 mClickTrackingTimer = null; 477 } 478 }); 479 } 480 } 481 482 private final class ItemViewClickedListener implements OnItemViewClickedListener { 483 @Override onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row)484 public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, 485 RowPresenter.ViewHolder rowViewHolder, Row row) { 486 487 if (item instanceof Movie) { 488 Movie movie = (Movie) item; 489 Log.d(TAG, "Item: " + item.toString()); 490 Intent intent = new Intent(getActivity(), PlaybackOverlayActivity.class); 491 intent.putExtra(MovieDetailsActivity.MOVIE, movie); 492 493 Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( 494 getActivity(), 495 ((ImageCardView) itemViewHolder.view).getMainImageView(), 496 MovieDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); 497 getActivity().startActivity(intent, bundle); 498 } 499 } 500 } 501 502 } 503