blob: 70d4f4be32257f14da7e741b4c89e91bb332f3e7 [file] [log] [blame]
The Android Open Source Project792a2202009-03-03 19:32:30 -08001/*
2 * Copyright (C) 2007 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.android.music;
18
The Android Open Source Project792a2202009-03-03 19:32:30 -080019import android.app.PendingIntent;
The Android Open Source Project792a2202009-03-03 19:32:30 -080020import android.content.Context;
21import android.content.Intent;
Marco Nelissen8717d342011-09-08 09:32:51 -070022import android.graphics.Bitmap;
Jack Hef02d3c62017-02-21 00:39:22 -050023import android.media.MediaDescription;
24import android.media.MediaMetadata;
25import android.media.browse.MediaBrowser.MediaItem;
26import android.media.session.MediaSession;
27import android.media.session.PlaybackState;
28import android.os.Bundle;
The Android Open Source Project792a2202009-03-03 19:32:30 -080029import android.os.Handler;
The Android Open Source Project792a2202009-03-03 19:32:30 -080030import android.os.Message;
The Android Open Source Project792a2202009-03-03 19:32:30 -080031import android.os.SystemClock;
Jack Hef02d3c62017-02-21 00:39:22 -050032import android.service.media.MediaBrowserService;
33import android.text.TextUtils;
The Android Open Source Project792a2202009-03-03 19:32:30 -080034import android.util.Log;
Jack Hef02d3c62017-02-21 00:39:22 -050035import com.android.music.utils.*;
The Android Open Source Project792a2202009-03-03 19:32:30 -080036
Marco Nelissen2b0b9132009-05-21 16:26:47 -070037import java.lang.ref.WeakReference;
Jack Hef02d3c62017-02-21 00:39:22 -050038import java.util.*;
39
40import static com.android.music.utils.MediaIDHelper.*;
The Android Open Source Project792a2202009-03-03 19:32:30 -080041
42/**
43 * Provides "background" audio playback capabilities, allowing the
44 * user to switch between activities without stopping playback.
45 */
Jack Hef02d3c62017-02-21 00:39:22 -050046public class MediaPlaybackService extends MediaBrowserService implements Playback.Callback {
47 private static final String TAG = LogHelper.makeLogTag(MediaPlaybackService.class);
Jack Heb0fba8b2017-01-26 15:54:38 -080048
Jack Hef02d3c62017-02-21 00:39:22 -050049 // Delay stopSelf by using a handler.
50 private static final int STOP_DELAY = 30000;
Jack Heb0fba8b2017-01-26 15:54:38 -080051
Jack Hef02d3c62017-02-21 00:39:22 -050052 public static final String ACTION_CMD = "com.android.music.ACTION_CMD";
53 public static final String CMD_NAME = "CMD_NAME";
54 public static final String CMD_PAUSE = "CMD_PAUSE";
55 public static final String CMD_REPEAT = "CMD_PAUSE";
56 public static final String REPEAT_MODE = "REPEAT_MODE";
The Android Open Source Project792a2202009-03-03 19:32:30 -080057
Jack Hef02d3c62017-02-21 00:39:22 -050058 public enum RepeatMode { REPEAT_NONE, REPEAT_ALL, REPEAT_CURRENT }
The Android Open Source Project792a2202009-03-03 19:32:30 -080059
Jack Hef02d3c62017-02-21 00:39:22 -050060 // Music catalog manager
61 private MusicProvider mMusicProvider;
62 private MediaSession mSession;
63 // "Now playing" queue:
64 private List<MediaSession.QueueItem> mPlayingQueue = null;
65 private int mCurrentIndexOnQueue = -1;
66 private MediaNotificationManager mMediaNotificationManager;
67 // Indicates whether the service was started.
68 private boolean mServiceStarted;
69 private DelayedStopHandler mDelayedStopHandler = new DelayedStopHandler(this);
70 private Playback mPlayback;
71 // Default mode is repeat none
72 private RepeatMode mRepeatMode = RepeatMode.REPEAT_NONE;
73 // Extra information for this session
74 private Bundle mExtras;
Jean-Michel Trivi085cbfd2010-03-08 17:06:18 -080075
Jack Heb0fba8b2017-01-26 15:54:38 -080076 public MediaPlaybackService() {}
The Android Open Source Project792a2202009-03-03 19:32:30 -080077
78 @Override
79 public void onCreate() {
Jack Hef02d3c62017-02-21 00:39:22 -050080 LogHelper.d(TAG, "onCreate()");
The Android Open Source Project792a2202009-03-03 19:32:30 -080081 super.onCreate();
Jack Hef02d3c62017-02-21 00:39:22 -050082 LogHelper.d(TAG, "Create MusicProvider");
83 mPlayingQueue = new ArrayList<>();
84 mMusicProvider = new MusicProvider(this);
Jean-Michel Trivi085cbfd2010-03-08 17:06:18 -080085
Jack Hef02d3c62017-02-21 00:39:22 -050086 LogHelper.d(TAG, "Create MediaSession");
87 // Start a new MediaSession
88 mSession = new MediaSession(this, "MediaPlaybackService");
89 // Set extra information
90 mExtras = new Bundle();
91 mExtras.putInt(REPEAT_MODE, mRepeatMode.ordinal());
92 mSession.setExtras(mExtras);
93 // Enable callbacks from MediaButtons and TransportControls
94 mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
95 | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
96 // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
97 PlaybackState.Builder stateBuilder = new PlaybackState.Builder().setActions(
98 PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE);
99 mSession.setPlaybackState(stateBuilder.build());
100 // MediaSessionCallback() has methods that handle callbacks from a media controller
101 mSession.setCallback(new MediaSessionCallback());
102 // Set the session's token so that client activities can communicate with it.
103 setSessionToken(mSession.getSessionToken());
Mike Lockwood8ef36792013-08-01 10:03:03 -0700104
Jack Hef02d3c62017-02-21 00:39:22 -0500105 mPlayback = new Playback(this, mMusicProvider);
106 mPlayback.setState(PlaybackState.STATE_NONE);
107 mPlayback.setCallback(this);
108 mPlayback.start();
Mike Lockwood8ef36792013-08-01 10:03:03 -0700109
Jack Hef02d3c62017-02-21 00:39:22 -0500110 Context context = getApplicationContext();
111 Intent intent = new Intent(context, MusicBrowserActivity.class);
112 PendingIntent pi = PendingIntent.getActivity(
113 context, 99 /*request code*/, intent, PendingIntent.FLAG_UPDATE_CURRENT);
114 mSession.setSessionActivity(pi);
Jack Heb0fba8b2017-01-26 15:54:38 -0800115
Jack Hef02d3c62017-02-21 00:39:22 -0500116 updatePlaybackState(null);
Jack Heb0fba8b2017-01-26 15:54:38 -0800117
Jack Hef02d3c62017-02-21 00:39:22 -0500118 mMediaNotificationManager = new MediaNotificationManager(this);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800119 }
120
121 @Override
Jack Hef02d3c62017-02-21 00:39:22 -0500122 public int onStartCommand(Intent startIntent, int flags, int startId) {
123 if (startIntent != null) {
124 String action = startIntent.getAction();
125 String command = startIntent.getStringExtra(CMD_NAME);
126 if (ACTION_CMD.equals(action)) {
127 if (CMD_PAUSE.equals(command)) {
128 if (mPlayback != null && mPlayback.isPlaying()) {
129 handlePauseRequest();
Marco Nelissen63dbbbb2009-08-17 14:17:27 -0700130 }
131 }
132 }
The Android Open Source Project792a2202009-03-03 19:32:30 -0800133 }
Marco Nelissenc1017e52009-09-24 13:21:06 -0700134 return START_STICKY;
The Android Open Source Project792a2202009-03-03 19:32:30 -0800135 }
Jack Heb0fba8b2017-01-26 15:54:38 -0800136
The Android Open Source Project792a2202009-03-03 19:32:30 -0800137 @Override
Jack Hef02d3c62017-02-21 00:39:22 -0500138 public void onDestroy() {
139 Log.d(TAG, "onDestroy");
140 // Service is being killed, so make sure we release our resources
141 handleStopRequest(null);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800142
Jack Hef02d3c62017-02-21 00:39:22 -0500143 mDelayedStopHandler.removeCallbacksAndMessages(null);
144 // Always release the MediaSession to clean up resources
145 // and notify associated MediaController(s).
146 mSession.release();
The Android Open Source Project792a2202009-03-03 19:32:30 -0800147 }
Jack Heb0fba8b2017-01-26 15:54:38 -0800148
Jack Hef02d3c62017-02-21 00:39:22 -0500149 @Override
150 public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
151 Log.d(TAG,
152 "OnGetRoot: clientPackageName=" + clientPackageName + "; clientUid=" + clientUid
153 + " ; rootHints=" + rootHints);
154 // Allow everyone to browse
155 return new BrowserRoot(MEDIA_ID_ROOT, null);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800156 }
157
Jack Hef02d3c62017-02-21 00:39:22 -0500158 @Override
159 public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
160 Log.d(TAG, "OnLoadChildren: parentMediaId=" + parentMediaId);
161 // Browsing not allowed
162 if (parentMediaId == null) {
163 result.sendResult(null);
164 return;
165 }
166 if (!mMusicProvider.isInitialized()) {
167 // Use result.detach to allow calling result.sendResult from another thread:
168 result.detach();
169
170 mMusicProvider.retrieveMediaAsync(new MusicProvider.MusicProviderCallback() {
The Android Open Source Project792a2202009-03-03 19:32:30 -0800171 @Override
Jack Hef02d3c62017-02-21 00:39:22 -0500172 public void onMusicCatalogReady(boolean success) {
173 Log.d(TAG, "Received catalog result, success: " + String.valueOf(success));
174 if (success) {
175 onLoadChildren(parentMediaId, result);
176 } else {
177 result.sendResult(Collections.emptyList());
The Android Open Source Project792a2202009-03-03 19:32:30 -0800178 }
179 }
Jack Hef02d3c62017-02-21 00:39:22 -0500180 });
The Android Open Source Project792a2202009-03-03 19:32:30 -0800181
The Android Open Source Project792a2202009-03-03 19:32:30 -0800182 } else {
Jack Hef02d3c62017-02-21 00:39:22 -0500183 // If our music catalog is already loaded/cached, load them into result immediately
184 List<MediaItem> mediaItems = new ArrayList<>();
Jack Heb0fba8b2017-01-26 15:54:38 -0800185
Jack Hef02d3c62017-02-21 00:39:22 -0500186 switch (parentMediaId) {
187 case MEDIA_ID_ROOT:
188 Log.d(TAG, "OnLoadChildren.ROOT");
189 mediaItems.add(new MediaItem(new MediaDescription.Builder()
190 .setMediaId(MEDIA_ID_MUSICS_BY_ARTIST)
191 .setTitle("Artists")
192 .build(),
193 MediaItem.FLAG_BROWSABLE));
194 mediaItems.add(new MediaItem(new MediaDescription.Builder()
195 .setMediaId(MEDIA_ID_MUSICS_BY_ALBUM)
196 .setTitle("Albums")
197 .build(),
198 MediaItem.FLAG_BROWSABLE));
199 mediaItems.add(new MediaItem(new MediaDescription.Builder()
200 .setMediaId(MEDIA_ID_MUSICS_BY_SONG)
201 .setTitle("Songs")
202 .build(),
203 MediaItem.FLAG_BROWSABLE));
204 mediaItems.add(new MediaItem(new MediaDescription.Builder()
205 .setMediaId(MEDIA_ID_MUSICS_BY_PLAYLIST)
206 .setTitle("Playlists")
207 .build(),
208 MediaItem.FLAG_BROWSABLE));
209 break;
210 case MEDIA_ID_MUSICS_BY_ARTIST:
211 Log.d(TAG, "OnLoadChildren.ARTIST");
212 for (String artist : mMusicProvider.getArtists()) {
213 MediaItem item = new MediaItem(
214 new MediaDescription.Builder()
215 .setMediaId(MediaIDHelper.createBrowseCategoryMediaID(
216 MEDIA_ID_MUSICS_BY_ARTIST, artist))
217 .setTitle(artist)
218 .build(),
219 MediaItem.FLAG_BROWSABLE);
220 mediaItems.add(item);
221 }
222 break;
223 case MEDIA_ID_MUSICS_BY_PLAYLIST:
224 LogHelper.d(TAG, "OnLoadChildren.PLAYLIST");
225 for (String playlist : mMusicProvider.getPlaylists()) {
226 MediaItem item = new MediaItem(
227 new MediaDescription.Builder()
228 .setMediaId(MediaIDHelper.createBrowseCategoryMediaID(
229 MEDIA_ID_MUSICS_BY_PLAYLIST, playlist))
230 .setTitle(playlist)
231 .build(),
232 MediaItem.FLAG_BROWSABLE);
233 mediaItems.add(item);
234 }
235 break;
236 case MEDIA_ID_MUSICS_BY_ALBUM:
237 Log.d(TAG, "OnLoadChildren.ALBUM");
238 loadAlbum(mMusicProvider.getAlbums(), mediaItems);
239 break;
240 case MEDIA_ID_MUSICS_BY_SONG:
241 Log.d(TAG, "OnLoadChildren.SONG");
242 String hierarchyAwareMediaID = MediaIDHelper.createBrowseCategoryMediaID(
243 parentMediaId, MEDIA_ID_MUSICS_BY_SONG);
244 loadSong(mMusicProvider.getMusicList(), mediaItems, hierarchyAwareMediaID);
245 break;
246 default:
247 if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_ARTIST)) {
248 String artist = MediaIDHelper.getHierarchy(parentMediaId)[1];
249 Log.d(TAG, "OnLoadChildren.SONGS_BY_ARTIST artist=" + artist);
250 loadAlbum(mMusicProvider.getAlbumByArtist(artist), mediaItems);
251 } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_ALBUM)) {
252 String album = MediaIDHelper.getHierarchy(parentMediaId)[1];
253 Log.d(TAG, "OnLoadChildren.SONGS_BY_ALBUM album=" + album);
254 loadSong(mMusicProvider.getMusicsByAlbum(album), mediaItems, parentMediaId);
255 } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_PLAYLIST)) {
256 String playlist = MediaIDHelper.getHierarchy(parentMediaId)[1];
257 LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_PLAYLIST playlist=", playlist);
258 if (playlist.equals(MEDIA_ID_NOW_PLAYING) && mPlayingQueue != null
259 && mPlayingQueue.size() > 0) {
260 loadPlayingQueue(mediaItems, parentMediaId);
261 } else {
262 loadSong(mMusicProvider.getMusicsByPlaylist(playlist), mediaItems,
263 parentMediaId);
264 }
265 } else {
266 Log.w(TAG, "Skipping unmatched parentMediaId: " + parentMediaId);
267 }
268 break;
The Android Open Source Project792a2202009-03-03 19:32:30 -0800269 }
Jack Hef02d3c62017-02-21 00:39:22 -0500270 Log.d(TAG,
271 "OnLoadChildren sending " + mediaItems.size() + " results for "
272 + parentMediaId);
273 result.sendResult(mediaItems);
Marco Nelissen3aa9ad02010-09-16 13:23:11 -0700274 }
The Android Open Source Project792a2202009-03-03 19:32:30 -0800275 }
Jack Heb0fba8b2017-01-26 15:54:38 -0800276
Jack Hef02d3c62017-02-21 00:39:22 -0500277 private void loadPlayingQueue(List<MediaItem> mediaItems, String parentId) {
278 for (MediaSession.QueueItem queueItem : mPlayingQueue) {
279 MediaItem mediaItem =
280 new MediaItem(queueItem.getDescription(), MediaItem.FLAG_PLAYABLE);
281 mediaItems.add(mediaItem);
282 }
283 }
284
285 private void loadSong(
286 Iterable<MediaMetadata> songList, List<MediaItem> mediaItems, String parentId) {
287 for (MediaMetadata metadata : songList) {
288 String hierarchyAwareMediaID =
289 MediaIDHelper.createMediaID(metadata.getDescription().getMediaId(), parentId);
290 Bundle songExtra = new Bundle();
291 songExtra.putLong(MediaMetadata.METADATA_KEY_DURATION,
292 metadata.getLong(MediaMetadata.METADATA_KEY_DURATION));
293 String title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE);
294 String artistName = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
295 MediaItem item = new MediaItem(new MediaDescription.Builder()
296 .setMediaId(hierarchyAwareMediaID)
297 .setTitle(title)
298 .setSubtitle(artistName)
299 .setExtras(songExtra)
300 .build(),
301 MediaItem.FLAG_PLAYABLE);
302 mediaItems.add(item);
303 }
304 }
305
306 private void loadAlbum(Iterable<MediaMetadata> albumList, List<MediaItem> mediaItems) {
307 for (MediaMetadata albumMetadata : albumList) {
308 String albumName = albumMetadata.getString(MediaMetadata.METADATA_KEY_ALBUM);
309 String artistName = albumMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
310 Bundle albumExtra = new Bundle();
311 albumExtra.putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS,
312 albumMetadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
313 MediaItem item = new MediaItem(
314 new MediaDescription.Builder()
315 .setMediaId(MediaIDHelper.createBrowseCategoryMediaID(
316 MEDIA_ID_MUSICS_BY_ALBUM, albumName))
317 .setTitle(albumName)
318 .setSubtitle(artistName)
319 .setIconBitmap(
320 albumMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART))
321 .setExtras(albumExtra)
322 .build(),
323 MediaItem.FLAG_BROWSABLE);
324 mediaItems.add(item);
325 }
326 }
327
328 private final class MediaSessionCallback extends MediaSession.Callback {
329 @Override
330 public void onPlay() {
331 Log.d(TAG, "play");
332
333 if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
334 mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
335 mSession.setQueue(mPlayingQueue);
336 mSession.setQueueTitle(getString(R.string.random_queue_title));
337 // start playing from the beginning of the queue
338 mCurrentIndexOnQueue = 0;
339 }
340
341 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
342 handlePlayRequest();
343 }
344 }
345
346 @Override
347 public void onSkipToQueueItem(long queueId) {
348 LogHelper.d(TAG, "OnSkipToQueueItem:", queueId);
349
350 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
351 // set the current index on queue from the music Id:
352 mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
353 // play the music
354 handlePlayRequest();
355 }
356 }
357
358 @Override
359 public void onSeekTo(long position) {
360 Log.d(TAG, "onSeekTo:" + position);
361 mPlayback.seekTo((int) position);
362 }
363
364 @Override
365 public void onPlayFromMediaId(String mediaId, Bundle extras) {
366 LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras);
367
368 // The mediaId used here is not the unique musicId. This one comes from the
369 // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
370 // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
371 // so we can build the correct playing queue, based on where the track was
372 // selected from.
373 mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider);
374 mSession.setQueue(mPlayingQueue);
375 String queueTitle = getString(R.string.browse_musics_by_genre_subtitle,
376 MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId));
377 mSession.setQueueTitle(queueTitle);
378
379 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
380 // set the current index on queue from the media Id:
381 mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, mediaId);
382
383 if (mCurrentIndexOnQueue < 0) {
384 LogHelper.e(TAG, "playFromMediaId: media ID ", mediaId,
385 " could not be found on queue. Ignoring.");
386 } else {
387 // play the music
388 handlePlayRequest();
The Android Open Source Project792a2202009-03-03 19:32:30 -0800389 }
390 }
Jack Hef02d3c62017-02-21 00:39:22 -0500391 }
392
393 @Override
394 public void onPause() {
395 LogHelper.d(TAG, "pause. current state=" + mPlayback.getState());
396 handlePauseRequest();
397 }
398
399 @Override
400 public void onStop() {
401 LogHelper.d(TAG, "stop. current state=" + mPlayback.getState());
402 handleStopRequest(null);
403 }
404
405 @Override
406 public void onSkipToNext() {
407 LogHelper.d(TAG, "skipToNext");
408 mCurrentIndexOnQueue++;
409 if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
410 // This sample's behavior: skipping to next when in last song returns to the
411 // first song.
412 mCurrentIndexOnQueue = 0;
413 }
414 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
415 handlePlayRequest();
416 } else {
417 LogHelper.e(TAG,
418 "skipToNext: cannot skip to next. next Index=" + mCurrentIndexOnQueue
419 + " queue length="
420 + (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
421 handleStopRequest("Cannot skip");
422 }
423 }
424
425 @Override
426 public void onSkipToPrevious() {
427 LogHelper.d(TAG, "skipToPrevious");
428 mCurrentIndexOnQueue--;
429 if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) {
430 // This sample's behavior: skipping to previous when in first song restarts the
431 // first song.
432 mCurrentIndexOnQueue = 0;
433 }
434 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
435 handlePlayRequest();
436 } else {
437 LogHelper.e(TAG,
438 "skipToPrevious: cannot skip to previous. previous Index="
439 + mCurrentIndexOnQueue + " queue length="
440 + (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
441 handleStopRequest("Cannot skip");
442 }
443 }
444
445 @Override
446 public void onPlayFromSearch(String query, Bundle extras) {
447 LogHelper.d(TAG, "playFromSearch query=", query);
448
449 if (TextUtils.isEmpty(query)) {
450 // A generic search like "Play music" sends an empty query
451 // and it's expected that we start playing something. What will be played depends
452 // on the app: favorite playlist, "I'm feeling lucky", most recent, etc.
453 mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
454 } else {
455 mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider);
456 }
457
458 LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size());
459 mSession.setQueue(mPlayingQueue);
460
461 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
462 // immediately start playing from the beginning of the search results
463 mCurrentIndexOnQueue = 0;
464
465 handlePlayRequest();
466 } else {
467 // if nothing was found, we need to warn the user and stop playing
468 handleStopRequest(getString(R.string.no_search_results));
469 }
470 }
471
472 @Override
473 public void onCustomAction(String action, Bundle extras) {
474 LogHelper.d(TAG, "onCustomAction action=", action, ", extras=", extras);
475 switch (action) {
476 case CMD_REPEAT:
477 mRepeatMode = RepeatMode.values()[extras.getInt(REPEAT_MODE)];
478 mExtras.putInt(REPEAT_MODE, mRepeatMode.ordinal());
479 mSession.setExtras(mExtras);
480 LogHelper.d(TAG, "modified repeatMode=", mRepeatMode);
481 break;
482 default:
483 LogHelper.d(TAG, "Unkown action=", action);
484 break;
The Android Open Source Project792a2202009-03-03 19:32:30 -0800485 }
486 }
487 }
488
489 /**
Jack Hef02d3c62017-02-21 00:39:22 -0500490 * Handle a request to play music
The Android Open Source Project792a2202009-03-03 19:32:30 -0800491 */
Jack Hef02d3c62017-02-21 00:39:22 -0500492 private void handlePlayRequest() {
493 LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());
494
495 mDelayedStopHandler.removeCallbacksAndMessages(null);
496 if (!mServiceStarted) {
497 LogHelper.v(TAG, "Starting service");
498 // The MusicService needs to keep running even after the calling MediaBrowser
499 // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
500 // need to play media.
501 startService(new Intent(getApplicationContext(), MediaPlaybackService.class));
502 mServiceStarted = true;
503 }
504
505 if (!mSession.isActive()) {
506 mSession.setActive(true);
507 }
508
509 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
510 updateMetadata();
511 mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue));
512 }
513 }
514
515 /**
516 * Handle a request to pause music
517 */
518 private void handlePauseRequest() {
519 LogHelper.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState());
520 mPlayback.pause();
521 // reset the delayed stop handler.
522 mDelayedStopHandler.removeCallbacksAndMessages(null);
523 mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
524 }
525
526 /**
527 * Handle a request to stop music
528 */
529 private void handleStopRequest(String withError) {
530 LogHelper.d(
531 TAG, "handleStopRequest: mState=" + mPlayback.getState() + " error=", withError);
532 mPlayback.stop(true);
533 // reset the delayed stop handler.
534 mDelayedStopHandler.removeCallbacksAndMessages(null);
535 mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
536
537 updatePlaybackState(withError);
538
539 // service is no longer necessary. Will be started again if needed.
540 stopSelf();
541 mServiceStarted = false;
542 }
543
544 private void updateMetadata() {
545 if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
546 LogHelper.e(TAG, "Can't retrieve current metadata.");
547 updatePlaybackState(getResources().getString(R.string.error_no_metadata));
548 return;
549 }
550 MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
551 String musicId =
552 MediaIDHelper.extractMusicIDFromMediaID(queueItem.getDescription().getMediaId());
553 MediaMetadata track = mMusicProvider.getMusicByMediaId(musicId).getMetadata();
554 final String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
555 if (!musicId.equals(trackId)) {
556 IllegalStateException e = new IllegalStateException("track ID should match musicId.");
557 LogHelper.e(TAG, "track ID should match musicId.", " musicId=", musicId,
558 " trackId=", trackId,
559 " mediaId from queueItem=", queueItem.getDescription().getMediaId(),
560 " title from queueItem=", queueItem.getDescription().getTitle(),
561 " mediaId from track=", track.getDescription().getMediaId(),
562 " title from track=", track.getDescription().getTitle(),
563 " source.hashcode from track=",
564 track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE).hashCode(), e);
565 throw e;
566 }
567 LogHelper.d(TAG, "Updating metadata for MusicID= " + musicId);
568 mSession.setMetadata(track);
569
570 // Set the proper album artwork on the media session, so it can be shown in the
571 // locked screen and in other places.
572 if (track.getDescription().getIconBitmap() == null
573 && track.getDescription().getIconUri() != null) {
574 String albumUri = track.getDescription().getIconUri().toString();
575 AlbumArtCache.getInstance().fetch(albumUri, new AlbumArtCache.FetchListener() {
576 @Override
577 public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
578 MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
579 MediaMetadata track = mMusicProvider.getMusicByMediaId(trackId).getMetadata();
580 track = new MediaMetadata
581 .Builder(track)
582
583 // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is
584 // used, for
585 // example, on the lockscreen background when the media session
586 // is active.
587 .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
588
589 // set small version of the album art in the DISPLAY_ICON. This
590 // is used on
591 // the MediaDescription and thus it should be small to be
592 // serialized if
593 // necessary..
594 .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, icon)
595
596 .build();
597
598 mMusicProvider.updateMusic(trackId, track);
599
600 // If we are still playing the same music
601 String currentPlayingId = MediaIDHelper.extractMusicIDFromMediaID(
602 queueItem.getDescription().getMediaId());
603 if (trackId.equals(currentPlayingId)) {
604 mSession.setMetadata(track);
The Android Open Source Project792a2202009-03-03 19:32:30 -0800605 }
606 }
Jack Hef02d3c62017-02-21 00:39:22 -0500607 });
The Android Open Source Project792a2202009-03-03 19:32:30 -0800608 }
609 }
Jack Heb0fba8b2017-01-26 15:54:38 -0800610
The Android Open Source Project792a2202009-03-03 19:32:30 -0800611 /**
Jack Hef02d3c62017-02-21 00:39:22 -0500612 * Update the current media player state, optionally showing an error message.
613 *
614 * @param error if not null, error message to present to the user.
The Android Open Source Project792a2202009-03-03 19:32:30 -0800615 */
Jack Hef02d3c62017-02-21 00:39:22 -0500616 private void updatePlaybackState(String error) {
617 LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState());
618 long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
619 if (mPlayback != null && mPlayback.isConnected()) {
620 position = mPlayback.getCurrentStreamPosition();
The Android Open Source Project792a2202009-03-03 19:32:30 -0800621 }
Jack Hef02d3c62017-02-21 00:39:22 -0500622
623 PlaybackState.Builder stateBuilder =
624 new PlaybackState.Builder().setActions(getAvailableActions());
625
626 int state = mPlayback.getState();
627
628 // If there is an error message, send it to the playback state:
629 if (error != null) {
630 // Error states are really only supposed to be used for errors that cause playback to
631 // stop unexpectedly and persist until the user takes action to fix it.
632 stateBuilder.setErrorMessage(error);
633 state = PlaybackState.STATE_ERROR;
634 }
635 stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime());
636
637 // Set the activeQueueItemId if the current index is valid.
638 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
639 MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
640 stateBuilder.setActiveQueueItemId(item.getQueueId());
641 }
642
643 mSession.setPlaybackState(stateBuilder.build());
644
645 if (state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_PAUSED) {
646 mMediaNotificationManager.startNotification();
647 }
648 }
649
650 private long getAvailableActions() {
651 long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID
652 | PlaybackState.ACTION_PLAY_FROM_SEARCH;
653 if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
654 return actions;
655 }
656 if (mPlayback.isPlaying()) {
657 actions |= PlaybackState.ACTION_PAUSE;
658 }
659 if (mCurrentIndexOnQueue > 0) {
660 actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
661 }
662 if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) {
663 actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
664 }
665 return actions;
666 }
667
668 private MediaMetadata getCurrentPlayingMusic() {
669 if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
670 MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
671 if (item != null) {
672 LogHelper.d(TAG,
673 "getCurrentPlayingMusic for musicId=", item.getDescription().getMediaId());
674 return mMusicProvider
675 .getMusicByMediaId(MediaIDHelper.extractMusicIDFromMediaID(
676 item.getDescription().getMediaId()))
677 .getMetadata();
678 }
679 }
680 return null;
The Android Open Source Project792a2202009-03-03 19:32:30 -0800681 }
682
683 /**
Jack Hef02d3c62017-02-21 00:39:22 -0500684 * Implementation of the Playback.Callback interface
The Android Open Source Project792a2202009-03-03 19:32:30 -0800685 */
Jack Hef02d3c62017-02-21 00:39:22 -0500686 @Override
687 public void onCompletion() {
688 // The media player finished playing the current song, so we go ahead
689 // and start the next.
690 if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
691 switch (mRepeatMode) {
692 case REPEAT_ALL:
693 // Increase the index
694 mCurrentIndexOnQueue++;
695 // Restart queue when reaching the end
696 if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
697 mCurrentIndexOnQueue = 0;
698 }
Marco Nelissenc37b2002012-10-16 12:59:54 -0700699 break;
Jack Hef02d3c62017-02-21 00:39:22 -0500700 case REPEAT_CURRENT:
701 // Do not change the index
702 break;
703 case REPEAT_NONE:
704 default:
705 // Increase the index
706 mCurrentIndexOnQueue++;
707 // Stop the queue when reaching the end
708 if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
709 handleStopRequest(null);
Marco Nelissen90d1f622012-04-05 12:27:56 -0700710 return;
711 }
Marco Nelissenbd0d2f12012-02-27 16:02:15 -0800712 break;
Marco Nelissenbd0d2f12012-02-27 16:02:15 -0800713 }
Jack Hef02d3c62017-02-21 00:39:22 -0500714 handlePlayRequest();
Marco Nelissenbd0d2f12012-02-27 16:02:15 -0800715 } else {
Jack Hef02d3c62017-02-21 00:39:22 -0500716 // If there is nothing to play, we stop and release the resources:
717 handleStopRequest(null);
Eric Laurent1cc72a12010-06-28 11:27:01 -0700718 }
Marco Nelissen2b0b9132009-05-21 16:26:47 -0700719 }
Marco Nelissen39888902010-03-02 10:27:05 -0800720
721 @Override
Jack Hef02d3c62017-02-21 00:39:22 -0500722 public void onPlaybackStatusChanged(int state) {
723 updatePlaybackState(null);
Marco Nelissen39888902010-03-02 10:27:05 -0800724 }
725
Jack Hef02d3c62017-02-21 00:39:22 -0500726 @Override
727 public void onError(String error) {
728 updatePlaybackState(error);
729 }
730
731 /**
732 * A simple handler that stops the service if playback is not active (playing)
733 */
734 private static class DelayedStopHandler extends Handler {
735 private final WeakReference<MediaPlaybackService> mWeakReference;
736
737 private DelayedStopHandler(MediaPlaybackService service) {
738 mWeakReference = new WeakReference<>(service);
739 }
740
741 @Override
742 public void handleMessage(Message msg) {
743 MediaPlaybackService service = mWeakReference.get();
744 if (service != null && service.mPlayback != null) {
745 if (service.mPlayback.isPlaying()) {
746 Log.d(TAG, "Ignoring delayed stop since the media player is in use.");
747 return;
748 }
749 Log.d(TAG, "Stopping service with delay handler.");
750 service.stopSelf();
751 service.mServiceStarted = false;
752 }
753 }
754 }
The Android Open Source Project792a2202009-03-03 19:32:30 -0800755}