این وبسایت یک نسخه آزمایشی از سایت developer android می‌باشد که در حال ترجمه می‌باشد.
برای پیوستن به تیم ترجمه و خواندن مستندات اینجا کلیک کنید.
MediaBrowserService / src / com.example.android.mediabrowserservice /

MusicService.java

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
 
17
package com.example.android.mediabrowserservice;
18
 
19
import android.content.Context;
20
import android.content.Intent;
21
import android.media.AudioManager;
22
import android.media.MediaDescription;
23
import android.media.MediaMetadata;
24
import android.media.MediaPlayer;
25
import android.media.MediaPlayer.OnCompletionListener;
26
import android.media.MediaPlayer.OnErrorListener;
27
import android.media.MediaPlayer.OnPreparedListener;
28
import android.media.browse.MediaBrowser;
29
import android.media.browse.MediaBrowser.MediaItem;
30
import android.media.session.MediaSession;
31
import android.media.session.PlaybackState;
32
import android.net.Uri;
33
import android.net.wifi.WifiManager;
34
import android.net.wifi.WifiManager.WifiLock;
35
import android.os.Bundle;
36
import android.os.Handler;
37
import android.os.Message;
38
import android.os.PowerManager;
39
import android.os.SystemClock;
40
import android.service.media.MediaBrowserService;
41
 
42
import com.example.android.mediabrowserservice.model.MusicProvider;
43
import com.example.android.mediabrowserservice.utils.LogHelper;
44
import com.example.android.mediabrowserservice.utils.MediaIDHelper;
45
import com.example.android.mediabrowserservice.utils.QueueHelper;
46
 
47
import java.io.IOException;
48
import java.util.ArrayList;
49
import java.util.List;
50
 
51
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
52
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT;
53
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID;
54
import static com.example.android.mediabrowserservice.utils.MediaIDHelper.extractBrowseCategoryFromMediaID;
55
 
56
/**
57
 * This class provides a MediaBrowser through a service. It exposes the media library to a browsing
58
 * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and
59
 * exposes it through its MediaSession.Token, which allows the client to create a MediaController
60
 * that connects to and send control commands to the MediaSession remotely. This is useful for
61
 * user interfaces that need to interact with your media session, like Android Auto. You can
62
 * (should) also use the same service from your app's UI, which gives a seamless playback
63
 * experience to the user.
64
 *
65
 * To implement a MediaBrowserService, you need to:
66
 *
67
 * <ul>
68
 *
69
 * <li> Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing
70
 *      related methods {@link android.service.media.MediaBrowserService#onGetRoot} and
71
 *      {@link android.service.media.MediaBrowserService#onLoadChildren};
72
 * <li> In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent
73
 *      with the session's token {@link android.service.media.MediaBrowserService#setSessionToken};
74
 *
75
 * <li> Set a callback on the
76
 *      {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}.
77
 *      The callback will receive all the user's actions, like play, pause, etc;
78
 *
79
 * <li> Handle all the actual music playing using any method your app prefers (for example,
80
 *      {@link android.media.MediaPlayer})
81
 *
82
 * <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods
83
 *      {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)}
84
 *      {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and
85
 *      {@link android.media.session.MediaSession#setQueue(java.util.List)})
86
 *
87
 * <li> Declare and export the service in AndroidManifest with an intent receiver for the action
88
 *      android.media.browse.MediaBrowserService
89
 *
90
 * </ul>
91
 *
92
 * To make your app compatible with Android Auto, you also need to:
93
 *
94
 * <ul>
95
 *
96
 * <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource
97
 *      with a &lt;automotiveApp&gt; root element. For a media app, this must include
98
 *      an &lt;uses name="media"/&gt; element as a child.
99
 *      For example, in AndroidManifest.xml:
100
 *          &lt;meta-data android:name="com.google.android.gms.car.application"
101
 *              android:resource="@xml/automotive_app_desc"/&gt;
102
 *      And in res/values/automotive_app_desc.xml:
103
 *          &lt;automotiveApp&gt;
104
 *              &lt;uses name="media"/&gt;
105
 *          &lt;/automotiveApp&gt;
106
 *
107
 * </ul>
108
 
109
 * @see <a href="README.md">README.md</a> for more details.
110
 *
111
 */
112
 
