/* * Copyright 2014 Google Inc. All rights reserved. * * 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 com.android.example.leanback.fastlane; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.MediaMetadataRetriever; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.ControlButtonPresenterSelector; import android.support.v17.leanback.widget.CursorObjectAdapter; 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.ObjectAdapter; import android.support.v17.leanback.widget.OnActionClickedListener; 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.PlaybackControlsRow.FastForwardAction; import android.support.v17.leanback.widget.PlaybackControlsRow.PlayPauseAction; import android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction; import android.support.v17.leanback.widget.PlaybackControlsRow.RewindAction; import android.support.v17.leanback.widget.PlaybackControlsRow.ShuffleAction; import android.support.v17.leanback.widget.PlaybackControlsRow.SkipNextAction; import android.support.v17.leanback.widget.PlaybackControlsRow.SkipPreviousAction; import android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsDownAction; import android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsUpAction; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; 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.SinglePresenterSelector; import android.util.Log; import com.android.example.leanback.R; import com.android.example.leanback.data.Video; import com.android.example.leanback.data.VideoDataManager; import com.android.example.leanback.data.VideoItemContract; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.Timer; import java.util.TimerTask; /* * Class for video playback with media control */ public class PlaybackOverlayFragment extends android.support.v17.leanback.app.PlaybackOverlayFragment { private static final String TAG = PlaybackOverlayFragment.class.getSimpleName(); private static Activity sActivity; private static final boolean SHOW_DETAIL = true; private static final boolean HIDE_MORE_ACTIONS = false; private static final int PRIMARY_CONTROLS = 5; private static final boolean SHOW_IMAGE = PRIMARY_CONTROLS <= 5; private static final int BACKGROUND_TYPE = PlaybackOverlayFragment.BG_LIGHT; private static final int CARD_WIDTH = 200; private static final int CARD_HEIGHT = 240; private static final int DEFAULT_UPDATE_PERIOD = 1000; private static final int UPDATE_PERIOD = 16; private static final int SIMULATED_BUFFERED_TIME = 10000; private static final int CLICK_TRACKING_DELAY = 1000; private ArrayObjectAdapter mRowsAdapter; private ArrayObjectAdapter mPrimaryActionsAdapter; private ArrayObjectAdapter mSecondaryActionsAdapter; private PlayPauseAction mPlayPauseAction; private RepeatAction mRepeatAction; private ThumbsUpAction mThumbsUpAction; private ThumbsDownAction mThumbsDownAction; private ShuffleAction mShuffleAction; private FastForwardAction mFastForwardAction; private RewindAction mRewindAction; private SkipNextAction mSkipNextAction; private SkipPreviousAction mSkipPreviousAction; private PlaybackControlsRow mPlaybackControlsRow; private long mDuration; private Handler mHandler; private Runnable mRunnable; private PicassoPlaybackControlsRowTarget mPlaybackControlsRowTarget; private Timer mClickTrackingTimer; private int mClickCount; private final Handler mClickTrackingHandler = new Handler(); OnPlayPauseClickedListener mCallback; private OnActionListener mOnActionListener = new OnActionListener(); private VideoDataManager mManager; private int mCurrentIndex; private Video mVideo; private ArrayList<Video> mItems; public void pressPlay() { mPlayPauseAction.setIndex(PlayPauseAction.PLAY); mOnActionListener.onActionClicked(mPlayPauseAction); } public void pressPause() { mPlayPauseAction.setIndex(PlayPauseAction.PAUSE); mOnActionListener.onActionClicked(mPlayPauseAction); } public void pressSkipNext() { mOnActionListener.onActionClicked(mSkipNextAction); } public void pressSkipPrevious() { mOnActionListener.onActionClicked(mSkipPreviousAction); } public void pressFastForward() { mOnActionListener.onActionClicked(mFastForwardAction); } public void pressRewind() { mOnActionListener.onActionClicked(mRewindAction); } // Container Activity must implement this interface public interface OnPlayPauseClickedListener { public void onFragmentPlayPause(Video video, int position, Boolean playPause); public void onFragmentFfwRwd(Video video, int position); } @Override public void onAttach(Activity activity) { super.onAttach(activity); sActivity = activity; // This makes sure that the container activity has implemented // the callback interface. If not, it throws an exception try { mCallback = (OnPlayPauseClickedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnPlayPauseClickedListener"); } } @Override public void onCreate(Bundle savedInstanceState) { Log.i(TAG, "onCreate"); super.onCreate(savedInstanceState); final ObjectAdapter rowContents = new CursorObjectAdapter((new SinglePresenterSelector(new CardPresenter()))); rowContents.registerObserver(new ObjectAdapter.DataObserver() { @Override public void onChanged() { super.onChanged(); mItems = new ArrayList<Video>(); for (int i = 0; i < rowContents.size(); i++) { mItems.add((Video)rowContents.get(i)); } } }); mManager = new VideoDataManager(getActivity(), getLoaderManager(), VideoItemContract.VideoItem.buildDirUri(), rowContents ); mManager.startDataLoading(); mHandler = new Handler(); mVideo = (Video)sActivity.getIntent().getSerializableExtra(Video.INTENT_EXTRA_VIDEO); Log.d(TAG, "onCreate mVideo=" + mVideo); setBackgroundType(BACKGROUND_TYPE); setFadingEnabled(false); setupRows(); setOnItemViewSelectedListener(new OnItemViewSelectedListener() { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { Log.i(TAG, "onItemSelected: " + item + " row " + row); } }); setOnItemViewClickedListener(new OnItemViewClickedListener() { @Override public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { Log.i(TAG, "onItemClicked: " + item + " row " + row); if (item instanceof Video) { Video selected = (Video) item; for (int index = 0; index < mItems.size(); index++) { Video video = mItems.get(index); if (selected.getTitle().equals(video.getTitle())) { setVideoIndex(index); return; } } } } }); } private void setupRows() { ClassPresenterSelector ps = new ClassPresenterSelector(); PlaybackControlsRowPresenter playbackControlsRowPresenter; if (SHOW_DETAIL) { playbackControlsRowPresenter = new PlaybackControlsRowPresenter( new DescriptionPresenter()); } else { playbackControlsRowPresenter = new PlaybackControlsRowPresenter(); } playbackControlsRowPresenter.setOnActionClickedListener(mOnActionListener); playbackControlsRowPresenter.setSecondaryActionsHidden(HIDE_MORE_ACTIONS); ps.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter); ps.addClassPresenter(ListRow.class, new ListRowPresenter()); mRowsAdapter = new ArrayObjectAdapter(ps); addPlaybackControlsRow(); addOtherRows(); setAdapter(mRowsAdapter); } private int getDuration() { Log.d(TAG, "getDuration()"); MediaMetadataRetriever mmr = new MediaMetadataRetriever(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { Log.d(TAG, "mVideo.getContentUrl()=" + mVideo.getContentUrl()); mmr.setDataSource(mVideo.getContentUrl(), new HashMap<String, String>()); } else { mmr.setDataSource(mVideo.getContentUrl()); } String time = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); mDuration = Long.parseLong(time); Log.d(TAG, "mDuration=" + mDuration); return (int) mDuration; } private int getCurrentIndex() { return mCurrentIndex; } private void addPlaybackControlsRow() { if (mVideo != null) { mPlaybackControlsRow = new PlaybackControlsRow(mVideo); } else { mPlaybackControlsRow = new PlaybackControlsRow(); } mRowsAdapter.add(mPlaybackControlsRow); updatePlaybackRow(); ControlButtonPresenterSelector presenterSelector = new ControlButtonPresenterSelector(); mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter); mPlaybackControlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); mPlayPauseAction = new PlayPauseAction(sActivity); mRepeatAction = new RepeatAction(sActivity); mThumbsUpAction = new ThumbsUpAction(sActivity); mThumbsDownAction = new ThumbsDownAction(sActivity); mShuffleAction = new ShuffleAction(sActivity); mSkipNextAction = new SkipNextAction(sActivity); mSkipPreviousAction = new SkipPreviousAction(sActivity); mFastForwardAction = new FastForwardAction(sActivity); mRewindAction = new RewindAction(sActivity); if (PRIMARY_CONTROLS > 5) { mPrimaryActionsAdapter.add(mThumbsUpAction); } else { mSecondaryActionsAdapter.add(mThumbsUpAction); } mPrimaryActionsAdapter.add(mSkipPreviousAction); if (PRIMARY_CONTROLS > 3) { mPrimaryActionsAdapter.add(new RewindAction(sActivity)); } mPrimaryActionsAdapter.add(mPlayPauseAction); if (PRIMARY_CONTROLS > 3) { mPrimaryActionsAdapter.add(new FastForwardAction(sActivity)); } mPrimaryActionsAdapter.add(mSkipNextAction); mSecondaryActionsAdapter.add(mRepeatAction); mSecondaryActionsAdapter.add(mShuffleAction); if (PRIMARY_CONTROLS > 5) { mPrimaryActionsAdapter.add(mThumbsDownAction); } else { mSecondaryActionsAdapter.add(mThumbsDownAction); } mSecondaryActionsAdapter.add(new PlaybackControlsRow.HighQualityAction(sActivity)); mSecondaryActionsAdapter.add(new PlaybackControlsRow.ClosedCaptioningAction(sActivity)); } private void notifyChanged(Action action) { ArrayObjectAdapter adapter = mPrimaryActionsAdapter; if (adapter.indexOf(action) >= 0) { adapter.notifyArrayItemRangeChanged(adapter.indexOf(action), 1); return; } adapter = mSecondaryActionsAdapter; if (adapter.indexOf(action) >= 0) { adapter.notifyArrayItemRangeChanged(adapter.indexOf(action), 1); return; } } private void updatePlaybackRow() { if (mVideo == null) { return; } if (mPlaybackControlsRow.getItem() != null) { Video item = (Video) mPlaybackControlsRow.getItem(); item.setTitle(mVideo.getTitle()); item.setDescription(mVideo.getDescription()); } mPlaybackControlsRowTarget = new PicassoPlaybackControlsRowTarget(mPlaybackControlsRow); updateVideoImage(getThumbURI(mVideo)); mRowsAdapter.notifyArrayItemRangeChanged(0, 1); int duration = getDuration(); mPlaybackControlsRow.setTotalTime(duration); mPlaybackControlsRow.setCurrentTime(0); mPlaybackControlsRow.setBufferedProgress(0); Log.d(TAG, "setTotalTime(getDuration()=" + duration + ")"); } private static URI getThumbURI(Video video) { try { return new URI(video.getThumbUrl()); } catch (URISyntaxException e) { return null; } } private void addOtherRows() { ObjectAdapter rowContents = new CursorObjectAdapter((new SinglePresenterSelector(new CardPresenter()))); VideoDataManager manager = new VideoDataManager(getActivity(), getLoaderManager(), VideoItemContract.VideoItem.buildDirUri(), rowContents ); manager.startDataLoading(); HeaderItem headerItem = new HeaderItem(0, "You may also like", null); mRowsAdapter.add(new ListRow(headerItem, manager.getItemList())); } private int getUpdatePeriod() { if (getView() == null || mPlaybackControlsRow.getTotalTime() <= 0) { return DEFAULT_UPDATE_PERIOD; } return Math.max(UPDATE_PERIOD, mPlaybackControlsRow.getTotalTime() / getView().getWidth()); } private void startProgressAutomation() { mRunnable = new Runnable() { @Override public void run() { int updatePeriod = getUpdatePeriod(); int currentTime = mPlaybackControlsRow.getCurrentTime() + updatePeriod; int totalTime = mPlaybackControlsRow.getTotalTime(); mPlaybackControlsRow.setCurrentTime(currentTime); mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME); if (totalTime > 0 && totalTime <= currentTime) { next(); } mHandler.postDelayed(this, updatePeriod); } }; mHandler.postDelayed(mRunnable, getUpdatePeriod()); } private void next() { int currentIndex = getCurrentIndex(); if (++currentIndex >= mItems.size()) { currentIndex = 0; } setVideoIndex(currentIndex); } private void prev() { int currentIndex = getCurrentIndex(); if (--currentIndex< 0) { currentIndex = mItems.size() - 1; } setVideoIndex(currentIndex); } private void setVideoIndex(int itemIndex) { mCurrentIndex = itemIndex; mVideo = mItems.get(mCurrentIndex); if (mPlayPauseAction.getIndex() == PlayPauseAction.PLAY) { mCallback.onFragmentPlayPause(mVideo, 0, false); } else { mCallback.onFragmentPlayPause(mVideo, 0, true); } updatePlaybackRow(); } private void fastForward() { Log.d(TAG, "current time: " + mPlaybackControlsRow.getCurrentTime()); updateRapidFfRwClickTracker(); int currentTime = mPlaybackControlsRow.getCurrentTime() + getFfRwSpeed(); if( currentTime > (int) mDuration ) { currentTime = (int) mDuration; } mCallback.onFragmentFfwRwd(mVideo, currentTime); mPlaybackControlsRow.setCurrentTime(currentTime); mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME); } private void fastRewind() { Log.d(TAG, "current time: " + mPlaybackControlsRow.getCurrentTime()); updateRapidFfRwClickTracker(); int currentTime = mPlaybackControlsRow.getCurrentTime() - getFfRwSpeed(); if( currentTime < 0 || currentTime > (int) mDuration ) { currentTime = 0; } mCallback.onFragmentFfwRwd(mVideo, currentTime); mPlaybackControlsRow.setCurrentTime(currentTime); mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME); } private void stopProgressAutomation() { if (mHandler != null && mRunnable != null) { mHandler.removeCallbacks(mRunnable); } } @Override public void onStop() { stopProgressAutomation(); super.onStop(); } static class DescriptionPresenter extends AbstractDetailsDescriptionPresenter { @Override protected void onBindDescription(ViewHolder viewHolder, Object item) { viewHolder.getTitle().setText(((Video) item).getTitle()); viewHolder.getSubtitle().setText(((Video) item).getDescription()); } } public static class PicassoPlaybackControlsRowTarget implements Target { PlaybackControlsRow mPlaybackControlsRow; public PicassoPlaybackControlsRowTarget(PlaybackControlsRow playbackControlsRow) { mPlaybackControlsRow = playbackControlsRow; } @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) { Drawable bitmapDrawable = new BitmapDrawable(sActivity.getResources(), bitmap); mPlaybackControlsRow.setImageDrawable(bitmapDrawable); } @Override public void onBitmapFailed(Drawable drawable) { mPlaybackControlsRow.setImageDrawable(drawable); } @Override public void onPrepareLoad(Drawable drawable) { // Do nothing, default_background mManager has its own transitions } } protected void updateVideoImage(URI uri) { Picasso.with(sActivity) .load(uri.toString()) .resize(convertDpToPixel(sActivity, CARD_WIDTH), convertDpToPixel(sActivity, CARD_HEIGHT)) .into(mPlaybackControlsRowTarget); } private static int convertDpToPixel(Context ctx, int dp) { float density = ctx.getResources().getDisplayMetrics().density; return Math.round((float) dp * density); } private class OnActionListener implements OnActionClickedListener { public void onActionClicked(Action action) { if (action.getId() == mPlayPauseAction.getId()) { if (mPlayPauseAction.getIndex() == PlayPauseAction.PLAY) { startProgressAutomation(); setFadingEnabled(true); mCallback.onFragmentPlayPause(mVideo, mPlaybackControlsRow.getCurrentTime(), true); } else { stopProgressAutomation(); setFadingEnabled(false); mCallback.onFragmentPlayPause(mVideo, mPlaybackControlsRow.getCurrentTime(), false); } } else if (action.getId() == mSkipNextAction.getId()) { next(); } else if (action.getId() == mSkipPreviousAction.getId()) { prev(); } else if (action.getId() == mFastForwardAction.getId()) { fastForward(); } else if (action.getId() == mRewindAction.getId()) { fastRewind(); } if (action instanceof PlaybackControlsRow.MultiAction) { ((PlaybackControlsRow.MultiAction) action).nextIndex(); notifyChanged(action); } } } private void updateRapidFfRwClickTracker() { if (null != mClickTrackingTimer) { mClickCount++; mClickTrackingTimer.cancel(); } else { mClickCount = 0; } Log.i(TAG, "RW/FF click count=" + mClickCount + ", speed=" + getFfRwSpeed()); mClickTrackingTimer = new Timer(); mClickTrackingTimer.schedule(new RapidFfRwClickTrackerTask(), CLICK_TRACKING_DELAY); } private class RapidFfRwClickTrackerTask extends TimerTask { @Override public void run() { mClickTrackingHandler.post(new Runnable() { @Override public void run() { mClickCount = 0; mClickTrackingTimer = null; } }); } } private int getFfRwSpeed() { // This works well for short videos (< 5 minutes). // Longer content should probably increase the speed more rapidly. return 10000 + mClickCount * 5000; } }