1 /* 2 * Copyright (C) 2011 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.musicplayer; 18 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.app.Service; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.media.AudioManager; 29 import android.media.MediaMetadataRetriever; 30 import android.media.MediaPlayer; 31 import android.media.MediaPlayer.OnCompletionListener; 32 import android.media.MediaPlayer.OnErrorListener; 33 import android.media.MediaPlayer.OnPreparedListener; 34 import android.media.RemoteControlClient; 35 import android.net.Uri; 36 import android.net.wifi.WifiManager; 37 import android.net.wifi.WifiManager.WifiLock; 38 import android.os.IBinder; 39 import android.os.PowerManager; 40 import android.util.Log; 41 import android.widget.Toast; 42 43 import java.io.IOException; 44 45 /** 46 * Service that handles media playback. This is the Service through which we perform all the media 47 * handling in our application. Upon initialization, it starts a {@link MusicRetriever} to scan 48 * the user's media. Then, it waits for Intents (which come from our main activity, 49 * {@link MainActivity}, which signal the service to perform specific operations: Play, Pause, 50 * Rewind, Skip, etc. 51 */ 52 public class MusicService extends Service implements OnCompletionListener, OnPreparedListener, 53 OnErrorListener, MusicFocusable, 54 PrepareMusicRetrieverTask.MusicRetrieverPreparedListener { 55 56 // The tag we put on debug messages 57 final static String TAG = "RandomMusicPlayer"; 58 59 // These are the Intent actions that we are prepared to handle. Notice that the fact these 60 // constants exist in our class is a mere convenience: what really defines the actions our 61 // service can handle are the <action> tags in the <intent-filters> tag for our service in 62 // AndroidManifest.xml. 63 public static final String ACTION_TOGGLE_PLAYBACK = 64 "com.example.android.musicplayer.action.TOGGLE_PLAYBACK"; 65 public static final String ACTION_PLAY = "com.example.android.musicplayer.action.PLAY"; 66 public static final String ACTION_PAUSE = "com.example.android.musicplayer.action.PAUSE"; 67 public static final String ACTION_STOP = "com.example.android.musicplayer.action.STOP"; 68 public static final String ACTION_SKIP = "com.example.android.musicplayer.action.SKIP"; 69 public static final String ACTION_REWIND = "com.example.android.musicplayer.action.REWIND"; 70 public static final String ACTION_URL = "com.example.android.musicplayer.action.URL"; 71 72 // The volume we set the media player to when we lose audio focus, but are allowed to reduce 73 // the volume instead of stopping playback. 74 public static final float DUCK_VOLUME = 0.1f; 75 76 // our media player 77 MediaPlayer mPlayer = null; 78 79 // our AudioFocusHelper object, if it's available (it's available on SDK level >= 8) 80 // If not available, this will be null. Always check for null before using! 81 AudioFocusHelper mAudioFocusHelper = null; 82 83 // indicates the state our service: 84 enum State { 85 Retrieving, // the MediaRetriever is retrieving music 86 Stopped, // media player is stopped and not prepared to play 87 Preparing, // media player is preparing... 88 Playing, // playback active (media player ready!). (but the media player may actually be 89 // paused in this state if we don't have audio focus. But we stay in this state 90 // so that we know we have to resume playback once we get focus back) 91 Paused // playback paused (media player ready!) 92 }; 93 94 State mState = State.Retrieving; 95 96 // if in Retrieving mode, this flag indicates whether we should start playing immediately 97 // when we are ready or not. 98 boolean mStartPlayingAfterRetrieve = false; 99 100 // if mStartPlayingAfterRetrieve is true, this variable indicates the URL that we should 101 // start playing when we are ready. If null, we should play a random song from the device 102 Uri mWhatToPlayAfterRetrieve = null; 103 104 enum PauseReason { 105 UserRequest, // paused by user request 106 FocusLoss, // paused because of audio focus loss 107 }; 108 109 // why did we pause? (only relevant if mState == State.Paused) 110 PauseReason mPauseReason = PauseReason.UserRequest; 111 112 // do we have audio focus? 113 enum AudioFocus { 114 NoFocusNoDuck, // we don't have audio focus, and can't duck 115 NoFocusCanDuck, // we don't have focus, but can play at a low volume ("ducking") 116 Focused // we have full audio focus 117 } 118 AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck; 119 120 // title of the song we are currently playing 121 String mSongTitle = ""; 122 123 // whether the song we are playing is streaming from the network 124 boolean mIsStreaming = false; 125 126 // Wifi lock that we hold when streaming files from the internet, in order to prevent the 127 // device from shutting off the Wifi radio 128 WifiLock mWifiLock; 129 130 // The ID we use for the notification (the onscreen alert that appears at the notification 131 // area at the top of the screen as an icon -- and as text as well if the user expands the 132 // notification area). 133 final int NOTIFICATION_ID = 1; 134 135 // Our instance of our MusicRetriever, which handles scanning for media and 136 // providing titles and URIs as we need. 137 MusicRetriever mRetriever; 138 139 // our RemoteControlClient object, which will use remote control APIs available in 140 // SDK level >= 14, if they're available. 141 RemoteControlClientCompat mRemoteControlClientCompat; 142 143 // Dummy album art we will pass to the remote control (if the APIs are available). 144 Bitmap mDummyAlbumArt; 145 146 // The component name of MusicIntentReceiver, for use with media button and remote control 147 // APIs 148 ComponentName mMediaButtonReceiverComponent; 149 150 AudioManager mAudioManager; 151 NotificationManager mNotificationManager; 152 153 Notification mNotification = null; 154 155 /** 156 * Makes sure the media player exists and has been reset. This will create the media player 157 * if needed, or reset the existing media player if one already exists. 158 */ 159 void createMediaPlayerIfNeeded() { 160 if (mPlayer == null) { 161 mPlayer = new MediaPlayer(); 162 163 // Make sure the media player will acquire a wake-lock while playing. If we don't do 164 // that, the CPU might go to sleep while the song is playing, causing playback to stop. 165 // 166 // Remember that to use this, we have to declare the android.permission.WAKE_LOCK 167 // permission in AndroidManifest.xml. 168 mPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); 169 170 // we want the media player to notify us when it's ready preparing, and when it's done 171 // playing: 172 mPlayer.setOnPreparedListener(this); 173 mPlayer.setOnCompletionListener(this); 174 mPlayer.setOnErrorListener(this); 175 } 176 else 177 mPlayer.reset(); 178 } 179 180 @Override 181 public void onCreate() { 182 Log.i(TAG, "debug: Creating service"); 183 184 // Create the Wifi lock (this does not acquire the lock, this just creates it) 185 mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE)) 186 .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock"); 187 188 mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 189 mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE); 190 191 // Create the retriever and start an asynchronous task that will prepare it. 192 mRetriever = new MusicRetriever(getContentResolver()); 193 (new PrepareMusicRetrieverTask(mRetriever,this)).execute(); 194 195 // create the Audio Focus Helper, if the Audio Focus feature is available (SDK 8 or above) 196 if (android.os.Build.VERSION.SDK_INT >= 8) 197 mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this); 198 else 199 mAudioFocus = AudioFocus.Focused; // no focus feature, so we always "have" audio focus 200 201 mDummyAlbumArt = BitmapFactory.decodeResource(getResources(), R.drawable.dummy_album_art); 202 203 mMediaButtonReceiverComponent = new ComponentName(this, MusicIntentReceiver.class); 204 } 205 206 /** 207 * Called when we receive an Intent. When we receive an intent sent to us via startService(), 208 * this is the method that gets called. So here we react appropriately depending on the 209 * Intent's action, which specifies what is being requested of us. 210 */ 211 @Override 212 public int onStartCommand(Intent intent, int flags, int startId) { 213 String action = intent.getAction(); 214 if (action.equals(ACTION_TOGGLE_PLAYBACK)) processTogglePlaybackRequest(); 215 else if (action.equals(ACTION_PLAY)) processPlayRequest(); 216 else if (action.equals(ACTION_PAUSE)) processPauseRequest(); 217 else if (action.equals(ACTION_SKIP)) processSkipRequest(); 218 else if (action.equals(ACTION_STOP)) processStopRequest(); 219 else if (action.equals(ACTION_REWIND)) processRewindRequest(); 220 else if (action.equals(ACTION_URL)) processAddRequest(intent); 221 222 return START_NOT_STICKY; // Means we started the service, but don't want it to 223 // restart in case it's killed. 224 } 225 226 void processTogglePlaybackRequest() { 227 if (mState == State.Paused || mState == State.Stopped) { 228 processPlayRequest(); 229 } else { 230 processPauseRequest(); 231 } 232 } 233 234 void processPlayRequest() { 235 if (mState == State.Retrieving) { 236 // If we are still retrieving media, just set the flag to start playing when we're 237 // ready 238 mWhatToPlayAfterRetrieve = null; // play a random song 239 mStartPlayingAfterRetrieve = true; 240 return; 241 } 242 243 tryToGetAudioFocus(); 244 245 // actually play the song 246 247 if (mState == State.Stopped) { 248 // If we're stopped, just go ahead to the next song and start playing 249 playNextSong(null); 250 } 251 else if (mState == State.Paused) { 252 // If we're paused, just continue playback and restore the 'foreground service' state. 253 mState = State.Playing; 254 setUpAsForeground(mSongTitle + " (playing)"); 255 configAndStartMediaPlayer(); 256 } 257 258 // Tell any remote controls that our playback state is 'playing'. 259 if (mRemoteControlClientCompat != null) { 260 mRemoteControlClientCompat 261 .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); 262 } 263 } 264 265 void processPauseRequest() { 266 if (mState == State.Retrieving) { 267 // If we are still retrieving media, clear the flag that indicates we should start 268 // playing when we're ready 269 mStartPlayingAfterRetrieve = false; 270 return; 271 } 272 273 if (mState == State.Playing) { 274 // Pause media player and cancel the 'foreground service' state. 275 mState = State.Paused; 276 mPlayer.pause(); 277 relaxResources(false); // while paused, we always retain the MediaPlayer 278 // do not give up audio focus 279 } 280 281 // Tell any remote controls that our playback state is 'paused'. 282 if (mRemoteControlClientCompat != null) { 283 mRemoteControlClientCompat 284 .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); 285 } 286 } 287 288 void processRewindRequest() { 289 if (mState == State.Playing || mState == State.Paused) 290 mPlayer.seekTo(0); 291 } 292 293 void processSkipRequest() { 294 if (mState == State.Playing || mState == State.Paused) { 295 tryToGetAudioFocus(); 296 playNextSong(null); 297 } 298 } 299 300 void processStopRequest() { 301 processStopRequest(false); 302 } 303 304 void processStopRequest(boolean force) { 305 if (mState == State.Playing || mState == State.Paused || force) { 306 mState = State.Stopped; 307 308 // let go of all resources... 309 relaxResources(true); 310 giveUpAudioFocus(); 311 312 // Tell any remote controls that our playback state is 'paused'. 313 if (mRemoteControlClientCompat != null) { 314 mRemoteControlClientCompat 315 .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); 316 } 317 318 // service is no longer necessary. Will be started again if needed. 319 stopSelf(); 320 } 321 } 322 323 /** 324 * Releases resources used by the service for playback. This includes the "foreground service" 325 * status and notification, the wake locks and possibly the MediaPlayer. 326 * 327 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not 328 */ 329 void relaxResources(boolean releaseMediaPlayer) { 330 // stop being a foreground service 331 stopForeground(true); 332 333 // stop and release the Media Player, if it's available 334 if (releaseMediaPlayer && mPlayer != null) { 335 mPlayer.reset(); 336 mPlayer.release(); 337 mPlayer = null; 338 } 339 340 // we can also release the Wifi lock, if we're holding it 341 if (mWifiLock.isHeld()) mWifiLock.release(); 342 } 343 344 void giveUpAudioFocus() { 345 if (mAudioFocus == AudioFocus.Focused && mAudioFocusHelper != null 346 && mAudioFocusHelper.abandonFocus()) 347 mAudioFocus = AudioFocus.NoFocusNoDuck; 348 } 349 350 /** 351 * Reconfigures MediaPlayer according to audio focus settings and starts/restarts it. This 352 * method starts/restarts the MediaPlayer respecting the current audio focus state. So if 353 * we have focus, it will play normally; if we don't have focus, it will either leave the 354 * MediaPlayer paused or set it to a low volume, depending on what is allowed by the 355 * current focus settings. This method assumes mPlayer != null, so if you are calling it, 356 * you have to do so from a context where you are sure this is the case. 357 */ 358 void configAndStartMediaPlayer() { 359 if (mAudioFocus == AudioFocus.NoFocusNoDuck) { 360 // If we don't have audio focus and can't duck, we have to pause, even if mState 361 // is State.Playing. But we stay in the Playing state so that we know we have to resume 362 // playback once we get the focus back. 363 if (mPlayer.isPlaying()) mPlayer.pause(); 364 return; 365 } 366 else if (mAudioFocus == AudioFocus.NoFocusCanDuck) 367 mPlayer.setVolume(DUCK_VOLUME, DUCK_VOLUME); // we'll be relatively quiet 368 else 369 mPlayer.setVolume(1.0f, 1.0f); // we can be loud 370 371 if (!mPlayer.isPlaying()) mPlayer.start(); 372 } 373 374 void processAddRequest(Intent intent) { 375 // user wants to play a song directly by URL or path. The URL or path comes in the "data" 376 // part of the Intent. This Intent is sent by {@link MainActivity} after the user 377 // specifies the URL/path via an alert box. 378 if (mState == State.Retrieving) { 379 // we'll play the requested URL right after we finish retrieving 380 mWhatToPlayAfterRetrieve = intent.getData(); 381 mStartPlayingAfterRetrieve = true; 382 } 383 else if (mState == State.Playing || mState == State.Paused || mState == State.Stopped) { 384 Log.i(TAG, "Playing from URL/path: " + intent.getData().toString()); 385 tryToGetAudioFocus(); 386 playNextSong(intent.getData().toString()); 387 } 388 } 389 390 void tryToGetAudioFocus() { 391 if (mAudioFocus != AudioFocus.Focused && mAudioFocusHelper != null 392 && mAudioFocusHelper.requestFocus()) 393 mAudioFocus = AudioFocus.Focused; 394 } 395 396 /** 397 * Starts playing the next song. If manualUrl is null, the next song will be randomly selected 398 * from our Media Retriever (that is, it will be a random song in the user's device). If 399 * manualUrl is non-null, then it specifies the URL or path to the song that will be played 400 * next. 401 */ 402 void playNextSong(String manualUrl) { 403 mState = State.Stopped; 404 relaxResources(false); // release everything except MediaPlayer 405 406 try { 407 MusicRetriever.Item playingItem = null; 408 if (manualUrl != null) { 409 // set the source of the media player to a manual URL or path 410 createMediaPlayerIfNeeded(); 411 mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 412 mPlayer.setDataSource(manualUrl); 413 mIsStreaming = manualUrl.startsWith("http:") || manualUrl.startsWith("https:"); 414 415 playingItem = new MusicRetriever.Item(0, null, manualUrl, null, 0); 416 } 417 else { 418 mIsStreaming = false; // playing a locally available song 419 420 playingItem = mRetriever.getRandomItem(); 421 if (playingItem == null) { 422 Toast.makeText(this, 423 "No available music to play. Place some music on your external storage " 424 + "device (e.g. your SD card) and try again.", 425 Toast.LENGTH_LONG).show(); 426 processStopRequest(true); // stop everything! 427 return; 428 } 429 430 // set the source of the media player a a content URI 431 createMediaPlayerIfNeeded(); 432 mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); 433 mPlayer.setDataSource(getApplicationContext(), playingItem.getURI()); 434 } 435 436 mSongTitle = playingItem.getTitle(); 437 438 mState = State.Preparing; 439 setUpAsForeground(mSongTitle + " (loading)"); 440 441 // Use the media button APIs (if available) to register ourselves for media button 442 // events 443 444 MediaButtonHelper.registerMediaButtonEventReceiverCompat( 445 mAudioManager, mMediaButtonReceiverComponent); 446 447 // Use the remote control APIs (if available) to set the playback state 448 449 if (mRemoteControlClientCompat == null) { 450 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); 451 intent.setComponent(mMediaButtonReceiverComponent); 452 mRemoteControlClientCompat = new RemoteControlClientCompat( 453 PendingIntent.getBroadcast(this /*context*/, 454 0 /*requestCode, ignored*/, intent /*intent*/, 0 /*flags*/)); 455 RemoteControlHelper.registerRemoteControlClient(mAudioManager, 456 mRemoteControlClientCompat); 457 } 458 459 mRemoteControlClientCompat.setPlaybackState( 460 RemoteControlClient.PLAYSTATE_PLAYING); 461 462 mRemoteControlClientCompat.setTransportControlFlags( 463 RemoteControlClient.FLAG_KEY_MEDIA_PLAY | 464 RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | 465 RemoteControlClient.FLAG_KEY_MEDIA_NEXT | 466 RemoteControlClient.FLAG_KEY_MEDIA_STOP); 467 468 // Update the remote controls 469 mRemoteControlClientCompat.editMetadata(true) 470 .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, playingItem.getArtist()) 471 .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, playingItem.getAlbum()) 472 .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, playingItem.getTitle()) 473 .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, 474 playingItem.getDuration()) 475 // TODO: fetch real item artwork 476 .putBitmap( 477 RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, 478 mDummyAlbumArt) 479 .apply(); 480 481 // starts preparing the media player in the background. When it's done, it will call 482 // our OnPreparedListener (that is, the onPrepared() method on this class, since we set 483 // the listener to 'this'). 484 // 485 // Until the media player is prepared, we *cannot* call start() on it! 486 mPlayer.prepareAsync(); 487 488 // If we are streaming from the internet, we want to hold a Wifi lock, which prevents 489 // the Wifi radio from going to sleep while the song is playing. If, on the other hand, 490 // we are *not* streaming, we want to release the lock if we were holding it before. 491 if (mIsStreaming) mWifiLock.acquire(); 492 else if (mWifiLock.isHeld()) mWifiLock.release(); 493 } 494 catch (IOException ex) { 495 Log.e("MusicService", "IOException playing next song: " + ex.getMessage()); 496 ex.printStackTrace(); 497 } 498 } 499 500 /** Called when media player is done playing current song. */ 501 public void onCompletion(MediaPlayer player) { 502 // The media player finished playing the current song, so we go ahead and start the next. 503 playNextSong(null); 504 } 505 506 /** Called when media player is done preparing. */ 507 public void onPrepared(MediaPlayer player) { 508 // The media player is done preparing. That means we can start playing! 509 mState = State.Playing; 510 updateNotification(mSongTitle + " (playing)"); 511 configAndStartMediaPlayer(); 512 } 513 514 /** Updates the notification. */ 515 void updateNotification(String text) { 516 PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0, 517 new Intent(getApplicationContext(), MainActivity.class), 518 PendingIntent.FLAG_UPDATE_CURRENT); 519 mNotification.setLatestEventInfo(getApplicationContext(), "RandomMusicPlayer", text, pi); 520 mNotificationManager.notify(NOTIFICATION_ID, mNotification); 521 } 522 523 /** 524 * Configures service as a foreground service. A foreground service is a service that's doing 525 * something the user is actively aware of (such as playing music), and must appear to the 526 * user as a notification. That's why we create the notification here. 527 */ 528 void setUpAsForeground(String text) { 529 PendingIntent pi = PendingIntent.getActivity(getApplicationContext(), 0, 530 new Intent(getApplicationContext(), MainActivity.class), 531 PendingIntent.FLAG_UPDATE_CURRENT); 532 mNotification = new Notification(); 533 mNotification.tickerText = text; 534 mNotification.icon = R.drawable.ic_stat_playing; 535 mNotification.flags |= Notification.FLAG_ONGOING_EVENT; 536 mNotification.setLatestEventInfo(getApplicationContext(), "RandomMusicPlayer", 537 text, pi); 538 startForeground(NOTIFICATION_ID, mNotification); 539 } 540 541 /** 542 * Called when there's an error playing media. When this happens, the media player goes to 543 * the Error state. We warn the user about the error and reset the media player. 544 */ 545 public boolean onError(MediaPlayer mp, int what, int extra) { 546 Toast.makeText(getApplicationContext(), "Media player error! Resetting.", 547 Toast.LENGTH_SHORT).show(); 548 Log.e(TAG, "Error: what=" + String.valueOf(what) + ", extra=" + String.valueOf(extra)); 549 550 mState = State.Stopped; 551 relaxResources(true); 552 giveUpAudioFocus(); 553 return true; // true indicates we handled the error 554 } 555 556 public void onGainedAudioFocus() { 557 Toast.makeText(getApplicationContext(), "gained audio focus.", Toast.LENGTH_SHORT).show(); 558 mAudioFocus = AudioFocus.Focused; 559 560 // restart media player with new focus settings 561 if (mState == State.Playing) 562 configAndStartMediaPlayer(); 563 } 564 565 public void onLostAudioFocus(boolean canDuck) { 566 Toast.makeText(getApplicationContext(), "lost audio focus." + (canDuck ? "can duck" : 567 "no duck"), Toast.LENGTH_SHORT).show(); 568 mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck; 569 570 // start/restart/pause media player with new focus settings 571 if (mPlayer != null && mPlayer.isPlaying()) 572 configAndStartMediaPlayer(); 573 } 574 575 public void onMusicRetrieverPrepared() { 576 // Done retrieving! 577 mState = State.Stopped; 578 579 // If the flag indicates we should start playing after retrieving, let's do that now. 580 if (mStartPlayingAfterRetrieve) { 581 tryToGetAudioFocus(); 582 playNextSong(mWhatToPlayAfterRetrieve == null ? 583 null : mWhatToPlayAfterRetrieve.toString()); 584 } 585 } 586 587 588 @Override 589 public void onDestroy() { 590 // Service is being killed, so make sure we release our resources 591 mState = State.Stopped; 592 relaxResources(true); 593 giveUpAudioFocus(); 594 } 595 596 @Override 597 public IBinder onBind(Intent arg0) { 598 return null; 599 } 600 } 601