113
public class MusicService extends MediaBrowserService implements OnPreparedListener,
114
        OnCompletionListener, OnErrorListener, AudioManager.OnAudioFocusChangeListener {
115
 
116
    private static final String TAG = "MusicService";
117
 
118
    // Action to thumbs up a media item
119
    private static final String CUSTOM_ACTION_THUMBS_UP = "thumbs_up";
120
    // Delay stopSelf by using a handler.
121
    private static final int STOP_DELAY = 30000;
122
 
123
    // The volume we set the media player to when we lose audio focus, but are
124
    // allowed to reduce the volume instead of stopping playback.
125
    public static final float VOLUME_DUCK = 0.2f;
126
 
127
    // The volume we set the media player when we have audio focus.
128
    public static final float VOLUME_NORMAL = 1.0f;
129
    public static final String ANDROID_AUTO_PACKAGE_NAME = "com.google.android.projection.gearhead";
130
    public static final String ANDROID_AUTO_SIMULATOR_PACKAGE_NAME = "com.google.android.mediasimulator";
131
 
132
    // Music catalog manager
133
    private MusicProvider mMusicProvider;
134
 
135
    private MediaSession mSession;
136
    private MediaPlayer mMediaPlayer;
137
 
138
    // "Now playing" queue:
139
    private List<MediaSession.QueueItem> mPlayingQueue;
140
    private int mCurrentIndexOnQueue;
141
 
142
    // Current local media player state
143
    private int mState = PlaybackState.STATE_NONE;
144
 
145
    // Wifi lock that we hold when streaming files from the internet, in order
146
    // to prevent the device from shutting off the Wifi radio
147
    private WifiLock mWifiLock;
148
 
149
    private MediaNotification mMediaNotification;
150
 
151
    // Indicates whether the service was started.
152
    private boolean mServiceStarted;
153
 
154
    enum AudioFocus {
155
        NoFocusNoDuck, // we don't have audio focus, and can't duck
156
        NoFocusCanDuck, // we don't have focus, but can play at a low volume
157
                        // ("ducking")
158
        Focused // we have full audio focus
159
    }
160
 
161
    // Type of audio focus we have:
162
    private AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck;
163
    private AudioManager mAudioManager;
164
 
165
    // Indicates if we should start playing immediately after we gain focus.
166
    private boolean mPlayOnFocusGain;
167
 
168
    private Handler mDelayedStopHandler = new Handler() {
169
        @Override
170
        public void handleMessage(Message msg) {
171
            if ((mMediaPlayer != null && mMediaPlayer.isPlaying()) ||
172
                    mPlayOnFocusGain) {
173
                LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use.");
174
                return;
175
            }
176
            LogHelper.d(TAG, "Stopping service with delay handler.");
177
            stopSelf();
178
            mServiceStarted = false;
179
        }
180
    };
181
 
182
    /*
183
     * (non-Javadoc)
184
     * @see android.app.Service#onCreate()
185
     */
186
    @Override
187
    public void onCreate() {
188
        super.onCreate();
189
        LogHelper.d(TAG, "onCreate");
190
 
191
        mPlayingQueue = new ArrayList<>();
192
 
193
        // Create the Wifi lock (this does not acquire the lock, this just creates it)
194
        mWifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
195
                .createWifiLock(WifiManager.WIFI_MODE_FULL, "MusicDemo_lock");
196
 
197
 
198
        // Create the music catalog metadata provider
199
        mMusicProvider = new MusicProvider();
200
        mMusicProvider.retrieveMedia(new MusicProvider.Callback() {
201
            @Override
202
            public void onMusicCatalogReady(boolean success) {
203
                mState = success ? PlaybackState.STATE_NONE : PlaybackState.STATE_ERROR;
204
            }
205
        });
206
 
207
        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
208
 
209
        // Start a new MediaSession
210
        mSession = new MediaSession(this, "MusicService");
211
        setSessionToken(mSession.getSessionToken());
212
        mSession.setCallback(new MediaSessionCallback());
213
        mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
214
                MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
215
 
216
        // Use these extras to reserve space for the corresponding actions, even when they are disabled
217
        // in the playbackstate, so the custom actions don't reflow.
218
        Bundle extras = new Bundle();
219
        extras.putBoolean(
220
            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT",
221
            true);
222
        extras.putBoolean(
223
            "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS",
224
            true);
225
        // If you want to reserve the Queue slot when there is no queue
226
        // (mSession.setQueue(emptylist)), uncomment the lines below:
227
        // extras.putBoolean(
228
        //   "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE",
229
        //   true);
230
        mSession.setExtras(extras);
231
 
232
        updatePlaybackState(null);
233
 
234
        mMediaNotification = new MediaNotification(this);
235
    }
236
 
237
    /*
238
     * (non-Javadoc)
239
     * @see android.app.Service#onDestroy()
240
     */
241
    @Override
242
    public void onDestroy() {
243
        LogHelper.d(TAG, "onDestroy");
244
 
245
        // Service is being killed, so make sure we release our resources
246
        handleStopRequest(null);
247
 
248
        mDelayedStopHandler.removeCallbacksAndMessages(null);
249
        // In particular, always release the MediaSession to clean up resources
250
        // and notify associated MediaController(s).
251
        mSession.release();
252
    }
253
 
254
 
255
    @Override
256
    public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
257
        LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName,
258
                "; clientUid=" + clientUid + " ; rootHints=", rootHints);
