1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.leanback.media;
18
19import android.content.Context;
20import android.graphics.drawable.Drawable;
21import android.media.AudioManager;
22import android.media.MediaPlayer;
23import android.net.Uri;
24import android.os.Handler;
25import android.view.KeyEvent;
26import android.view.SurfaceHolder;
27import android.view.View;
28
29import androidx.annotation.RestrictTo;
30import androidx.leanback.widget.Action;
31import androidx.leanback.widget.ArrayObjectAdapter;
32import androidx.leanback.widget.OnItemViewSelectedListener;
33import androidx.leanback.widget.PlaybackControlsRow;
34import androidx.leanback.widget.Presenter;
35import androidx.leanback.widget.Row;
36import androidx.leanback.widget.RowPresenter;
37
38import java.io.IOException;
39import java.util.List;
40
41/**
42 * This glue extends the {@link androidx.leanback.media.PlaybackControlGlue} with a
43 * {@link MediaPlayer} synchronization. It supports 7 actions:
44 *
45 * <ul>
46 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.FastForwardAction}</li>
47 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.RewindAction}</li>
48 * <li>{@link  androidx.leanback.widget.PlaybackControlsRow.PlayPauseAction}</li>
49 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.RepeatAction}</li>
50 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.ThumbsDownAction}</li>
51 * <li>{@link androidx.leanback.widget.PlaybackControlsRow.ThumbsUpAction}</li>
52 * </ul>
53 *
54 * @hide
55 * @deprecated Use {@link MediaPlayerAdapter} with {@link PlaybackTransportControlGlue} or
56 *             {@link PlaybackBannerControlGlue}.
57 */
58@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
59@Deprecated
60public class MediaPlayerGlue extends PlaybackControlGlue implements
61        OnItemViewSelectedListener {
62
63    public static final int NO_REPEAT = 0;
64    public static final int REPEAT_ONE = 1;
65    public static final int REPEAT_ALL = 2;
66
67    public static final int FAST_FORWARD_REWIND_STEP = 10 * 1000; // in milliseconds
68    public static final int FAST_FORWARD_REWIND_REPEAT_DELAY = 200; // in milliseconds
69    private static final String TAG = "MediaPlayerGlue";
70    protected final PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
71    protected final PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
72    MediaPlayer mPlayer = new MediaPlayer();
73    private final PlaybackControlsRow.RepeatAction mRepeatAction;
74    private Runnable mRunnable;
75    private Handler mHandler = new Handler();
76    private boolean mInitialized = false; // true when the MediaPlayer is prepared/initialized
77    private Action mSelectedAction; // the action which is currently selected by the user
78    private long mLastKeyDownEvent = 0L; // timestamp when the last DPAD_CENTER KEY_DOWN occurred
79    private Uri mMediaSourceUri = null;
80    private String mMediaSourcePath = null;
81    private MediaPlayer.OnCompletionListener mOnCompletionListener;
82    private String mArtist;
83    private String mTitle;
84    private Drawable mCover;
85
86    /**
87     * Sets the drawable representing cover image.
88     */
89    public void setCover(Drawable cover) {
90        this.mCover = cover;
91    }
92
93    /**
94     * Sets the artist name.
95     */
96    public void setArtist(String artist) {
97        this.mArtist = artist;
98    }
99
100    /**
101     * Sets the media title.
102     */
103    public void setTitle(String title) {
104        this.mTitle = title;
105    }
106
107    /**
108     * Sets the url for the video.
109     */
110    public void setVideoUrl(String videoUrl) {
111        setMediaSource(videoUrl);
112        onMetadataChanged();
113    }
114
115    /**
116     * Constructor.
117     */
118    public MediaPlayerGlue(Context context) {
119        this(context, new int[]{1}, new int[]{1});
120    }
121
122    /**
123     * Constructor.
124     */
125    public MediaPlayerGlue(
126            Context context, int[] fastForwardSpeeds, int[] rewindSpeeds) {
127        super(context, fastForwardSpeeds, rewindSpeeds);
128
129        // Instantiate secondary actions
130        mRepeatAction = new PlaybackControlsRow.RepeatAction(getContext());
131        mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(getContext());
132        mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(getContext());
133        mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
134        mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
135    }
136
137    @Override
138    protected void onAttachedToHost(PlaybackGlueHost host) {
139        super.onAttachedToHost(host);
140        if (host instanceof SurfaceHolderGlueHost) {
141            ((SurfaceHolderGlueHost) host).setSurfaceHolderCallback(
142                    new VideoPlayerSurfaceHolderCallback());
143        }
144    }
145
146    /**
147     * Will reset the {@link MediaPlayer} and the glue such that a new file can be played. You are
148     * not required to call this method before playing the first file. However you have to call it
149     * before playing a second one.
150     */
151    public void reset() {
152        changeToUnitialized();
153        mPlayer.reset();
154    }
155
156    void changeToUnitialized() {
157        if (mInitialized) {
158            mInitialized = false;
159            List<PlayerCallback> callbacks = getPlayerCallbacks();
160            if (callbacks != null) {
161                for (PlayerCallback callback: callbacks) {
162                    callback.onPreparedStateChanged(MediaPlayerGlue.this);
163                }
164            }
165        }
166    }
167
168    /**
169     * Release internal MediaPlayer. Should not use the object after call release().
170     */
171    public void release() {
172        changeToUnitialized();
173        mPlayer.release();
174    }
175
176    @Override
177    protected void onDetachedFromHost() {
178        if (getHost() instanceof SurfaceHolderGlueHost) {
179            ((SurfaceHolderGlueHost) getHost()).setSurfaceHolderCallback(null);
180        }
181        reset();
182        release();
183        super.onDetachedFromHost();
184    }
185
186    @Override
187    protected void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter) {
188        secondaryActionsAdapter.add(mRepeatAction);
189        secondaryActionsAdapter.add(mThumbsDownAction);
190        secondaryActionsAdapter.add(mThumbsUpAction);
191    }
192
193    /**
194     * @see MediaPlayer#setDisplay(SurfaceHolder)
195     */
196    public void setDisplay(SurfaceHolder surfaceHolder) {
197        mPlayer.setDisplay(surfaceHolder);
198    }
199
200    @Override
201    public void enableProgressUpdating(final boolean enabled) {
202        if (mRunnable != null) mHandler.removeCallbacks(mRunnable);
203        if (!enabled) {
204            return;
205        }
206        if (mRunnable == null) {
207            mRunnable = new Runnable() {
208                @Override
209                public void run() {
210                    updateProgress();
211                    mHandler.postDelayed(this, getUpdatePeriod());
212                }
213            };
214        }
215        mHandler.postDelayed(mRunnable, getUpdatePeriod());
216    }
217
218    @Override
219    public void onActionClicked(Action action) {
220        // If either 'Shuffle' or 'Repeat' has been clicked we need to make sure the actions index
221        // is incremented and the UI updated such that we can display the new state.
222        super.onActionClicked(action);
223        if (action instanceof PlaybackControlsRow.RepeatAction) {
224            ((PlaybackControlsRow.RepeatAction) action).nextIndex();
225        } else if (action == mThumbsUpAction) {
226            if (mThumbsUpAction.getIndex() == PlaybackControlsRow.ThumbsAction.INDEX_SOLID) {
227                mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
228            } else {
229                mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_SOLID);
230                mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
231            }
232        } else if (action == mThumbsDownAction) {
233            if (mThumbsDownAction.getIndex() == PlaybackControlsRow.ThumbsAction.INDEX_SOLID) {
234                mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
235            } else {
236                mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_SOLID);
237                mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsAction.INDEX_OUTLINE);
238            }
239        }
240        onMetadataChanged();
241    }
242
243    @Override
244    public boolean onKey(View v, int keyCode, KeyEvent event) {
245        // This method is overridden in order to make implement fast forwarding and rewinding when
246        // the user keeps the corresponding action pressed.
247        // We only consume DPAD_CENTER Action_DOWN events on the Fast-Forward and Rewind action and
248        // only if it has not been pressed in the last X milliseconds.
249        boolean consume = mSelectedAction instanceof PlaybackControlsRow.RewindAction;
250        consume = consume || mSelectedAction instanceof PlaybackControlsRow.FastForwardAction;
251        consume = consume && mInitialized;
252        consume = consume && event.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER;
253        consume = consume && event.getAction() == KeyEvent.ACTION_DOWN;
254        consume = consume && System
255                .currentTimeMillis() - mLastKeyDownEvent > FAST_FORWARD_REWIND_REPEAT_DELAY;
256
257        if (consume) {
258            mLastKeyDownEvent = System.currentTimeMillis();
259            int newPosition = getCurrentPosition() + FAST_FORWARD_REWIND_STEP;
260            if (mSelectedAction instanceof PlaybackControlsRow.RewindAction) {
261                newPosition = getCurrentPosition() - FAST_FORWARD_REWIND_STEP;
262            }
263            // Make sure the new calculated duration is in the range 0 >= X >= MediaDuration
264            if (newPosition < 0) newPosition = 0;
265            if (newPosition > getMediaDuration()) newPosition = getMediaDuration();
266            seekTo(newPosition);
267            return true;
268        }
269
270        return super.onKey(v, keyCode, event);
271    }
272
273    @Override
274    public boolean hasValidMedia() {
275        return mTitle != null && (mMediaSourcePath != null || mMediaSourceUri != null);
276    }
277
278    @Override
279    public boolean isMediaPlaying() {
280        return mInitialized && mPlayer.isPlaying();
281    }
282
283    @Override
284    public boolean isPlaying() {
285        return isMediaPlaying();
286    }
287
288    @Override
289    public CharSequence getMediaTitle() {
290        return mTitle != null ? mTitle : "N/a";
291    }
292
293    @Override
294    public CharSequence getMediaSubtitle() {
295        return mArtist != null ? mArtist : "N/a";
296    }
297
298    @Override
299    public int getMediaDuration() {
300        return mInitialized ? mPlayer.getDuration() : 0;
301    }
302
303    @Override
304    public Drawable getMediaArt() {
305        return mCover;
306    }
307
308    @Override
309    public long getSupportedActions() {
310        return PlaybackControlGlue.ACTION_PLAY_PAUSE
311                | PlaybackControlGlue.ACTION_FAST_FORWARD
312                | PlaybackControlGlue.ACTION_REWIND;
313    }
314
315    @Override
316    public int getCurrentSpeedId() {
317        // 0 = Pause, 1 = Normal Playback Speed
318        return isMediaPlaying() ? 1 : 0;
319    }
320
321    @Override
322    public int getCurrentPosition() {
323        return mInitialized ? mPlayer.getCurrentPosition() : 0;
324    }
325
326    @Override
327    public void play(int speed) {
328        if (!mInitialized || mPlayer.isPlaying()) {
329            return;
330        }
331        mPlayer.start();
332        onMetadataChanged();
333        onStateChanged();
334        updateProgress();
335    }
336
337    @Override
338    public void pause() {
339        if (isMediaPlaying()) {
340            mPlayer.pause();
341            onStateChanged();
342        }
343    }
344
345    /**
346     * Sets the playback mode. It currently support no repeat, repeat once and infinite
347     * loop mode.
348     */
349    public void setMode(int mode) {
350        switch(mode) {
351            case NO_REPEAT:
352                mOnCompletionListener = null;
353                break;
354            case REPEAT_ONE:
355                mOnCompletionListener = new MediaPlayer.OnCompletionListener() {
356                    public boolean mFirstRepeat;
357
358                    @Override
359                    public void onCompletion(MediaPlayer mediaPlayer) {
360                        if (!mFirstRepeat) {
361                            mFirstRepeat = true;
362                            mediaPlayer.setOnCompletionListener(null);
363                        }
364                        play();
365                    }
366                };
367                break;
368            case REPEAT_ALL:
369                mOnCompletionListener = new MediaPlayer.OnCompletionListener() {
370                    @Override
371                    public void onCompletion(MediaPlayer mediaPlayer) {
372                        play();
373                    }
374                };
375                break;
376        }
377    }
378
379    /**
380     * Called whenever the user presses fast-forward/rewind or when the user keeps the
381     * corresponding action pressed.
382     *
383     * @param newPosition The new position of the media track in milliseconds.
384     */
385    protected void seekTo(int newPosition) {
386        if (!mInitialized) {
387            return;
388        }
389        mPlayer.seekTo(newPosition);
390    }
391
392    /**
393     * Sets the media source of the player witha given URI.
394     *
395     * @return Returns <code>true</code> if uri represents a new media; <code>false</code>
396     * otherwise.
397     * @see MediaPlayer#setDataSource(String)
398     */
399    public boolean setMediaSource(Uri uri) {
400        if (mMediaSourceUri != null ? mMediaSourceUri.equals(uri) : uri == null) {
401            return false;
402        }
403        mMediaSourceUri = uri;
404        mMediaSourcePath = null;
405        prepareMediaForPlaying();
406        return true;
407    }
408
409    /**
410     * Sets the media source of the player with a String path URL.
411     *
412     * @return Returns <code>true</code> if path represents a new media; <code>false</code>
413     * otherwise.
414     * @see MediaPlayer#setDataSource(String)
415     */
416    public boolean setMediaSource(String path) {
417        if (mMediaSourcePath != null ? mMediaSourcePath.equals(path) : path == null) {
418            return false;
419        }
420        mMediaSourceUri = null;
421        mMediaSourcePath = path;
422        prepareMediaForPlaying();
423        return true;
424    }
425
426    private void prepareMediaForPlaying() {
427        reset();
428        try {
429            if (mMediaSourceUri != null) {
430                mPlayer.setDataSource(getContext(), mMediaSourceUri);
431            } else if (mMediaSourcePath != null) {
432                mPlayer.setDataSource(mMediaSourcePath);
433            } else {
434                return;
435            }
436        } catch (IOException e) {
437            e.printStackTrace();
438            throw new RuntimeException(e);
439        }
440        mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
441        mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
442            @Override
443            public void onPrepared(MediaPlayer mp) {
444                mInitialized = true;
445                List<PlayerCallback> callbacks = getPlayerCallbacks();
446                if (callbacks != null) {
447                    for (PlayerCallback callback: callbacks) {
448                        callback.onPreparedStateChanged(MediaPlayerGlue.this);
449                    }
450                }
451            }
452        });
453
454        if (mOnCompletionListener != null) {
455            mPlayer.setOnCompletionListener(mOnCompletionListener);
456        }
457
458        mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
459            @Override
460            public void onBufferingUpdate(MediaPlayer mp, int percent) {
461                if (getControlsRow() == null) {
462                    return;
463                }
464                getControlsRow().setBufferedProgress((int) (mp.getDuration() * (percent / 100f)));
465            }
466        });
467        mPlayer.prepareAsync();
468        onStateChanged();
469    }
470
471    /**
472     * This is a listener implementation for the {@link OnItemViewSelectedListener}.
473     * This implementation is required in order to detect KEY_DOWN events
474     * on the {@link androidx.leanback.widget.PlaybackControlsRow.FastForwardAction} and
475     * {@link androidx.leanback.widget.PlaybackControlsRow.RewindAction}. Thus you
476     * should <u>NOT</u> set another {@link OnItemViewSelectedListener} on your
477     * Fragment. Instead, override this method and call its super (this)
478     * implementation.
479     *
480     * @see OnItemViewSelectedListener#onItemSelected(
481     *Presenter.ViewHolder, Object, RowPresenter.ViewHolder, Object)
482     */
483    @Override
484    public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
485                               RowPresenter.ViewHolder rowViewHolder, Row row) {
486        if (item instanceof Action) {
487            mSelectedAction = (Action) item;
488        } else {
489            mSelectedAction = null;
490        }
491    }
492
493    @Override
494    public boolean isPrepared() {
495        return mInitialized;
496    }
497
498    /**
499     * Implements {@link SurfaceHolder.Callback} that can then be set on the
500     * {@link PlaybackGlueHost}.
501     */
502    class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback {
503        @Override
504        public void surfaceCreated(SurfaceHolder surfaceHolder) {
505            setDisplay(surfaceHolder);
506        }
507
508        @Override
509        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
510        }
511
512        @Override
513        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
514            setDisplay(null);
515        }
516    }
517}
518