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