259
        // To ensure you are not allowing any arbitrary app to browse your app's contents, you
260
        // need to check the origin:
261
        if (!PackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
262
            // If the request comes from an untrusted package, return null. No further calls will
263
            // be made to other media browsing methods.
264
            LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package "
265
                    + clientPackageName);
266
            return null;
267
        }
268
        if (ANDROID_AUTO_PACKAGE_NAME.equals(clientPackageName)) {
269
            // Optional: if your app needs to adapt ads, music library or anything else that
270
            // needs to run differently when connected to the car, this is where you should handle
271
            // it.
272
        }
273
        return new BrowserRoot(MEDIA_ID_ROOT, null);
274
    }
275
 
276
    @Override
277
    public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
278
        if (!mMusicProvider.isInitialized()) {
279
            // Use result.detach to allow calling result.sendResult from another thread:
280
            result.detach();
281
 
282
            mMusicProvider.retrieveMedia(new MusicProvider.Callback() {
283
                @Override
284
                public void onMusicCatalogReady(boolean success) {
285
                    if (success) {
286
                        loadChildrenImpl(parentMediaId, result);
287
                    } else {
288
                        updatePlaybackState(getString(R.string.error_no_metadata));
289
                        result.sendResult(new ArrayList<MediaItem>());
290
                    }
291
                }
292
            });
293
 
294
        } else {
295
            // If our music catalog is already loaded/cached, load them into result immediately
296
            loadChildrenImpl(parentMediaId, result);
297
        }
298
    }
299
 
300
    /**
301
     * Actual implementation of onLoadChildren that assumes that MusicProvider is already
302
     * initialized.
303
     */
304
    private void loadChildrenImpl(final String parentMediaId,
305
                                  final Result<List<MediaBrowser.MediaItem>> result) {
306
        LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
307
 
308
        List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>();
309
 
310
        if (MEDIA_ID_ROOT.equals(parentMediaId)) {
311
            LogHelper.d(TAG, "OnLoadChildren.ROOT");
312
            mediaItems.add(new MediaBrowser.MediaItem(
313
                    new MediaDescription.Builder()
314
                        .setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
315
                        .setTitle(getString(R.string.browse_genres))
316
                        .setIconUri(Uri.parse("android.resource://" +
317
                                "com.example.android.mediabrowserservice/drawable/ic_by_genre"))
318
                        .setSubtitle(getString(R.string.browse_genre_subtitle))
319
                        .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE
320
            ));
321
 
322
        } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) {
323
            LogHelper.d(TAG, "OnLoadChildren.GENRES");
324
            for (String genre: mMusicProvider.getGenres()) {
325
                MediaBrowser.MediaItem item = new MediaBrowser.MediaItem(
326
                    new MediaDescription.Builder()
327
                        .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre))
328
                        .setTitle(genre)
329
                        .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre))
330
                        .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE
331
                );
332
                mediaItems.add(item);
333
            }
334
 
