Home | History | Annotate | Download | only in media
      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 
     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