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