335
        } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) {
336
            String genre = extractBrowseCategoryFromMediaID(parentMediaId)[1];
337
            LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE  genre=", genre);
338
            for (MediaMetadata track: mMusicProvider.getMusicsByGenre(genre)) {
339
                // Since mediaMetadata fields are immutable, we need to create a copy, so we
340
                // can set a hierarchy-aware mediaID. We will need to know the media hierarchy
341
                // when we get a onPlayFromMusicID call, so we can create the proper queue based
342
                // on where the music was selected from (by artist, by genre, random, etc)
343
                String hierarchyAwareMediaID = MediaIDHelper.createTrackMediaID(
344
                        MEDIA_ID_MUSICS_BY_GENRE, genre, track);
345
                MediaMetadata trackCopy = new MediaMetadata.Builder(track)
346
                        .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
347
                        .build();
348
                MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem(
349
                        trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE);
350
                mediaItems.add(bItem);
351
            }
352
        } else {
353
            LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId);
354
        }
355
        result.sendResult(mediaItems);
356
    }
357
 
358
 
359
 
360
    private final class MediaSessionCallback extends MediaSession.Callback {
361
        @Override
362
        public void onPlay() {
363
            LogHelper.d(TAG, "play");
364
 
365
            if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
366
                mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
367
                mSession.setQueue(mPlayingQueue);
368
                mSession.setQueueTitle(getString(R.string.random_queue_title));
369
                // start playing from the beginning of the queue
370
                mCurrentIndexOnQueue = 0;
371
            }
372
 
373
            if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
374
                handlePlayRequest();
375
            }
376
        }
377
 
378
        @Override
379
        public void onSkipToQueueItem(long queueId) {
380
            LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
381
 
382
            if (mState == PlaybackState.STATE_PAUSED) {
383
                mState = PlaybackState.STATE_STOPPED;
384
            }
385
 
386
            if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
387
 
388
                // set the current index on queue from the music Id:
389
                mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
390
 
391
                // play the music
392
                handlePlayRequest();
393
            }
394
        }
395
 
396
        @Override
397
        public void onPlayFromMediaId(String mediaId, Bundle extras) {
398
            LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, "  extras=", extras);
399
 
400
            if (mState == PlaybackState.STATE_PAUSED) {
401
                mState = PlaybackState.STATE_STOPPED;
402
            }
403
 
404
            // The mediaId used here is not the unique musicId. This one comes from the
405
            // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
406
            // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
407
            // so we can build the correct playing queue, based on where the track was
408
            // selected from.
409
            mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider);
410
            mSession.setQueue(mPlayingQueue);
411
            String queueTitle = getString(R.string.browse_musics_by_genre_subtitle,
412
                    MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId));
413
            mSession.setQueueTitle(queueTitle);
414
 
415
            if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
416
                String uniqueMusicID = MediaIDHelper.extractMusicIDFromMediaID(mediaId);
417
 
418
                // set the current index on queue from the music Id:
419
                mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(
420
                        mPlayingQueue, uniqueMusicID);
421
 
422
                // play the music
423
                handlePlayRequest();
424
            }
425
        }
426
 
427
        @Override
428
        public void onPause() {
429
            LogHelper.d(TAG, "pause. current state=" + mState);
430
            handlePauseRequest();
431
        }
432
 
433
        @Override
434
        public void onStop() {
435
            LogHelper.d(TAG, "stop. current state=" + mState);
436
            handleStopRequest(null);
437
        }
438
 
439
        @Override
440
        public void onSkipToNext() {
441
            LogHelper.d(TAG, "skipToNext");
442
            mCurrentIndexOnQueue++;
443
            if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
444
                mCurrentIndexOnQueue = 0;
445
            }
446
            if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
447
                mState = PlaybackState.STATE_STOPPED;
448
                handlePlayRequest();
449
            } else {
450
                LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" +
451
                        mCurrentIndexOnQueue + " queue length=" +
452
                        (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
453
                handleStopRequest("Cannot skip");
454
            }
455
        }
456
 
457
        @Override
458
        public void onSkipToPrevious() {
459
            LogHelper.d(TAG, "skipToPrevious");
460
            mCurrentIndexOnQueue--;
461
            if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) {
462
                // This sample's behavior: skipping to previous when in first song restarts the
463
                // first song.
464
                mCurrentIndexOnQueue = 0;
465
            }
466
            if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
467
                mState = PlaybackState.STATE_STOPPED;
468
                handlePlayRequest();
