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");
      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.example.android.supportv4.media;
     18 
     19 import android.content.BroadcastReceiver;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.IntentFilter;
     23 import android.media.AudioManager;
     24 import android.media.MediaPlayer;
     25 import android.media.session.PlaybackState;
     26 import android.net.wifi.WifiManager;
     27 import android.os.PowerManager;
     28 import android.support.v4.media.MediaMetadataCompat;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 
     32 import com.example.android.supportv4.media.model.MusicProvider;
     33 import com.example.android.supportv4.media.utils.MediaIDHelper;
     34 
     35 import java.io.IOException;
     36 
     37 import static android.media.MediaPlayer.OnCompletionListener;
     38 import static android.media.MediaPlayer.OnErrorListener;
     39 import static android.media.MediaPlayer.OnPreparedListener;
     40 import static android.media.MediaPlayer.OnSeekCompleteListener;
     41 import static android.support.v4.media.session.MediaSessionCompat.QueueItem;
     42 
     43 /**
     44  * A class that implements local media playback using {@link android.media.MediaPlayer}
     45  */
     46 public 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