1 /*
2  * Copyright (C) 2015 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  */
15 
16 package android.support.v17.leanback.supportleanbackshowcase.app.media;
17 
18 import android.content.Context;
19 import android.graphics.Color;
20 import android.graphics.drawable.Drawable;
21 import android.media.AudioManager;
22 import android.media.MediaPlayer;
23 import android.net.Uri;
24 import android.os.Handler;
25 import android.support.v17.leanback.app.PlaybackControlGlue;
26 import android.support.v17.leanback.app.PlaybackOverlayFragment;
27 import android.support.v17.leanback.supportleanbackshowcase.R;
28 import android.support.v17.leanback.widget.Action;
29 import android.support.v17.leanback.widget.ArrayObjectAdapter;
30 import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
31 import android.support.v17.leanback.widget.OnItemViewSelectedListener;
32 import android.support.v17.leanback.widget.PlaybackControlsRow;
33 import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
34 import android.support.v17.leanback.widget.Presenter;
35 import android.support.v17.leanback.widget.Row;
36 import android.support.v17.leanback.widget.RowPresenter;
37 import android.util.Log;
38 import android.view.KeyEvent;
39 import android.view.SurfaceHolder;
40 import android.view.View;
41 
42 import java.io.IOException;
43 
44 /**
45  * This glue extends the {@link PlaybackControlGlue} with a {@link MediaPlayer} synchronization. It
46  * supports 7 actions: <ul> <li>{@link android.support.v17.leanback.widget.PlaybackControlsRow.FastForwardAction}</li>
47  * <li>{@link android.support.v17.leanback.widget.PlaybackControlsRow.RewindAction}</li> <li>{@link
48  * android.support.v17.leanback.widget.PlaybackControlsRow.PlayPauseAction}</li> <li>{@link
49  * android.support.v17.leanback.widget.PlaybackControlsRow.ShuffleAction}</li> <li>{@link
50  * android.support.v17.leanback.widget.PlaybackControlsRow.RepeatAction}</li> <li>{@link
51  * android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsDownAction}</li> <li>{@link
52  * android.support.v17.leanback.widget.PlaybackControlsRow.ThumbsUpAction}</li> </ul>
53  * <p/>
54  */
55 public abstract class MediaPlayerGlue extends PlaybackControlGlue implements
56         OnItemViewSelectedListener {
57 
58     public static final int FAST_FORWARD_REWIND_STEP = 10 * 1000; // in milliseconds
59     public static final int FAST_FORWARD_REWIND_REPEAT_DELAY = 200; // in milliseconds
60     private static final String TAG = "MediaPlayerGlue";
61     protected final PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
62     protected final PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
63     private final Context mContext;
64     private final MediaPlayer mPlayer = new MediaPlayer();
65     private final PlaybackControlsRow.RepeatAction mRepeatAction;
66     private final PlaybackControlsRow.ShuffleAction mShuffleAction;
67     private PlaybackControlsRow mControlsRow;
68     private Runnable mRunnable;
69     private Handler mHandler = new Handler();
70     private boolean mInitialized = false; // true when the MediaPlayer is prepared/initialized
71     private OnMediaFileFinishedPlayingListener mMediaFileFinishedPlayingListener;
72     private Action mSelectedAction; // the action which is currently selected by the user
73     private long mLastKeyDownEvent = 0L; // timestamp when the last DPAD_CENTER KEY_DOWN occurred
74     private MetaData mMetaData;
75     private Uri mMediaSourceUri = null;
76     private String mMediaSourcePath = null;
77 
MediaPlayerGlue(Context context, PlaybackOverlayFragment fragment)78     public MediaPlayerGlue(Context context, PlaybackOverlayFragment fragment) {
79         super(context, fragment, new int[]{1});
80         mContext = context;
81 
82         // Instantiate secondary actions
83         mShuffleAction = new PlaybackControlsRow.ShuffleAction(mContext);
84         mRepeatAction = new PlaybackControlsRow.RepeatAction(mContext);
85         mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(mContext);
86         mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(mContext);
87         mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE);
88         mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE);
89 
90         // Register selected listener such that we know what action the user currently has focused.
91         fragment.setOnItemViewSelectedListener(this);
92     }
93 
94     /**
95      * Will reset the {@link MediaPlayer} and the glue such that a new file can be played. You are
96      * not required to call this method before playing the first file. However you have to call it
97      * before playing a second one.
98      */
reset()99     void reset() {
100         mInitialized = false;
101         mPlayer.reset();
102     }
103 
setOnMediaFileFinishedPlayingListener(OnMediaFileFinishedPlayingListener listener)104     public void setOnMediaFileFinishedPlayingListener(OnMediaFileFinishedPlayingListener listener) {
105         mMediaFileFinishedPlayingListener = listener;
106     }
107 
108     /**
109      * Override this method in case you need to add different secondary actions.
110      *
111      * @param secondaryActionsAdapter The adapter you need to add the {@link Action}s to.
112      */
addSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter)113     protected void addSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) {
114         secondaryActionsAdapter.add(mShuffleAction);
115         secondaryActionsAdapter.add(mRepeatAction);
116         secondaryActionsAdapter.add(mThumbsDownAction);
117         secondaryActionsAdapter.add(mThumbsUpAction);
118     }
119 
120     /**
121      * @see MediaPlayer#setDisplay(SurfaceHolder)
122      */
setDisplay(SurfaceHolder surfaceHolder)123     public void setDisplay(SurfaceHolder surfaceHolder) {
124         mPlayer.setDisplay(surfaceHolder);
125     }
126 
127     /**
128      * Use this method to setup the {@link PlaybackControlsRowPresenter}. It'll be called
129      * <u>after</u> the {@link PlaybackControlsRowPresenter} has been created and the primary and
130      * secondary actions have been added.
131      *
132      * @param presenter The PlaybackControlsRowPresenter used to display the controls.
133      */
setupControlsRowPresenter(PlaybackControlsRowPresenter presenter)134     public void setupControlsRowPresenter(PlaybackControlsRowPresenter presenter) {
135         // TODO: hahnr@ move into resources
136         presenter.setProgressColor(getContext().getResources().getColor(
137                 R.color.player_progress_color));
138         presenter.setBackgroundColor(getContext().getResources().getColor(
139                 R.color.player_background_color));
140     }
141 
createControlsRowAndPresenter()142     @Override public PlaybackControlsRowPresenter createControlsRowAndPresenter() {
143         PlaybackControlsRowPresenter presenter = super.createControlsRowAndPresenter();
144         mControlsRow = getControlsRow();
145 
146         // Add secondary actions and change the control row color.
147         ArrayObjectAdapter secondaryActions = new ArrayObjectAdapter(
148                 new ControlButtonPresenterSelector());
149         mControlsRow.setSecondaryActionsAdapter(secondaryActions);
150         addSecondaryActions(secondaryActions);
151         setupControlsRowPresenter(presenter);
152         return presenter;
153     }
154 
enableProgressUpdating(final boolean enabled)155     @Override public void enableProgressUpdating(final boolean enabled) {
156         if (!enabled) {
157             if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
158             return;
159         }
160         mRunnable = new Runnable() {
161             @Override public void run() {
162                 updateProgress();
163                 Log.d(TAG, "enableProgressUpdating(boolean)");
164                 mHandler.postDelayed(this, getUpdatePeriod());
165             }
166         };
167         mHandler.postDelayed(mRunnable, getUpdatePeriod());
168     }
169 
onActionClicked(Action action)170     @Override public void onActionClicked(Action action) {
171         // If either 'Shuffle' or 'Repeat' has been clicked we need to make sure the acitons index
172         // is incremented and the UI updated such that we can display the new state.
173         super.onActionClicked(action);
174         if (action instanceof PlaybackControlsRow.ShuffleAction) {
175             mShuffleAction.nextIndex();
176         } else if (action instanceof PlaybackControlsRow.RepeatAction) {
177             mRepeatAction.nextIndex();
178         } else if (action instanceof PlaybackControlsRow.ThumbsUpAction) {
179             if (mThumbsUpAction.getIndex() == PlaybackControlsRow.ThumbsAction.SOLID) {
180                 mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE);
181             } else {
182                 mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.SOLID);
183                 mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE);
184             }
185         } else if (action instanceof PlaybackControlsRow.ThumbsDownAction) {
186             if (mThumbsDownAction.getIndex() == PlaybackControlsRow.ThumbsAction.SOLID) {
187                 mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE);
188             } else {
189                 mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.SOLID);
190                 mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.OUTLINE);
191             }
192         }
193         onMetadataChanged();
194     }
195 
onKey(View v, int keyCode, KeyEvent event)196     @Override public boolean onKey(View v, int keyCode, KeyEvent event) {
197         // This method is overridden in order to make implement fast forwarding and rewinding when
198         // the user keeps the corresponding action pressed.
199         // We only consume DPAD_CENTER Action_DOWN events on the Fast-Forward and Rewind action and
200         // only if it has not been pressed in the last X milliseconds.
201         boolean consume = mSelectedAction instanceof PlaybackControlsRow.RewindAction;
202         consume = consume || mSelectedAction instanceof PlaybackControlsRow.FastForwardAction;
203         consume = consume && mInitialized;
204         consume = consume && event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER;
205         consume = consume && event.getAction() == KeyEvent.ACTION_DOWN;
206         consume = consume && System
207                 .currentTimeMillis() - mLastKeyDownEvent > FAST_FORWARD_REWIND_REPEAT_DELAY;
208         if (consume) {
209             mLastKeyDownEvent = System.currentTimeMillis();
210             int newPosition = getCurrentPosition() + FAST_FORWARD_REWIND_STEP;
211             if (mSelectedAction instanceof PlaybackControlsRow.RewindAction) {
212                 newPosition = getCurrentPosition() - FAST_FORWARD_REWIND_STEP;
213             }
214             // Make sure the new calculated duration is in the range 0 >= X >= MediaDuration
215             if (newPosition < 0) newPosition = 0;
216             if (newPosition > getMediaDuration()) newPosition = getMediaDuration();
217             seekTo(newPosition);
218             return true;
219         }
220         return super.onKey(v, keyCode, event);
221     }
222 
hasValidMedia()223     @Override public boolean hasValidMedia() {
224         return mMetaData != null;
225     }
226 
isMediaPlaying()227     @Override public boolean isMediaPlaying() {
228         return mPlayer.isPlaying();
229     }
230 
getMediaTitle()231     @Override public CharSequence getMediaTitle() {
232         return hasValidMedia() ? mMetaData.getTitle() : "N/a";
233     }
234 
getMediaSubtitle()235     @Override public CharSequence getMediaSubtitle() {
236         return hasValidMedia() ? mMetaData.getArtist() : "N/a";
237     }
238 
getMediaDuration()239     @Override public int getMediaDuration() {
240         return mInitialized ? mPlayer.getDuration() : 0;
241     }
242 
getMediaArt()243     @Override public Drawable getMediaArt() {
244         return hasValidMedia() ? mMetaData.getCover() : null;
245     }
246 
getSupportedActions()247     @Override public long getSupportedActions() {
248         return PlaybackControlGlue.ACTION_PLAY_PAUSE | PlaybackControlGlue.ACTION_FAST_FORWARD | PlaybackControlGlue.ACTION_REWIND;
249     }
250 
getCurrentSpeedId()251     @Override public int getCurrentSpeedId() {
252         // 0 = Pause, 1 = Normal Playback Speed
253         return mPlayer.isPlaying() ? 1 : 0;
254     }
255 
getCurrentPosition()256     @Override public int getCurrentPosition() {
257         return mInitialized ? mPlayer.getCurrentPosition() : 0;
258     }
259 
startPlayback(int speed)260     @Override protected void startPlayback(int speed) throws IllegalStateException {
261         mPlayer.start();
262     }
263 
pausePlayback()264     @Override protected void pausePlayback() {
265         if (mPlayer.isPlaying()) {
266             mPlayer.pause();
267         }
268     }
269 
skipToNext()270     @Override protected void skipToNext() {
271         // Not supported.
272     }
273 
skipToPrevious()274     @Override protected void skipToPrevious() {
275         // Not supported.
276     }
277 
278     /**
279      * Called whenever the user presses fast-forward/rewind or when the user keeps the corresponding
280      * action pressed.
281      *
282      * @param newPosition The new position of the media track in milliseconds.
283      */
seekTo(int newPosition)284     protected void seekTo(int newPosition) {
285         mPlayer.seekTo(newPosition);
286     }
287 
288     /**
289      * Sets the media source of the player witha given URI.
290      * @see MediaPlayer#setDataSource(String)
291      * @return Returns <code>true</code> if uri represents a new media; <code>false</code>
292      * otherwise.
293      */
setMediaSource(Uri uri)294     public boolean setMediaSource(Uri uri) {
295         if (mMediaSourceUri != null && mMediaSourceUri.equals(uri)) {
296             return false;
297         }
298         mMediaSourceUri = uri;
299         return true;
300     }
301 
302     /**
303      * Sets the media source of the player with a String path URL.
304      * @see MediaPlayer#setDataSource(String)
305      * @return Returns <code>true</code> if path represents a new media; <code>false</code>
306      * otherwise.
307      */
setMediaSource(String path)308     public boolean setMediaSource(String path) {
309         if (mMediaSourcePath != null && mMediaSourcePath.equals(mMediaSourcePath)) {
310             return false;
311         }
312         mMediaSourcePath = path;
313         return true;
314     }
315 
prepareMediaForPlaying()316     public void prepareMediaForPlaying() {
317         reset();
318         try {
319             if (mMediaSourceUri != null) mPlayer.setDataSource(getContext(), mMediaSourceUri);
320             else mPlayer.setDataSource(mMediaSourcePath);
321         } catch (IOException e) {
322             throw new RuntimeException(e);
323         }
324         mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
325         mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
326             @Override public void onPrepared(MediaPlayer mp) {
327                 mInitialized = true;
328                 mPlayer.start();
329                 onMetadataChanged();
330                 onStateChanged();
331                 updateProgress();
332             }
333         });
334         mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
335             @Override public void onCompletion(MediaPlayer mp) {
336                 if (mInitialized && mMediaFileFinishedPlayingListener != null)
337                     mMediaFileFinishedPlayingListener.onMediaFileFinishedPlaying(mMetaData);
338             }
339         });
340         mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
341             @Override public void onBufferingUpdate(MediaPlayer mp, int percent) {
342                 mControlsRow.setBufferedProgress((int) (mp.getDuration() * (percent / 100f)));
343             }
344         });
345         mPlayer.prepareAsync();
346         onStateChanged();
347     }
348 
349     /**
350      * Call to <code>startPlayback(1)</code>.
351      *
352      * @throws IllegalStateException See {@link MediaPlayer} for further information about it's
353      * different states when setting a data source and preparing it to be played.
354      */
startPlayback()355     public void startPlayback() throws IllegalStateException {
356         startPlayback(1);
357     }
358 
359     /**
360      * @return Returns <code>true</code> iff 'Shuffle' is <code>ON</code>.
361      */
useShuffle()362     public boolean useShuffle() {
363         return mShuffleAction.getIndex() == PlaybackControlsRow.ShuffleAction.ON;
364     }
365 
366     /**
367      * @return Returns <code>true</code> iff 'Repeat-One' is <code>ON</code>.
368      */
repeatOne()369     public boolean repeatOne() {
370         return mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.ONE;
371     }
372 
373     /**
374      * @return Returns <code>true</code> iff 'Repeat-All' is <code>ON</code>.
375      */
repeatAll()376     public boolean repeatAll() {
377         return mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.ALL;
378     }
379 
setMetaData(MetaData metaData)380     public void setMetaData(MetaData metaData) {
381         mMetaData = metaData;
382         onMetadataChanged();
383     }
384 
385     /**
386      * This is a listener implementation for the {@link OnItemViewSelectedListener} of the {@link
387      * PlaybackOverlayFragment}. This implementation is required in order to detect KEY_DOWN events
388      * on the {@link android.support.v17.leanback.widget.PlaybackControlsRow.FastForwardAction} and
389      * {@link android.support.v17.leanback.widget.PlaybackControlsRow.RewindAction}. Thus you should
390      * <u>NOT</u> set another {@link OnItemViewSelectedListener} on your {@link
391      * PlaybackOverlayFragment}. Instead, override this method and call its super (this)
392      * implementation.
393      *
394      * @see OnItemViewSelectedListener#onItemSelected(Presenter.ViewHolder, Object,
395      * RowPresenter.ViewHolder, Row)
396      */
onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row)397     @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
398                                          RowPresenter.ViewHolder rowViewHolder, Row row) {
399         if (item instanceof Action) {
400             mSelectedAction = (Action) item;
401         } else {
402             mSelectedAction = null;
403         }
404     }
405 
406     /**
407      * A listener which will be called whenever a track is finished playing.
408      */
409     public interface OnMediaFileFinishedPlayingListener {
410 
411         /**
412          * Called when a track is finished playing.
413          *
414          * @param metaData The track's {@link MetaData} which just finished playing.
415          */
onMediaFileFinishedPlaying(MetaData metaData)416         void onMediaFileFinishedPlaying(MetaData metaData);
417 
418     }
419 
420     /**
421      * Holds the meta data such as track title, artist and cover art. It'll be used by the {@link
422      * MediaPlayerGlue}.
423      */
424     public static class MetaData {
425 
426         private String mTitle;
427         private String mArtist;
428         private Drawable mCover;
429 
getTitle()430         public String getTitle() {
431             return mTitle;
432         }
433 
setTitle(String title)434         public void setTitle(String title) {
435             this.mTitle = title;
436         }
437 
getArtist()438         public String getArtist() {
439             return mArtist;
440         }
441 
setArtist(String artist)442         public void setArtist(String artist) {
443             this.mArtist = artist;
444         }
445 
getCover()446         public Drawable getCover() {
447             return mCover;
448         }
449 
setCover(Drawable cover)450         public void setCover(Drawable cover) {
451             this.mCover = cover;
452         }
453 
454     }
455 
456 }
457