469
            } else {
470
                LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" +
471
                        mCurrentIndexOnQueue + " queue length=" +
472
                        (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
473
                handleStopRequest("Cannot skip");
474
            }
475
        }
476
 
477
        @Override
478
        public void onCustomAction(String action, Bundle extras) {
479
            if (CUSTOM_ACTION_THUMBS_UP.equals(action)) {
480
                LogHelper.i(TAG, "onCustomAction: favorite for current track");
481
                MediaMetadata track = getCurrentPlayingMusic();
482
                if (track != null) {
483
                    String mediaId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
484
                    mMusicProvider.setFavorite(mediaId, !mMusicProvider.isFavorite(mediaId));
485
                }
486
                updatePlaybackState(null);
487
            } else {
488
                LogHelper.e(TAG, "Unsupported action: ", action);
489
            }
490
 
491
        }
492
 
493
        @Override
494
        public void onPlayFromSearch(String query, Bundle extras) {
495
            LogHelper.d(TAG, "playFromSearch  query=", query);
496
 
497
            if (mState == PlaybackState.STATE_PAUSED) {
498
                mState = PlaybackState.STATE_STOPPED;
499
            }
500
 
501
            mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider);
502
            LogHelper.d(TAG, "playFromSearch  playqueue.length=" + mPlayingQueue.size());
503
            mSession.setQueue(mPlayingQueue);
504
 
505
            if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
506
                // start playing from the beginning of the queue
507
                mCurrentIndexOnQueue = 0;
508
 
509
                handlePlayRequest();
510
            }
511
        }
512
    }
513
 
514
 
515
 
516
    /*
517
     * Called when media player is done playing current song.
518
     * @see android.media.MediaPlayer.OnCompletionListener
519
     */
520
    @Override
521
    public void onCompletion(MediaPlayer player) {
522
        LogHelper.d(TAG, "onCompletion from MediaPlayer");
523
        // The media player finished playing the current song, so we go ahead
524
        // and start the next.
525
        if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
526
            // In this sample, we restart the playing queue when it gets to the end:
527
            mCurrentIndexOnQueue++;
528
            if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
529
                mCurrentIndexOnQueue = 0;
530
            }
531
            handlePlayRequest();
532
        } else {
533
            // If there is nothing to play, we stop and release the resources:
534
            handleStopRequest(null);
535
        }
536
    }
537
 
538
    /*
539
     * Called when media player is done preparing.
540
     * @see android.media.MediaPlayer.OnPreparedListener
541
     */
542
    @Override
543
    public void onPrepared(MediaPlayer player) {
544
        LogHelper.d(TAG, "onPrepared from MediaPlayer");
545
        // The media player is done preparing. That means we can start playing if we
546
        // have audio focus.
547
        configMediaPlayerState();
548
    }
549
 
550
    /**
551
     * Called when there's an error playing media. When this happens, the media
552
     * player goes to the Error state. We warn the user about the error and
553
     * reset the media player.
554
     *
555
     * @see android.media.MediaPlayer.OnErrorListener
556
     */
557
    @Override
558
    public boolean onError(MediaPlayer mp, int what, int extra) {
559
        LogHelper.e(TAG, "Media player error: what=" + what + ", extra=" + extra);
560
        handleStopRequest("MediaPlayer error " + what + " (" + extra + ")");
561
        return true; // true indicates we handled the error
562
    }
563
 
564
 
565
 
566
 
567
    /**
568
     * Called by AudioManager on audio focus changes.
569
     */
570
    @Override
571
    public void onAudioFocusChange(int focusChange) {
572
        LogHelper.d(TAG, "onAudioFocusChange. focusChange=" + focusChange);
573
        if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
574
            // We have gained focus:
575
            mAudioFocus = AudioFocus.Focused;
576
 
577
        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
578
                focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
579
                focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
580
            // We have lost focus. If we can duck (low playback volume), we can keep playing.
581
            // Otherwise, we need to pause the playback.
582
            boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
583
            mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck;
584
 
585
            // If we are playing, we need to reset media player by calling configMediaPlayerState
586
            // with mAudioFocus properly set.
587
            if (mState == PlaybackState.STATE_PLAYING && !canDuck) {
588
                // If we don't have audio focus and can't duck, we save the information that
589
                // we were playing, so that we can resume playback once we get the focus back.
590
                mPlayOnFocusGain = true;
591
            }
592
        } else {
593
            LogHelper.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange);
