1/*
2 * Copyright (C) 2015 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
17package com.example.android.supportv4.media;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.media.AudioManager;
24import android.media.MediaPlayer;
25import android.media.session.PlaybackState;
26import android.net.wifi.WifiManager;
27import android.os.PowerManager;
28import android.support.v4.media.MediaMetadataCompat;
29import android.text.TextUtils;
30import android.util.Log;
31
32import com.example.android.supportv4.media.model.MusicProvider;
33import com.example.android.supportv4.media.utils.MediaIDHelper;
34
35import java.io.IOException;
36
37import static android.media.MediaPlayer.OnCompletionListener;
38import static android.media.MediaPlayer.OnErrorListener;
39import static android.media.MediaPlayer.OnPreparedListener;
40import static android.media.MediaPlayer.OnSeekCompleteListener;
41import static android.support.v4.media.session.MediaSessionCompat.QueueItem;
42
43/**
44 * A class that implements local media playback using {@link android.media.MediaPlayer}
45 */
46public class Playback implements AudioManager.OnAudioFocusChangeListener,
47        OnCompletionListener, OnErrorListener, OnPreparedListener, OnSeekCompleteListener {
48
49    private static final String TAG = "Playback";
50
51    // The volume we set the media player to when we lose audio focus, but are
52    // allowed to reduce the volume instead of stopping playback.
53    public static final float VOLUME_DUCK = 0.2f;
54    // The volume we set the media player when we have audio focus.
55    public static final float VOLUME_NORMAL = 1.0f;
56
57    // we don't have audio focus, and can't duck (play at a low volume)
58    private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
59    // we don't have focus, but can duck (play at a low volume)
60    private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
61    // we have full audio focus
62    private static final int AUDIO_FOCUSED  = 2;
63
64    private final MediaBrowserServiceSupport mService;
65    private final WifiManager.WifiLock mWifiLock;
66    private int mState;
67    private boolean mPlayOnFocusGain;
68    private Callback mCallback;
69    private MusicProvider mMusicProvider;
70    private volatile boolean mAudioNoisyReceiverRegistered;
71    private volatile int mCurrentPosition;
72    private volatile String mCurrentMediaId;
73
74    // Type of audio focus we have:
75    private int mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
76    private AudioManager mAudioManager;
77    private MediaPlayer mMediaPlayer;
78
79    private IntentFilter mAudioNoisyIntentFilter =
80            new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
81
82    private BroadcastReceiver mAudioNoisyReceiver = new BroadcastReceiver() {
83        @Override
84        public void onReceive(Context context, Intent intent) {
85            if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
86                Log.d(TAG, "Headphones disconnected.");
87                if (isPlaying()) {
88                    Intent i = new Intent(context, MediaBrowserServiceSupport.class);
89                    i.setAction(MediaBrowserServiceSupport.ACTION_CMD);
90                    i.putExtra(MediaBrowserServiceSupport.CMD_NAME, MediaBrowserServiceSupport.CMD_PAUSE);
91                    mService.startService(i);
92                }
93            }
94        }
95    };
96
97    public Playback(MediaBrowserServiceSupport service, MusicProvider musicProvider) {
98        this.mService = service;
99        this.mMusicProvider = musicProvider;
100        this.mAudioManager = (AudioManager) service.getSystemService(Context.AUDIO_SERVICE);
101        // Create the Wifi lock (this does not acquire the lock, this just creates it)
102        this.mWifiLock = ((WifiManager) service.getSystemService(Context.WIFI_SERVICE))
103                .createWifiLock(WifiManager.WIFI_MODE_FULL, "sample_lock");
104    }
105
106    public void start() {
107    }
108
109    public void stop(boolean notifyListeners) {
110        mState = PlaybackState.STATE_STOPPED;
111        if (notifyListeners && mCallback != null) {
112            mCallback.onPlaybackStatusChanged(mState);
113        }
114        mCurrentPosition = getCurrentStreamPosition();
115        // Give up Audio focus
116        giveUpAudioFocus();
117        unregisterAudioNoisyReceiver();
118        // Relax all resources
119        relaxResources(true);
120        if (mWifiLock.isHeld()) {
121            mWifiLock.release();
122        }
123    }
124
125    public void setState(int state) {
126        this.mState = state;
127    }
128
129    public int getState() {
130        return mState;
131    }
132
133    public boolean isConnected() {
134        return true;
135    }
136
137    public boolean isPlaying() {
138        return mPlayOnFocusGain || (mMediaPlayer != null && mMediaPlayer.isPlaying());
139    }
140
141    public int getCurrentStreamPosition() {
142        return mMediaPlayer != null ?
143                mMediaPlayer.getCurrentPosition() : mCurrentPosition;
144    }
145
146    public void play(QueueItem item) {
147        mPlayOnFocusGain = true;
148        tryToGetAudioFocus();
149        registerAudioNoisyReceiver();
150        String mediaId = item.getDescription().getMediaId();
151        boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId);
152        if (mediaHasChanged) {
153            mCurrentPosition = 0;
154            mCurrentMediaId = mediaId;
155        }
156
157        if (mState == PlaybackState.STATE_PAUSED && !mediaHasChanged && mMediaPlayer != null) {
158            configMediaPlayerState();
159        } else {
160            mState = PlaybackState.STATE_STOPPED;
161            relaxResources(false); // release everything except MediaPlayer
162            MediaMetadataCompat track = mMusicProvider.getMusic(
163                    MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId()));
164
165            String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE);
166
167            try {
168                createMediaPlayerIfNeeded();
169
170                mState = PlaybackState.STATE_BUFFERING;
171
172                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
173                mMediaPlayer.setDataSource(source);
174
175                // Starts preparing the media player in the background. When
176                // it's done, it will call our OnPreparedListener (that is,
177                // the onPrepared() method on this class, since we set the
178                // listener to 'this'). Until the media player is prepared,
179                // we *cannot* call start() on it!
180                mMediaPlayer.prepareAsync();
181
182                // If we are streaming from the internet, we want to hold a
183                // Wifi lock, which prevents the Wifi radio from going to
184                // sleep while the song is playing.
185                mWifiLock.acquire();
186
187                if (mCallback != null) {
188                    mCallback.onPlaybackStatusChanged(mState);
189                }
190
191            } catch (IOException ex) {
192                Log.e(TAG, "Exception playing song", ex);
193                if (mCallback != null) {
194                    mCallback.onError(ex.getMessage());
195                }
196            }
197        }
198    }
199
200    public void pause() {
201        if (mState == PlaybackState.STATE_PLAYING) {
202            // Pause media player and cancel the 'foreground service' state.
203            if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
204                mMediaPlayer.pause();
205                mCurrentPosition = mMediaPlayer.getCurrentPosition();
206            }
207            // while paused, retain the MediaPlayer but give up audio focus
208            relaxResources(false);
209            giveUpAudioFocus();
210        }
211        mState = PlaybackState.STATE_PAUSED;
212        if (mCallback != null) {
213            mCallback.onPlaybackStatusChanged(mState);
214        }
215        unregisterAudioNoisyReceiver();
216    }
217
218    public void seekTo(int position) {
219        Log.d(TAG, "seekTo called with " + position);
220
221        if (mMediaPlayer == null) {
222            // If we do not have a current media player, simply update the current position
223            mCurrentPosition = position;
224        } else {
225            if (mMediaPlayer.isPlaying()) {
226                mState = PlaybackState.STATE_BUFFERING;
227            }
228            mMediaPlayer.seekTo(position);
229            if (mCallback != null) {
230                mCallback.onPlaybackStatusChanged(mState);
231            }
232        }
233    }
234
235    public void setCallback(Callback callback) {
236        this.mCallback = callback;
237    }
238
239    /**
240     * Try to get the system audio focus.
241     */
242    private void tryToGetAudioFocus() {
243        Log.d(TAG, "tryToGetAudioFocus");
244        if (mAudioFocus != AUDIO_FOCUSED) {
245            int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
246                    AudioManager.AUDIOFOCUS_GAIN);
247            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
248                mAudioFocus = AUDIO_FOCUSED;
249            }
250        }
251    }
252
253    /**
254     * Give up the audio focus.
255     */
256    private void giveUpAudioFocus() {
257        Log.d(TAG, "giveUpAudioFocus");
258        if (mAudioFocus == AUDIO_FOCUSED) {
259            if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
260                mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
261            }
262        }
263    }
264
265    /**
266     * Reconfigures MediaPlayer according to audio focus settings and
267     * starts/restarts it. This method starts/restarts the MediaPlayer
268     * respecting the current audio focus state. So if we have focus, it will
269     * play normally; if we don't have focus, it will either leave the
270     * MediaPlayer paused or set it to a low volume, depending on what is
271     * allowed by the current focus settings. This method assumes mPlayer !=
272     * null, so if you are calling it, you have to do so from a context where
273     * you are sure this is the case.
274     */
275    private void configMediaPlayerState() {
276        Log.d(TAG, "configMediaPlayerState. mAudioFocus=" + mAudioFocus);
277        if (mAudioFocus == AUDIO_NO_FOCUS_NO_DUCK) {
278            // If we don't have audio focus and can't duck, we have to pause,
279            if (mState == PlaybackState.STATE_PLAYING) {
280                pause();
281            }
282        } else {  // we have audio focus:
283            if (mAudioFocus == AUDIO_NO_FOCUS_CAN_DUCK) {
284                mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
285            } else {
286                if (mMediaPlayer != null) {
287                    mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
288                } // else do something for remote client.
289            }
290            // If we were playing when we lost focus, we need to resume playing.
291            if (mPlayOnFocusGain) {
292                if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
293                    Log.d(TAG,"configMediaPlayerState startMediaPlayer. seeking to "
294                            + mCurrentPosition);
295                    if (mCurrentPosition == mMediaPlayer.getCurrentPosition()) {
296                        mMediaPlayer.start();
297                        mState = PlaybackState.STATE_PLAYING;
298                    } else {
299                        mMediaPlayer.seekTo(mCurrentPosition);
300                        mState = PlaybackState.STATE_BUFFERING;
301                    }
302                }
303                mPlayOnFocusGain = false;
304            }
305        }
306        if (mCallback != null) {
307            mCallback.onPlaybackStatusChanged(mState);
308        }
309    }
310
311    /**
312     * Called by AudioManager on audio focus changes.
313     * Implementation of {@link android.media.AudioManager.OnAudioFocusChangeListener}
314     */
315    @Override
316    public void onAudioFocusChange(int focusChange) {
317        Log.d(TAG, "onAudioFocusChange. focusChange=" + focusChange);
318        if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
319            // We have gained focus:
320            mAudioFocus = AUDIO_FOCUSED;
321
322        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
323                focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
324                focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
325            // We have lost focus. If we can duck (low playback volume), we can keep playing.
326            // Otherwise, we need to pause the playback.
327            boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
328            mAudioFocus = canDuck ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK;
329
330            // If we are playing, we need to reset media player by calling configMediaPlayerState
331            // with mAudioFocus properly set.
332            if (mState == PlaybackState.STATE_PLAYING && !canDuck) {
333                // If we don't have audio focus and can't duck, we save the information that
334                // we were playing, so that we can resume playback once we get the focus back.
335                mPlayOnFocusGain = true;
336            }
337        } else {
338            Log.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange);
339        }
340        configMediaPlayerState();
341    }
342
343    /**
344     * Called when MediaPlayer has completed a seek
345     *
346     * @see android.media.MediaPlayer.OnSeekCompleteListener
347     */
348    @Override
349    public void onSeekComplete(MediaPlayer mp) {
350        Log.d(TAG, "onSeekComplete from MediaPlayer:" + mp.getCurrentPosition());
351        mCurrentPosition = mp.getCurrentPosition();
352        if (mState == PlaybackState.STATE_BUFFERING) {
353            mMediaPlayer.start();
354            mState = PlaybackState.STATE_PLAYING;
355        }
356        if (mCallback != null) {
357            mCallback.onPlaybackStatusChanged(mState);
358        }
359    }
360
361    /**
362     * Called when media player is done playing current song.
363     *
364     * @see android.media.MediaPlayer.OnCompletionListener
365     */
366    @Override
367    public void onCompletion(MediaPlayer player) {
368        Log.d(TAG, "onCompletion from MediaPlayer");
369        // The media player finished playing the current song, so we go ahead
370        // and start the next.
371        if (mCallback != null) {
372            mCallback.onCompletion();
373        }
374    }
375
376    /**
377     * Called when media player is done preparing.
378     *
379     * @see android.media.MediaPlayer.OnPreparedListener
380     */
381    @Override
382    public void onPrepared(MediaPlayer player) {
383        Log.d(TAG, "onPrepared from MediaPlayer");
384        // The media player is done preparing. That means we can start playing if we
385        // have audio focus.
386        configMediaPlayerState();
387    }
388
389    /**
390     * Called when there's an error playing media. When this happens, the media
391     * player goes to the Error state. We warn the user about the error and
392     * reset the media player.
393     *
394     * @see android.media.MediaPlayer.OnErrorListener
395     */
396    @Override
397    public boolean onError(MediaPlayer mp, int what, int extra) {
398        Log.e(TAG, "Media player error: what=" + what + ", extra=" + extra);
399        if (mCallback != null) {
400            mCallback.onError("MediaPlayer error " + what + " (" + extra + ")");
401        }
402        return true; // true indicates we handled the error
403    }
404
405    /**
406     * Makes sure the media player exists and has been reset. This will create
407     * the media player if needed, or reset the existing media player if one
408     * already exists.
409     */
410    private void createMediaPlayerIfNeeded() {
411        Log.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer==null));
412        if (mMediaPlayer == null) {
413            mMediaPlayer = new MediaPlayer();
414
415            // Make sure the media player will acquire a wake-lock while
416            // playing. If we don't do that, the CPU might go to sleep while the
417            // song is playing, causing playback to stop.
418            mMediaPlayer.setWakeMode(mService.getApplicationContext(),
419                    PowerManager.PARTIAL_WAKE_LOCK);
420
421            // we want the media player to notify us when it's ready preparing,
422            // and when it's done playing:
423            mMediaPlayer.setOnPreparedListener(this);
424            mMediaPlayer.setOnCompletionListener(this);
425            mMediaPlayer.setOnErrorListener(this);
426            mMediaPlayer.setOnSeekCompleteListener(this);
427        } else {
428            mMediaPlayer.reset();
429        }
430    }
431
432    /**
433     * Releases resources used by the service for playback. This includes the
434     * "foreground service" status, the wake locks and possibly the MediaPlayer.
435     *
436     * @param releaseMediaPlayer Indicates whether the Media Player should also
437     *            be released or not
438     */
439    private void relaxResources(boolean releaseMediaPlayer) {
440        Log.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer);
441
442        mService.stopForeground(true);
443
444        // stop and release the Media Player, if it's available
445        if (releaseMediaPlayer && mMediaPlayer != null) {
446            mMediaPlayer.reset();
447            mMediaPlayer.release();
448            mMediaPlayer = null;
449        }
450
451        // we can also release the Wifi lock, if we're holding it
452        if (mWifiLock.isHeld()) {
453            mWifiLock.release();
454        }
455    }
456
457    private void registerAudioNoisyReceiver() {
458        if (!mAudioNoisyReceiverRegistered) {
459            mService.registerReceiver(mAudioNoisyReceiver, mAudioNoisyIntentFilter);
460            mAudioNoisyReceiverRegistered = true;
461        }
462    }
463
464    private void unregisterAudioNoisyReceiver() {
465        if (mAudioNoisyReceiverRegistered) {
466            mService.unregisterReceiver(mAudioNoisyReceiver);
467            mAudioNoisyReceiverRegistered = false;
468        }
469    }
470
471    interface Callback {
472        /**
473         * On current music completed.
474         */
475        void onCompletion();
476        /**
477         * on Playback status changed
478         * Implementations can use this callback to update
479         * playback state on the media sessions.
480         */
481        void onPlaybackStatusChanged(int state);
482
483        /**
484         * @param error to be added to the PlaybackState
485         */
486        void onError(String error);
487
488    }
489
490}
491