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 
17 package androidx.leanback.media;
18 
19 import android.content.Context;
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.view.KeyEvent;
26 import android.view.SurfaceHolder;
27 import android.view.View;
28 
29 import androidx.annotation.RestrictTo;
30 import androidx.leanback.widget.Action;
31 import androidx.leanback.widget.ArrayObjectAdapter;
32 import androidx.leanback.widget.OnItemViewSelectedListener;
33 import androidx.leanback.widget.PlaybackControlsRow;
34 import androidx.leanback.widget.Presenter;
35 import androidx.leanback.widget.Row;
36 import androidx.leanback.widget.RowPresenter;
37 
38 import java.io.IOException;
39 import 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
60 public 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      */
setCover(Drawable cover)89     public void setCover(Drawable cover) {
90         this.mCover = cover;
91     }
92 
93     /**
94      * Sets the artist name.
95      */
setArtist(String artist)96     public void setArtist(String artist) {
97         this.mArtist = artist;
98     }
99 
100     /**
101      * Sets the media title.
102      */
setTitle(String title)103     public void setTitle(String title) {
104         this.mTitle = title;
105     }
106 
107     /**
108      * Sets the url for the video.
109      */
setVideoUrl(String videoUrl)110     public void setVideoUrl(String videoUrl) {
111         setMediaSource(videoUrl);
112         onMetadataChanged();
113     }
114 
115     /**
116      * Constructor.
117      */
MediaPlayerGlue(Context context)118     public MediaPlayerGlue(Context context) {
119         this(context, new int[]{1}, new int[]{1});
120     }
121 
122     /**
123      * Constructor.
124      */
MediaPlayerGlue( Context context, int[] fastForwardSpeeds, int[] rewindSpeeds)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
onAttachedToHost(PlaybackGlueHost host)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      */
reset()151     public void reset() {
152         changeToUnitialized();
153         mPlayer.reset();
154     }
155 
changeToUnitialized()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      */
release()171     public void release() {
172         changeToUnitialized();
173         mPlayer.release();
174     }
175 
176     @Override
onDetachedFromHost()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
onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter)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      */
setDisplay(SurfaceHolder surfaceHolder)196     public void setDisplay(SurfaceHolder surfaceHolder) {
197         mPlayer.setDisplay(surfaceHolder);
198     }
199 
200     @Override
enableProgressUpdating(final boolean enabled)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
onActionClicked(Action action)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
onKey(View v, int keyCode, KeyEvent event)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
hasValidMedia()274     public boolean hasValidMedia() {
275         return mTitle != null && (mMediaSourcePath != null || mMediaSourceUri != null);
276     }
277 
278     @Override
isMediaPlaying()279     public boolean isMediaPlaying() {
280         return mInitialized && mPlayer.isPlaying();
281     }
282 
283     @Override
isPlaying()284     public boolean isPlaying() {
285         return isMediaPlaying();
286     }
287 
288     @Override
getMediaTitle()289     public CharSequence getMediaTitle() {
290         return mTitle != null ? mTitle : "N/a";
291     }
292 
293     @Override
getMediaSubtitle()294     public CharSequence getMediaSubtitle() {
295         return mArtist != null ? mArtist : "N/a";
296     }
297 
298     @Override
getMediaDuration()299     public int getMediaDuration() {
300         return mInitialized ? mPlayer.getDuration() : 0;
301     }
302 
303     @Override
getMediaArt()304     public Drawable getMediaArt() {
305         return mCover;
306     }
307 
308     @Override
getSupportedActions()309     public long getSupportedActions() {
310         return PlaybackControlGlue.ACTION_PLAY_PAUSE
311                 | PlaybackControlGlue.ACTION_FAST_FORWARD
312                 | PlaybackControlGlue.ACTION_REWIND;
313     }
314 
315     @Override
getCurrentSpeedId()316     public int getCurrentSpeedId() {
317         // 0 = Pause, 1 = Normal Playback Speed
318         return isMediaPlaying() ? 1 : 0;
319     }
320 
321     @Override
getCurrentPosition()322     public int getCurrentPosition() {
323         return mInitialized ? mPlayer.getCurrentPosition() : 0;
324     }
325 
326     @Override
play(int speed)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
pause()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      */
setMode(int mode)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      */
seekTo(int newPosition)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      */
setMediaSource(Uri uri)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      */
setMediaSource(String path)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 
prepareMediaForPlaying()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
onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row)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
isPrepared()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
surfaceCreated(SurfaceHolder surfaceHolder)504         public void surfaceCreated(SurfaceHolder surfaceHolder) {
505             setDisplay(surfaceHolder);
506         }
507 
508         @Override
surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2)509         public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
510         }
511 
512         @Override
surfaceDestroyed(SurfaceHolder surfaceHolder)513         public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
514             setDisplay(null);
515         }
516     }
517 }
518