594
        }
595
 
596
        configMediaPlayerState();
597
    }
598
 
599
 
600
 
601
    /**
602
     * Handle a request to play music
603
     */
604
    private void handlePlayRequest() {
605
        LogHelper.d(TAG, "handlePlayRequest: mState=" + mState);
606
 
607
        mDelayedStopHandler.removeCallbacksAndMessages(null);
608
        if (!mServiceStarted) {
609
            LogHelper.v(TAG, "Starting service");
610
            // The MusicService needs to keep running even after the calling MediaBrowser
611
            // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
612
            // need to play media.
613
            startService(new Intent(getApplicationContext(), MusicService.class));
614
            mServiceStarted = true;
615
        }
616
 
617
        mPlayOnFocusGain = true;
618
        tryToGetAudioFocus();
619
 
620
        if (!mSession.isActive()) {
621
            mSession.setActive(true);
622
        }
623
 
624
        // actually play the song
625
        if (mState == PlaybackState.STATE_PAUSED) {
626
            // If we're paused, just continue playback and restore the
627
            // 'foreground service' state.
628
            configMediaPlayerState();
629
        } else {
630
            // If we're stopped or playing a song,
631
            // just go ahead to the new song and (re)start playing
632
            playCurrentSong();
633
        }
634
    }
635
 
636
 
637
    /**
638
     * Handle a request to pause music
639
     */
640
    private void handlePauseRequest() {
641
        LogHelper.d(TAG, "handlePauseRequest: mState=" + mState);
642
 
643
        if (mState == PlaybackState.STATE_PLAYING) {
644
            // Pause media player and cancel the 'foreground service' state.
645
            mState = PlaybackState.STATE_PAUSED;
646
            if (mMediaPlayer.isPlaying()) {
647
                mMediaPlayer.pause();
648
            }
649
            // while paused, retain the MediaPlayer but give up audio focus
650
            relaxResources(false);
651
            giveUpAudioFocus();
652
        }
653
        updatePlaybackState(null);
654
    }
655
 
656
    /**
657
     * Handle a request to stop music
658
     */
659
    private void handleStopRequest(String withError) {
660
        LogHelper.d(TAG, "handleStopRequest: mState=" + mState + " error=", withError);
661
        mState = PlaybackState.STATE_STOPPED;
662
 
663
        // let go of all resources...
664
        relaxResources(true);
665
        giveUpAudioFocus();
666
        updatePlaybackState(withError);
667
 
668
        mMediaNotification.stopNotification();
669
 
670
        // service is no longer necessary. Will be started again if needed.
671
        stopSelf();
672
        mServiceStarted = false;
673
    }
674
 
675
    /**
676
     * Releases resources used by the service for playback. This includes the
677
     * "foreground service" status, the wake locks and possibly the MediaPlayer.
678
     *
679
     * @param releaseMediaPlayer Indicates whether the Media Player should also
680
     *            be released or not
681
     */
682
    private void relaxResources(boolean releaseMediaPlayer) {
683
        LogHelper.d(TAG, "relaxResources. releaseMediaPlayer=" + releaseMediaPlayer);
684
        // stop being a foreground service
685
        stopForeground(true);
686
 
687
        // reset the delayed stop handler.
688
        mDelayedStopHandler.removeCallbacksAndMessages(null);
689
        mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
690
 
691
        // stop and release the Media Player, if it's available
692
        if (releaseMediaPlayer && mMediaPlayer != null) {
693
            mMediaPlayer.reset();
694
            mMediaPlayer.release();
695
            mMediaPlayer = null;
696
        }
697
 
698
        // we can also release the Wifi lock, if we're holding it
699
        if (mWifiLock.isHeld()) {
700
            mWifiLock.release();
701
        }
702
    }
703
 
704
    /**
705
     * Reconfigures MediaPlayer according to audio focus settings and
706
     * starts/restarts it. This method starts/restarts the MediaPlayer
707
     * respecting the current audio focus state. So if we have focus, it will
708
     * play normally; if we don't have focus, it will either leave the
709
     * MediaPlayer paused or set it to a low volume, depending on what is
710
     * allowed by the current focus settings. This method assumes mPlayer !=
711
     * null, so if you are calling it, you have to do so from a context where
712
     * you are sure this is the case.
713
     */
