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
16package android.support.v17.leanback.supportleanbackshowcase.app.media;
17
18import android.content.Context;
19import android.graphics.Color;
20import android.graphics.drawable.Drawable;
21import android.media.AudioManager;
22import android.media.MediaPlayer;
23import android.net.Uri;
24import android.os.Handler;
25import android.support.v17.leanback.app.PlaybackControlGlue;
26import android.support.v17.leanback.app.PlaybackOverlayFragment;
27import android.support.v17.leanback.supportleanbackshowcase.R;
28import android.support.v17.leanback.widget.Action;
29import android.support.v17.leanback.widget.ArrayObjectAdapter;
30import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
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.Row;
36import android.support.v17.leanback.widget.RowPresenter;
37import android.util.Log;
38import android.view.KeyEvent;
39import android.view.SurfaceHolder;
40import android.view.View;
41
42import 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 */
55public 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
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     */
99    void reset() {
100        mInitialized = false;
101        mPlayer.reset();
102    }
103
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     */
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     */
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     */
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
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
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
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
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
223    @Override public boolean hasValidMedia() {
224        return mMetaData != null;
225    }
226
227    @Override public boolean isMediaPlaying() {
228        return mPlayer.isPlaying();
229    }
230
231    @Override public CharSequence getMediaTitle() {
232        return hasValidMedia() ? mMetaData.getTitle() : "N/a";
233    }
234
235    @Override public CharSequence getMediaSubtitle() {
236        return hasValidMedia() ? mMetaData.getArtist() : "N/a";
237    }
238
239    @Override public int getMediaDuration() {
240        return mInitialized ? mPlayer.getDuration() : 0;
241    }
242
243    @Override public Drawable getMediaArt() {
244        return hasValidMedia() ? mMetaData.getCover() : null;
245    }
246
247    @Override public long getSupportedActions() {
248        return PlaybackControlGlue.ACTION_PLAY_PAUSE | PlaybackControlGlue.ACTION_FAST_FORWARD | PlaybackControlGlue.ACTION_REWIND;
249    }
250
251    @Override public int getCurrentSpeedId() {
252        // 0 = Pause, 1 = Normal Playback Speed
253        return mPlayer.isPlaying() ? 1 : 0;
254    }
255
256    @Override public int getCurrentPosition() {
257        return mInitialized ? mPlayer.getCurrentPosition() : 0;
258    }
259
260    @Override protected void startPlayback(int speed) throws IllegalStateException {
261        mPlayer.start();
262    }
263
264    @Override protected void pausePlayback() {
265        if (mPlayer.isPlaying()) {
266            mPlayer.pause();
267        }
268    }
269
270    @Override protected void skipToNext() {
271        // Not supported.
272    }
273
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
376    public boolean repeatAll() {
377        return mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.ALL;
378    }
379
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     */
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         */
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
430        public String getTitle() {
431            return mTitle;
432        }
433
434        public void setTitle(String title) {
435            this.mTitle = title;
436        }
437
438        public String getArtist() {
439            return mArtist;
440        }
441
442        public void setArtist(String artist) {
443            this.mArtist = artist;
444        }
445
446        public Drawable getCover() {
447            return mCover;
448        }
449
450        public void setCover(Drawable cover) {
451            this.mCover = cover;
452        }
453
454    }
455
456}
457