2 * Copyright 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.moiseum.wolnelektury.view.player.service;
19 import android.content.Context;
20 import android.content.res.AssetFileDescriptor;
21 import android.media.MediaPlayer;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.os.SystemClock;
25 import android.support.v4.media.MediaMetadataCompat;
26 import android.support.v4.media.session.PlaybackStateCompat;
27 import android.util.Log;
29 import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils;
32 * Exposes the functionality of the {@link MediaPlayer} and implements the {@link PlayerAdapter}
33 * so that MainActivity can control music playback.
35 public final class MediaPlayerAdapter extends PlayerAdapter {
37 private class MediaPlayerException extends Exception {
39 private final int what;
40 private final int extra;
42 public MediaPlayerException(int what, int extra) {
48 public String getMessage() {
49 return "Media player failed with code (" + what + "," + extra + ")";
53 private static final String TAG = MediaPlayerAdapter.class.getSimpleName();
54 private static final int FIVE_SECONDS = 5000;
55 private static final int CALLBACK_INTERVAL = 400;
57 private final Context mContext;
58 private MediaPlayer mMediaPlayer;
59 private String mFilename;
60 private PlaybackInfoListener mPlaybackInfoListener;
61 private MediaMetadataCompat mCurrentMedia;
63 private boolean mCurrentMediaPlayedToCompletion;
65 private Handler handler = new Handler();
66 private Runnable positionUpdateRunnable = new Runnable() {
69 mPlaybackInfoListener.onPlaybackProgress(mMediaPlayer.getCurrentPosition());
70 handler.postDelayed(this, CALLBACK_INTERVAL);
74 // Work-around for a MediaPlayer bug related to the behavior of MediaPlayer.seekTo()
76 private int mSeekWhileNotPlaying = -1;
78 MediaPlayerAdapter(Context context, PlaybackInfoListener listener) {
80 mContext = context.getApplicationContext();
81 mPlaybackInfoListener = listener;
85 * Once the {@link MediaPlayer} is released, it can't be used again, and another one has to be
86 * created. In the onStop() method of the MainActivity the {@link MediaPlayer} is
87 * released. Then in the onStart() of the MainActivity a new {@link MediaPlayer}
88 * object has to be created. That's why this method is private, and called by load(int) and
89 * not the constructor.
91 private void initializeMediaPlayer() {
92 if (mMediaPlayer == null) {
93 mMediaPlayer = new MediaPlayer();
94 mMediaPlayer.setOnCompletionListener(mediaPlayer -> {
95 mPlaybackInfoListener.onPlaybackCompleted();
97 // Set the state to "paused" because it most closely matches the state
98 // in MediaPlayer with regards to available state transitions compared
100 // Paused allows: seekTo(), start(), pause(), stop()
101 // Stop allows: stop()
102 setNewState(PlaybackStateCompat.STATE_PAUSED);
104 mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
105 Log.e(TAG, "Media player experienced failure: " + what + ", " + extra);
106 setNewState(PlaybackStateCompat.STATE_ERROR);
112 // Implements PlaybackControl.
114 public void playFromMedia(MediaMetadataCompat metadata) {
115 mCurrentMedia = metadata;
116 final String mediaId = metadata.getDescription().getMediaId();
117 playFile(AudiobookLibrary.getMusicFilename(mediaId));
121 public MediaMetadataCompat getCurrentMedia() {
122 return mCurrentMedia;
125 private void playFile(String filename) {
126 boolean mediaChanged = (mFilename == null || !filename.equals(mFilename));
127 if (mCurrentMediaPlayedToCompletion) {
128 // Last audio file was played to completion, the resourceId hasn't changed, but the
129 // player was released, so force a reload of the media file for playback.
131 mCurrentMediaPlayedToCompletion = false;
142 mFilename = filename;
144 initializeMediaPlayer();
147 String cachedFileForUrl = FileCacheUtils.getCachedFileForUrl(mFilename);
148 mMediaPlayer.setDataSource(cachedFileForUrl);
149 mMediaPlayer.prepare();
150 mPlaybackInfoListener.onPlaybackPrepared(mMediaPlayer.getDuration());
151 } catch (Exception e) {
152 Log.e(TAG, "Failed to load file: " + mFilename, e);
153 setNewState(PlaybackStateCompat.STATE_ERROR);
160 public void onStop() {
161 // Regardless of whether or not the MediaPlayer has been created / started, the state must
162 // be updated, so that MediaNotificationManager can take down the notification.
163 setNewState(PlaybackStateCompat.STATE_STOPPED);
167 private void release() {
168 if (mMediaPlayer != null) {
169 handler.removeCallbacks(positionUpdateRunnable);
170 mMediaPlayer.release();
176 public boolean isPlaying() {
177 return mMediaPlayer != null && mMediaPlayer.isPlaying();
181 protected void onPlay() {
182 if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
183 mMediaPlayer.start();
184 handler.postDelayed(positionUpdateRunnable, CALLBACK_INTERVAL);
185 setNewState(PlaybackStateCompat.STATE_PLAYING);
190 protected void onPause() {
191 if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
192 mMediaPlayer.pause();
193 handler.removeCallbacks(positionUpdateRunnable);
194 setNewState(PlaybackStateCompat.STATE_PAUSED);
198 // This is the main reducer for the player state machine.
199 private void setNewState(@PlaybackStateCompat.State int newPlayerState) {
200 mState = newPlayerState;
201 if (mState == PlaybackStateCompat.STATE_ERROR) {
202 handler.removeCallbacks(positionUpdateRunnable);
206 // Whether playback goes to completion, or whether it is stopped, the
207 // mCurrentMediaPlayedToCompletion is set to true.
208 if (mState == PlaybackStateCompat.STATE_STOPPED) {
209 mCurrentMediaPlayedToCompletion = true;
212 // Work around for MediaPlayer.getCurrentPosition() when it changes while not playing.
213 final long reportPosition;
214 if (mSeekWhileNotPlaying >= 0) {
215 reportPosition = mSeekWhileNotPlaying;
217 if (mState == PlaybackStateCompat.STATE_PLAYING) {
218 mSeekWhileNotPlaying = -1;
221 reportPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
224 final PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder();
225 stateBuilder.setActions(getAvailableActions());
226 stateBuilder.setState(mState,
229 SystemClock.elapsedRealtime());
230 if (mState == PlaybackStateCompat.STATE_PLAYING) {
231 Bundle extras = new Bundle();
232 extras.putInt(AudiobookService.EXTRA_PLAYBACK_TOTAL, mMediaPlayer.getDuration());
233 stateBuilder.setExtras(extras);
235 mPlaybackInfoListener.onPlaybackStateChange(stateBuilder.build());
239 * Set the current capabilities available on this session. Note: If a capability is not
240 * listed in the bitmask of capabilities then the MediaSession will not handle it. For
241 * example, if you don't want ACTION_STOP to be handled by the MediaSession, then don't
242 * included it in the bitmask that's returned.
244 @PlaybackStateCompat.Actions
245 private long getAvailableActions() {
246 long actions = PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
247 | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
248 | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
249 | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
251 case PlaybackStateCompat.STATE_STOPPED:
252 actions |= PlaybackStateCompat.ACTION_PLAY
253 | PlaybackStateCompat.ACTION_PAUSE;
255 case PlaybackStateCompat.STATE_PLAYING:
256 actions |= PlaybackStateCompat.ACTION_STOP
257 | PlaybackStateCompat.ACTION_PAUSE
258 | PlaybackStateCompat.ACTION_SEEK_TO;
260 case PlaybackStateCompat.STATE_PAUSED:
261 actions |= PlaybackStateCompat.ACTION_PLAY
262 | PlaybackStateCompat.ACTION_STOP;
265 actions |= PlaybackStateCompat.ACTION_PLAY
266 | PlaybackStateCompat.ACTION_PLAY_PAUSE
267 | PlaybackStateCompat.ACTION_STOP
268 | PlaybackStateCompat.ACTION_PAUSE;
274 public void seekTo(long position) {
275 if (mMediaPlayer != null) {
276 if (!mMediaPlayer.isPlaying()) {
277 mSeekWhileNotPlaying = (int) position;
279 mMediaPlayer.seekTo((int) position);
281 // Set the state (to the current state) because the position changed and should
282 // be reported to clients.
288 public void fastForward() {
289 if (mMediaPlayer != null) {
290 int seekTo = mMediaPlayer.getCurrentPosition() + FIVE_SECONDS;
291 int newState = mState;
293 if (seekTo > mMediaPlayer.getDuration()) {
294 seekTo = mMediaPlayer.getDuration();
295 newState = PlaybackStateCompat.STATE_PAUSED;
296 mMediaPlayer.pause();
299 mMediaPlayer.seekTo(seekTo);
300 setNewState(newState);
305 public void rewind() {
306 if (mMediaPlayer != null) {
307 int seekTo = mMediaPlayer.getCurrentPosition() - FIVE_SECONDS;
308 int newState = mState;
312 newState = PlaybackStateCompat.STATE_PAUSED;
313 mMediaPlayer.pause();
316 mMediaPlayer.seekTo(seekTo);
317 setNewState(newState);
322 public void setVolume(float volume) {
323 if (mMediaPlayer != null) {
324 mMediaPlayer.setVolume(volume, volume);