714
    private void configMediaPlayerState() {
715
        LogHelper.d(TAG, "configAndStartMediaPlayer. mAudioFocus=" + mAudioFocus);
716
        if (mAudioFocus == AudioFocus.NoFocusNoDuck) {
717
            // If we don't have audio focus and can't duck, we have to pause,
718
            if (mState == PlaybackState.STATE_PLAYING) {
719
                handlePauseRequest();
720
            }
721
        } else {  // we have audio focus:
722
            if (mAudioFocus == AudioFocus.NoFocusCanDuck) {
723
                mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
724
            } else {
725
                mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
726
            }
727
            // If we were playing when we lost focus, we need to resume playing.
728
            if (mPlayOnFocusGain) {
729
                if (!mMediaPlayer.isPlaying()) {
730
                    LogHelper.d(TAG, "configAndStartMediaPlayer startMediaPlayer.");
731
                    mMediaPlayer.start();
732
                }
733
                mPlayOnFocusGain = false;
734
                mState = PlaybackState.STATE_PLAYING;
735
            }
736
        }
737
        updatePlaybackState(null);
738
    }
739
 
740
    /**
741
     * Makes sure the media player exists and has been reset. This will create
742
     * the media player if needed, or reset the existing media player if one
743
     * already exists.
744
     */
745
    private void createMediaPlayerIfNeeded() {
746
        LogHelper.d(TAG, "createMediaPlayerIfNeeded. needed? " + (mMediaPlayer==null));
747
        if (mMediaPlayer == null) {
748
            mMediaPlayer = new MediaPlayer();
749
 
750
            // Make sure the media player will acquire a wake-lock while
751
            // playing. If we don't do that, the CPU might go to sleep while the
752
            // song is playing, causing playback to stop.
753
            mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
754
 
755
            // we want the media player to notify us when it's ready preparing,
756
            // and when it's done playing:
757
            mMediaPlayer.setOnPreparedListener(this);
758
            mMediaPlayer.setOnCompletionListener(this);
759
            mMediaPlayer.setOnErrorListener(this);
760
        } else {
761
            mMediaPlayer.reset();
762
        }
763
    }
764
 
765
    /**
766
     * Starts playing the current song in the playing queue.
767
     */
768
    void playCurrentSong() {
769
        MediaMetadata track = getCurrentPlayingMusic();
770
        if (track == null) {
771
            LogHelper.e(TAG, "playSong:  ignoring request to play next song, because cannot" +
772
                    " find it." +
773
                    " currentIndex=" + mCurrentIndexOnQueue +
774
                    " playQueue.size=" + (mPlayingQueue==null?"null": mPlayingQueue.size()));
775
            return;
776
        }
777
        String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE);
778
        LogHelper.d(TAG, "playSong:  current (" + mCurrentIndexOnQueue + ") in playingQueue. " +
779
                " musicId=" + track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) +
780
                " source=" + source);
781
 
782
        mState = PlaybackState.STATE_STOPPED;
783
        relaxResources(false); // release everything except MediaPlayer
784
 
785
        try {
786
            createMediaPlayerIfNeeded();
787
 
788
            mState = PlaybackState.STATE_BUFFERING;
789
 
790
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
791
            mMediaPlayer.setDataSource(source);
792
 
793
            // Starts preparing the media player in the background. When
794
            // it's done, it will call our OnPreparedListener (that is,
795
            // the onPrepared() method on this class, since we set the
796
            // listener to 'this'). Until the media player is prepared,
797
            // we *cannot* call start() on it!
798
            mMediaPlayer.prepareAsync();
799
 
800
            // If we are streaming from the internet, we want to hold a
801
            // Wifi lock, which prevents the Wifi radio from going to
802
            // sleep while the song is playing.
803
            mWifiLock.acquire();
804
 
805
            updatePlaybackState(null);
806
            updateMetadata();
807
 
808
        } catch (IOException ex) {
809
            LogHelper.e(TAG, ex, "IOException playing song");
810
            updatePlaybackState(ex.getMessage());
811
        }
812
    }
813
 
814
 
815
 
816
    private void updateMetadata() {
817
        if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
818
            LogHelper.e(TAG, "Can't retrieve current metadata.");
819
            mState = PlaybackState.STATE_ERROR;
820
            updatePlaybackState(getResources().getString(R.string.error_no_metadata));
821
            return;
822
        }
