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