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