823
        MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
824
        String mediaId = queueItem.getDescription().getMediaId();
825
        MediaMetadata track = mMusicProvider.getMusic(mediaId);
826
        String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
827
        if (!mediaId.equals(trackId)) {
828
            throw new IllegalStateException("track ID (" + trackId + ") " +
829
                    "should match mediaId (" + mediaId + ")");
830
        }
831
        LogHelper.d(TAG, "Updating metadata for MusicID= " + mediaId);
832
        mSession.setMetadata(track);
833
    }
834
 
835
 
836
    /**
837
     * Update the current media player state, optionally showing an error message.
838
     *
839
     * @param error if not null, error message to present to the user.
840
     *
841
     */
842
    private void updatePlaybackState(String error) {
843
 
844
        LogHelper.d(TAG, "updatePlaybackState, setting session playback state to " + mState);
845
        long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
846
        if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
847
            position = mMediaPlayer.getCurrentPosition();
848
        }
849
        PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
850
                .setActions(getAvailableActions());
851
 
852
        setCustomAction(stateBuilder);
853
 
854
        // If there is an error message, send it to the playback state:
855
        if (error != null) {
856
            // Error states are really only supposed to be used for errors that cause playback to
857
            // stop unexpectedly and persist until the user takes action to fix it.
858
            stateBuilder.setErrorMessage(error);
859
            mState = PlaybackState.STATE_ERROR;
860
        }
861
        stateBuilder.setState(mState, position, 1.0f, SystemClock.elapsedRealtime());
862
 
863
        // Set the activeQueueItemId if the current index is valid.
864
        if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
865
            MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
866
            stateBuilder.setActiveQueueItemId(item.getQueueId());
867
        }
868
 
869
        mSession.setPlaybackState(stateBuilder.build());
870
 
871
        if (mState == PlaybackState.STATE_PLAYING || mState == PlaybackState.STATE_PAUSED) {
872
            mMediaNotification.startNotification();
873
        }
874
    }
875
 
876
    private void setCustomAction(PlaybackState.Builder stateBuilder) {
877
        MediaMetadata currentMusic = getCurrentPlayingMusic();
878
        if (currentMusic != null) {
879
            // Set appropriate "Favorite" icon on Custom action:
880
            String mediaId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
881
            int favoriteIcon = R.drawable.ic_star_off;
882
            if (mMusicProvider.isFavorite(mediaId)) {
883
                favoriteIcon = R.drawable.ic_star_on;
884
            }
885
            LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ",
886
                    mediaId, " current favorite=", mMusicProvider.isFavorite(mediaId));
887
            stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite),
888
                    favoriteIcon);
889
        }
890
    }
891
 
892
    private long getAvailableActions() {
893
        long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
894
                PlaybackState.ACTION_PLAY_FROM_SEARCH;
895
        if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
896
            return actions;
897
        }
898
        if (mState == PlaybackState.STATE_PLAYING) {
899
            actions |= PlaybackState.ACTION_PAUSE;
900
        }
901
        if (mCurrentIndexOnQueue > 0) {
902
            actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
903
        }
904
        if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) {
905
            actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
906
        }
907
        return actions;
908
    }
909
 
910
    private MediaMetadata getCurrentPlayingMusic() {
911
        if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
912
            MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
913
            if (item != null) {
914
                LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=",
915
                        item.getDescription().getMediaId());
916
                return mMusicProvider.getMusic(item.getDescription().getMediaId());
917
            }
918
        }
919
        return null;
920
    }
921
 
922
    /**
923
     * Try to get the system audio focus.
924
     */
925
    void tryToGetAudioFocus() {
926
        LogHelper.d(TAG, "tryToGetAudioFocus");
927
        if (mAudioFocus != AudioFocus.Focused) {
928
            int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
929
                    AudioManager.AUDIOFOCUS_GAIN);
930
            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
931
                mAudioFocus = AudioFocus.Focused;
932
            }
933
        }
934
    }
935
 
936
    /**
937
     * Give up the audio focus.
938
     */
939
    void giveUpAudioFocus() {
940
        LogHelper.d(TAG, "giveUpAudioFocus");
941
        if (mAudioFocus == AudioFocus.Focused) {