Added Android code
[wl-app.git] / Android / app / src / main / java / com / moiseum / wolnelektury / view / player / service / MediaPlayerAdapter.java
1 /*
2  * Copyright 2017 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 com.moiseum.wolnelektury.view.player.service;
18
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;
28
29 import com.moiseum.wolnelektury.connection.downloads.FileCacheUtils;
30
31 /**
32  * Exposes the functionality of the {@link MediaPlayer} and implements the {@link PlayerAdapter}
33  * so that MainActivity can control music playback.
34  */
35 public final class MediaPlayerAdapter extends PlayerAdapter {
36
37         private class MediaPlayerException extends Exception {
38
39                 private final int what;
40                 private final int extra;
41
42                 public MediaPlayerException(int what, int extra) {
43                         this.what = what;
44                         this.extra = extra;
45                 }
46
47                 @Override
48                 public String getMessage() {
49                         return "Media player failed with code (" + what + "," + extra + ")";
50                 }
51         }
52
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;
56
57     private final Context mContext;
58     private MediaPlayer mMediaPlayer;
59     private String mFilename;
60     private PlaybackInfoListener mPlaybackInfoListener;
61     private MediaMetadataCompat mCurrentMedia;
62     private int mState;
63     private boolean mCurrentMediaPlayedToCompletion;
64
65         private Handler handler = new Handler();
66         private Runnable positionUpdateRunnable = new Runnable() {
67                 @Override
68                 public void run() {
69                         mPlaybackInfoListener.onPlaybackProgress(mMediaPlayer.getCurrentPosition());
70                         handler.postDelayed(this, CALLBACK_INTERVAL);
71                 }
72         };
73
74     // Work-around for a MediaPlayer bug related to the behavior of MediaPlayer.seekTo()
75     // while not playing.
76     private int mSeekWhileNotPlaying = -1;
77
78     MediaPlayerAdapter(Context context, PlaybackInfoListener listener) {
79         super(context);
80         mContext = context.getApplicationContext();
81         mPlaybackInfoListener = listener;
82     }
83
84     /**
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.
90      */
91     private void initializeMediaPlayer() {
92         if (mMediaPlayer == null) {
93             mMediaPlayer = new MediaPlayer();
94             mMediaPlayer.setOnCompletionListener(mediaPlayer -> {
95                 mPlaybackInfoListener.onPlaybackCompleted();
96
97                 // Set the state to "paused" because it most closely matches the state
98                 // in MediaPlayer with regards to available state transitions compared
99                 // to "stop".
100                 // Paused allows: seekTo(), start(), pause(), stop()
101                 // Stop allows: stop()
102                 setNewState(PlaybackStateCompat.STATE_PAUSED);
103             });
104             mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
105                     Log.e(TAG, "Media player experienced failure: " + what + ", " + extra);
106                 setNewState(PlaybackStateCompat.STATE_ERROR);
107                     return false;
108             });
109         }
110     }
111
112     // Implements PlaybackControl.
113     @Override
114     public void playFromMedia(MediaMetadataCompat metadata) {
115         mCurrentMedia = metadata;
116         final String mediaId = metadata.getDescription().getMediaId();
117         playFile(AudiobookLibrary.getMusicFilename(mediaId));
118     }
119
120     @Override
121     public MediaMetadataCompat getCurrentMedia() {
122         return mCurrentMedia;
123     }
124
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.
130             mediaChanged = true;
131             mCurrentMediaPlayedToCompletion = false;
132         }
133         if (!mediaChanged) {
134             if (!isPlaying()) {
135                 play();
136             }
137             return;
138         } else {
139             release();
140         }
141
142         mFilename = filename;
143
144         initializeMediaPlayer();
145
146         try {
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);
154         }
155
156         play();
157     }
158
159     @Override
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);
164         release();
165     }
166
167     private void release() {
168         if (mMediaPlayer != null) {
169                 handler.removeCallbacks(positionUpdateRunnable);
170             mMediaPlayer.release();
171             mMediaPlayer = null;
172         }
173     }
174
175     @Override
176     public boolean isPlaying() {
177         return mMediaPlayer != null && mMediaPlayer.isPlaying();
178     }
179
180     @Override
181     protected void onPlay() {
182         if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
183             mMediaPlayer.start();
184                 handler.postDelayed(positionUpdateRunnable, CALLBACK_INTERVAL);
185             setNewState(PlaybackStateCompat.STATE_PLAYING);
186         }
187     }
188
189     @Override
190     protected void onPause() {
191         if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
192             mMediaPlayer.pause();
193                 handler.removeCallbacks(positionUpdateRunnable);
194             setNewState(PlaybackStateCompat.STATE_PAUSED);
195         }
196     }
197
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);
203                 mFilename = null;
204         }
205
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;
210         }
211
212         // Work around for MediaPlayer.getCurrentPosition() when it changes while not playing.
213         final long reportPosition;
214         if (mSeekWhileNotPlaying >= 0) {
215             reportPosition = mSeekWhileNotPlaying;
216
217             if (mState == PlaybackStateCompat.STATE_PLAYING) {
218                 mSeekWhileNotPlaying = -1;
219             }
220         } else {
221             reportPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
222         }
223
224         final PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder();
225         stateBuilder.setActions(getAvailableActions());
226         stateBuilder.setState(mState,
227                               reportPosition,
228                               1.0f,
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);
234         }
235         mPlaybackInfoListener.onPlaybackStateChange(stateBuilder.build());
236     }
237
238     /**
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.
243      */
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;
250         switch (mState) {
251             case PlaybackStateCompat.STATE_STOPPED:
252                 actions |= PlaybackStateCompat.ACTION_PLAY
253                            | PlaybackStateCompat.ACTION_PAUSE;
254                 break;
255             case PlaybackStateCompat.STATE_PLAYING:
256                 actions |= PlaybackStateCompat.ACTION_STOP
257                            | PlaybackStateCompat.ACTION_PAUSE
258                            | PlaybackStateCompat.ACTION_SEEK_TO;
259                 break;
260             case PlaybackStateCompat.STATE_PAUSED:
261                 actions |= PlaybackStateCompat.ACTION_PLAY
262                            | PlaybackStateCompat.ACTION_STOP;
263                 break;
264             default:
265                 actions |= PlaybackStateCompat.ACTION_PLAY
266                            | PlaybackStateCompat.ACTION_PLAY_PAUSE
267                            | PlaybackStateCompat.ACTION_STOP
268                            | PlaybackStateCompat.ACTION_PAUSE;
269         }
270         return actions;
271     }
272
273     @Override
274     public void seekTo(long position) {
275         if (mMediaPlayer != null) {
276             if (!mMediaPlayer.isPlaying()) {
277                 mSeekWhileNotPlaying = (int) position;
278             }
279             mMediaPlayer.seekTo((int) position);
280
281             // Set the state (to the current state) because the position changed and should
282             // be reported to clients.
283             setNewState(mState);
284         }
285     }
286
287     @Override
288     public void fastForward() {
289             if (mMediaPlayer != null) {
290                 int seekTo = mMediaPlayer.getCurrentPosition() + FIVE_SECONDS;
291                 int newState = mState;
292
293                 if (seekTo > mMediaPlayer.getDuration()) {
294                         seekTo = mMediaPlayer.getDuration();
295                         newState = PlaybackStateCompat.STATE_PAUSED;
296                         mMediaPlayer.pause();
297                     }
298
299                     mMediaPlayer.seekTo(seekTo);
300                     setNewState(newState);
301             }
302     }
303
304     @Override
305     public void rewind() {
306             if (mMediaPlayer != null) {
307                     int seekTo = mMediaPlayer.getCurrentPosition() - FIVE_SECONDS;
308                     int newState = mState;
309
310                     if (seekTo < 0) {
311                             seekTo = 0;
312                             newState = PlaybackStateCompat.STATE_PAUSED;
313                             mMediaPlayer.pause();
314                     }
315
316                     mMediaPlayer.seekTo(seekTo);
317                     setNewState(newState);
318             }
319     }
320
321     @Override
322     public void setVolume(float volume) {
323         if (mMediaPlayer != null) {
324             mMediaPlayer.setVolume(volume, volume);
325         }
326     }
327 }