1*90c8c64dSAndroid Build Coastguard Worker  /*
2*90c8c64dSAndroid Build Coastguard Worker  * Copyright (C) 2014 The Android Open Source Project
3*90c8c64dSAndroid Build Coastguard Worker  *
4*90c8c64dSAndroid Build Coastguard Worker  * Licensed under the Apache License, Version 2.0 (the "License");
5*90c8c64dSAndroid Build Coastguard Worker  * you may not use this file except in compliance with the License.
6*90c8c64dSAndroid Build Coastguard Worker  * You may obtain a copy of the License at
7*90c8c64dSAndroid Build Coastguard Worker  *
8*90c8c64dSAndroid Build Coastguard Worker  *      http://www.apache.org/licenses/LICENSE-2.0
9*90c8c64dSAndroid Build Coastguard Worker  *
10*90c8c64dSAndroid Build Coastguard Worker  * Unless required by applicable law or agreed to in writing, software
11*90c8c64dSAndroid Build Coastguard Worker  * distributed under the License is distributed on an "AS IS" BASIS,
12*90c8c64dSAndroid Build Coastguard Worker  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*90c8c64dSAndroid Build Coastguard Worker  * See the License for the specific language governing permissions and
14*90c8c64dSAndroid Build Coastguard Worker  * limitations under the License.
15*90c8c64dSAndroid Build Coastguard Worker  */
16*90c8c64dSAndroid Build Coastguard Worker 
17*90c8c64dSAndroid Build Coastguard Worker package com.example.android.mediabrowserservice;
18*90c8c64dSAndroid Build Coastguard Worker 
19*90c8c64dSAndroid Build Coastguard Worker  import android.app.PendingIntent;
20*90c8c64dSAndroid Build Coastguard Worker  import android.content.Context;
21*90c8c64dSAndroid Build Coastguard Worker  import android.content.Intent;
22*90c8c64dSAndroid Build Coastguard Worker  import android.graphics.Bitmap;
23*90c8c64dSAndroid Build Coastguard Worker  import android.media.MediaDescription;
24*90c8c64dSAndroid Build Coastguard Worker  import android.media.MediaMetadata;
25*90c8c64dSAndroid Build Coastguard Worker  import android.media.browse.MediaBrowser.MediaItem;
26*90c8c64dSAndroid Build Coastguard Worker  import android.media.session.MediaSession;
27*90c8c64dSAndroid Build Coastguard Worker  import android.media.session.PlaybackState;
28*90c8c64dSAndroid Build Coastguard Worker  import android.net.Uri;
29*90c8c64dSAndroid Build Coastguard Worker  import android.os.Bundle;
30*90c8c64dSAndroid Build Coastguard Worker  import android.os.Handler;
31*90c8c64dSAndroid Build Coastguard Worker  import android.os.Message;
32*90c8c64dSAndroid Build Coastguard Worker  import android.os.SystemClock;
33*90c8c64dSAndroid Build Coastguard Worker  import android.service.media.MediaBrowserService;
34*90c8c64dSAndroid Build Coastguard Worker  import android.text.TextUtils;
35*90c8c64dSAndroid Build Coastguard Worker 
36*90c8c64dSAndroid Build Coastguard Worker  import com.example.android.mediabrowserservice.model.MusicProvider;
37*90c8c64dSAndroid Build Coastguard Worker  import com.example.android.mediabrowserservice.utils.CarHelper;
38*90c8c64dSAndroid Build Coastguard Worker  import com.example.android.mediabrowserservice.utils.LogHelper;
39*90c8c64dSAndroid Build Coastguard Worker  import com.example.android.mediabrowserservice.utils.MediaIDHelper;
40*90c8c64dSAndroid Build Coastguard Worker  import com.example.android.mediabrowserservice.utils.QueueHelper;
41*90c8c64dSAndroid Build Coastguard Worker 
42*90c8c64dSAndroid Build Coastguard Worker  import java.lang.ref.WeakReference;
43*90c8c64dSAndroid Build Coastguard Worker  import java.util.ArrayList;
44*90c8c64dSAndroid Build Coastguard Worker  import java.util.Collections;
45*90c8c64dSAndroid Build Coastguard Worker  import java.util.List;
46*90c8c64dSAndroid Build Coastguard Worker 
47*90c8c64dSAndroid Build Coastguard Worker  import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE;
48*90c8c64dSAndroid Build Coastguard Worker  import static com.example.android.mediabrowserservice.utils.MediaIDHelper.MEDIA_ID_ROOT;
49*90c8c64dSAndroid Build Coastguard Worker  import static com.example.android.mediabrowserservice.utils.MediaIDHelper.createBrowseCategoryMediaID;
50*90c8c64dSAndroid Build Coastguard Worker 
51*90c8c64dSAndroid Build Coastguard Worker  /**
52*90c8c64dSAndroid Build Coastguard Worker   * This class provides a MediaBrowser through a service. It exposes the media library to a browsing
53*90c8c64dSAndroid Build Coastguard Worker   * client, through the onGetRoot and onLoadChildren methods. It also creates a MediaSession and
54*90c8c64dSAndroid Build Coastguard Worker   * exposes it through its MediaSession.Token, which allows the client to create a MediaController
55*90c8c64dSAndroid Build Coastguard Worker   * that connects to and send control commands to the MediaSession remotely. This is useful for
56*90c8c64dSAndroid Build Coastguard Worker   * user interfaces that need to interact with your media session, like Android Auto. You can
57*90c8c64dSAndroid Build Coastguard Worker   * (should) also use the same service from your app's UI, which gives a seamless playback
58*90c8c64dSAndroid Build Coastguard Worker   * experience to the user.
59*90c8c64dSAndroid Build Coastguard Worker   *
60*90c8c64dSAndroid Build Coastguard Worker   * To implement a MediaBrowserService, you need to:
61*90c8c64dSAndroid Build Coastguard Worker   *
62*90c8c64dSAndroid Build Coastguard Worker   * <ul>
63*90c8c64dSAndroid Build Coastguard Worker   *
64*90c8c64dSAndroid Build Coastguard Worker   * <li> Extend {@link android.service.media.MediaBrowserService}, implementing the media browsing
65*90c8c64dSAndroid Build Coastguard Worker   *      related methods {@link android.service.media.MediaBrowserService#onGetRoot} and
66*90c8c64dSAndroid Build Coastguard Worker   *      {@link android.service.media.MediaBrowserService#onLoadChildren};
67*90c8c64dSAndroid Build Coastguard Worker   * <li> In onCreate, start a new {@link android.media.session.MediaSession} and notify its parent
68*90c8c64dSAndroid Build Coastguard Worker   *      with the session's token {@link android.service.media.MediaBrowserService#setSessionToken};
69*90c8c64dSAndroid Build Coastguard Worker   *
70*90c8c64dSAndroid Build Coastguard Worker   * <li> Set a callback on the
71*90c8c64dSAndroid Build Coastguard Worker   *      {@link android.media.session.MediaSession#setCallback(android.media.session.MediaSession.Callback)}.
72*90c8c64dSAndroid Build Coastguard Worker   *      The callback will receive all the user's actions, like play, pause, etc;
73*90c8c64dSAndroid Build Coastguard Worker   *
74*90c8c64dSAndroid Build Coastguard Worker   * <li> Handle all the actual music playing using any method your app prefers (for example,
75*90c8c64dSAndroid Build Coastguard Worker   *      {@link android.media.MediaPlayer})
76*90c8c64dSAndroid Build Coastguard Worker   *
77*90c8c64dSAndroid Build Coastguard Worker   * <li> Update playbackState, "now playing" metadata and queue, using MediaSession proper methods
78*90c8c64dSAndroid Build Coastguard Worker   *      {@link android.media.session.MediaSession#setPlaybackState(android.media.session.PlaybackState)}
79*90c8c64dSAndroid Build Coastguard Worker   *      {@link android.media.session.MediaSession#setMetadata(android.media.MediaMetadata)} and
80*90c8c64dSAndroid Build Coastguard Worker   *      {@link android.media.session.MediaSession#setQueue(java.util.List)})
81*90c8c64dSAndroid Build Coastguard Worker   *
82*90c8c64dSAndroid Build Coastguard Worker   * <li> Declare and export the service in AndroidManifest with an intent receiver for the action
83*90c8c64dSAndroid Build Coastguard Worker   *      android.media.browse.MediaBrowserService
84*90c8c64dSAndroid Build Coastguard Worker   *
85*90c8c64dSAndroid Build Coastguard Worker   * </ul>
86*90c8c64dSAndroid Build Coastguard Worker   *
87*90c8c64dSAndroid Build Coastguard Worker   * To make your app compatible with Android Auto, you also need to:
88*90c8c64dSAndroid Build Coastguard Worker   *
89*90c8c64dSAndroid Build Coastguard Worker   * <ul>
90*90c8c64dSAndroid Build Coastguard Worker   *
91*90c8c64dSAndroid Build Coastguard Worker   * <li> Declare a meta-data tag in AndroidManifest.xml linking to a xml resource
92*90c8c64dSAndroid Build Coastguard Worker   *      with a &lt;automotiveApp&gt; root element. For a media app, this must include
93*90c8c64dSAndroid Build Coastguard Worker   *      an &lt;uses name="media"/&gt; element as a child.
94*90c8c64dSAndroid Build Coastguard Worker   *      For example, in AndroidManifest.xml:
95*90c8c64dSAndroid Build Coastguard Worker   *          &lt;meta-data android:name="com.google.android.gms.car.application"
96*90c8c64dSAndroid Build Coastguard Worker   *              android:resource="@xml/automotive_app_desc"/&gt;
97*90c8c64dSAndroid Build Coastguard Worker   *      And in res/values/automotive_app_desc.xml:
98*90c8c64dSAndroid Build Coastguard Worker   *          &lt;automotiveApp&gt;
99*90c8c64dSAndroid Build Coastguard Worker   *              &lt;uses name="media"/&gt;
100*90c8c64dSAndroid Build Coastguard Worker   *          &lt;/automotiveApp&gt;
101*90c8c64dSAndroid Build Coastguard Worker   *
102*90c8c64dSAndroid Build Coastguard Worker   * </ul>
103*90c8c64dSAndroid Build Coastguard Worker 
104*90c8c64dSAndroid Build Coastguard Worker   * @see <a href="README.md">README.md</a> for more details.
105*90c8c64dSAndroid Build Coastguard Worker   *
106*90c8c64dSAndroid Build Coastguard Worker   */
107*90c8c64dSAndroid Build Coastguard Worker 
108*90c8c64dSAndroid Build Coastguard Worker  public class MusicService extends MediaBrowserService implements Playback.Callback {
109*90c8c64dSAndroid Build Coastguard Worker 
110*90c8c64dSAndroid Build Coastguard Worker      // The action of the incoming Intent indicating that it contains a command
111*90c8c64dSAndroid Build Coastguard Worker      // to be executed (see {@link #onStartCommand})
112*90c8c64dSAndroid Build Coastguard Worker      public static final String ACTION_CMD = "com.example.android.mediabrowserservice.ACTION_CMD";
113*90c8c64dSAndroid Build Coastguard Worker      // The key in the extras of the incoming Intent indicating the command that
114*90c8c64dSAndroid Build Coastguard Worker      // should be executed (see {@link #onStartCommand})
115*90c8c64dSAndroid Build Coastguard Worker      public static final String CMD_NAME = "CMD_NAME";
116*90c8c64dSAndroid Build Coastguard Worker      // A value of a CMD_NAME key in the extras of the incoming Intent that
117*90c8c64dSAndroid Build Coastguard Worker      // indicates that the music playback should be paused (see {@link #onStartCommand})
118*90c8c64dSAndroid Build Coastguard Worker      public static final String CMD_PAUSE = "CMD_PAUSE";
119*90c8c64dSAndroid Build Coastguard Worker 
120*90c8c64dSAndroid Build Coastguard Worker      private static final String TAG = LogHelper.makeLogTag(MusicService.class);
121*90c8c64dSAndroid Build Coastguard Worker      // Action to thumbs up a media item
122*90c8c64dSAndroid Build Coastguard Worker      private static final String CUSTOM_ACTION_THUMBS_UP =
123*90c8c64dSAndroid Build Coastguard Worker          "com.example.android.mediabrowserservice.THUMBS_UP";
124*90c8c64dSAndroid Build Coastguard Worker      // Delay stopSelf by using a handler.
125*90c8c64dSAndroid Build Coastguard Worker      private static final int STOP_DELAY = 30000;
126*90c8c64dSAndroid Build Coastguard Worker 
127*90c8c64dSAndroid Build Coastguard Worker      // Music catalog manager
128*90c8c64dSAndroid Build Coastguard Worker      private MusicProvider mMusicProvider;
129*90c8c64dSAndroid Build Coastguard Worker      private MediaSession mSession;
130*90c8c64dSAndroid Build Coastguard Worker      // "Now playing" queue:
131*90c8c64dSAndroid Build Coastguard Worker      private List<MediaSession.QueueItem> mPlayingQueue;
132*90c8c64dSAndroid Build Coastguard Worker      private int mCurrentIndexOnQueue;
133*90c8c64dSAndroid Build Coastguard Worker      private MediaNotificationManager mMediaNotificationManager;
134*90c8c64dSAndroid Build Coastguard Worker      // Indicates whether the service was started.
135*90c8c64dSAndroid Build Coastguard Worker      private boolean mServiceStarted;
136*90c8c64dSAndroid Build Coastguard Worker      private DelayedStopHandler mDelayedStopHandler = new DelayedStopHandler(this);
137*90c8c64dSAndroid Build Coastguard Worker      private Playback mPlayback;
138*90c8c64dSAndroid Build Coastguard Worker      private PackageValidator mPackageValidator;
139*90c8c64dSAndroid Build Coastguard Worker 
140*90c8c64dSAndroid Build Coastguard Worker      /*
141*90c8c64dSAndroid Build Coastguard Worker       * (non-Javadoc)
142*90c8c64dSAndroid Build Coastguard Worker       * @see android.app.Service#onCreate()
143*90c8c64dSAndroid Build Coastguard Worker       */
144*90c8c64dSAndroid Build Coastguard Worker      @Override
onCreate()145*90c8c64dSAndroid Build Coastguard Worker      public void onCreate() {
146*90c8c64dSAndroid Build Coastguard Worker          super.onCreate();
147*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "onCreate");
148*90c8c64dSAndroid Build Coastguard Worker 
149*90c8c64dSAndroid Build Coastguard Worker          mPlayingQueue = new ArrayList<>();
150*90c8c64dSAndroid Build Coastguard Worker          mMusicProvider = new MusicProvider();
151*90c8c64dSAndroid Build Coastguard Worker          mPackageValidator = new PackageValidator(this);
152*90c8c64dSAndroid Build Coastguard Worker 
153*90c8c64dSAndroid Build Coastguard Worker          // Start a new MediaSession
154*90c8c64dSAndroid Build Coastguard Worker          mSession = new MediaSession(this, "MusicService");
155*90c8c64dSAndroid Build Coastguard Worker          setSessionToken(mSession.getSessionToken());
156*90c8c64dSAndroid Build Coastguard Worker          mSession.setCallback(new MediaSessionCallback());
157*90c8c64dSAndroid Build Coastguard Worker          mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
158*90c8c64dSAndroid Build Coastguard Worker              MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
159*90c8c64dSAndroid Build Coastguard Worker 
160*90c8c64dSAndroid Build Coastguard Worker          mPlayback = new Playback(this, mMusicProvider);
161*90c8c64dSAndroid Build Coastguard Worker          mPlayback.setState(PlaybackState.STATE_NONE);
162*90c8c64dSAndroid Build Coastguard Worker          mPlayback.setCallback(this);
163*90c8c64dSAndroid Build Coastguard Worker          mPlayback.start();
164*90c8c64dSAndroid Build Coastguard Worker 
165*90c8c64dSAndroid Build Coastguard Worker          Context context = getApplicationContext();
166*90c8c64dSAndroid Build Coastguard Worker          Intent intent = new Intent(context, MusicPlayerActivity.class);
167*90c8c64dSAndroid Build Coastguard Worker          PendingIntent pi = PendingIntent.getActivity(context, 99 /*request code*/,
168*90c8c64dSAndroid Build Coastguard Worker                  intent, PendingIntent.FLAG_UPDATE_CURRENT);
169*90c8c64dSAndroid Build Coastguard Worker          mSession.setSessionActivity(pi);
170*90c8c64dSAndroid Build Coastguard Worker 
171*90c8c64dSAndroid Build Coastguard Worker          Bundle extras = new Bundle();
172*90c8c64dSAndroid Build Coastguard Worker          CarHelper.setSlotReservationFlags(extras, true, true, true);
173*90c8c64dSAndroid Build Coastguard Worker          mSession.setExtras(extras);
174*90c8c64dSAndroid Build Coastguard Worker 
175*90c8c64dSAndroid Build Coastguard Worker          updatePlaybackState(null);
176*90c8c64dSAndroid Build Coastguard Worker 
177*90c8c64dSAndroid Build Coastguard Worker          mMediaNotificationManager = new MediaNotificationManager(this);
178*90c8c64dSAndroid Build Coastguard Worker      }
179*90c8c64dSAndroid Build Coastguard Worker 
180*90c8c64dSAndroid Build Coastguard Worker      /**
181*90c8c64dSAndroid Build Coastguard Worker       * (non-Javadoc)
182*90c8c64dSAndroid Build Coastguard Worker       * @see android.app.Service#onStartCommand(android.content.Intent, int, int)
183*90c8c64dSAndroid Build Coastguard Worker       */
184*90c8c64dSAndroid Build Coastguard Worker      @Override
onStartCommand(Intent startIntent, int flags, int startId)185*90c8c64dSAndroid Build Coastguard Worker      public int onStartCommand(Intent startIntent, int flags, int startId) {
186*90c8c64dSAndroid Build Coastguard Worker          if (startIntent != null) {
187*90c8c64dSAndroid Build Coastguard Worker              String action = startIntent.getAction();
188*90c8c64dSAndroid Build Coastguard Worker              String command = startIntent.getStringExtra(CMD_NAME);
189*90c8c64dSAndroid Build Coastguard Worker              if (ACTION_CMD.equals(action)) {
190*90c8c64dSAndroid Build Coastguard Worker                  if (CMD_PAUSE.equals(command)) {
191*90c8c64dSAndroid Build Coastguard Worker                      if (mPlayback != null && mPlayback.isPlaying()) {
192*90c8c64dSAndroid Build Coastguard Worker                          handlePauseRequest();
193*90c8c64dSAndroid Build Coastguard Worker                      }
194*90c8c64dSAndroid Build Coastguard Worker                  }
195*90c8c64dSAndroid Build Coastguard Worker              }
196*90c8c64dSAndroid Build Coastguard Worker          }
197*90c8c64dSAndroid Build Coastguard Worker          return START_STICKY;
198*90c8c64dSAndroid Build Coastguard Worker      }
199*90c8c64dSAndroid Build Coastguard Worker 
200*90c8c64dSAndroid Build Coastguard Worker      /**
201*90c8c64dSAndroid Build Coastguard Worker       * (non-Javadoc)
202*90c8c64dSAndroid Build Coastguard Worker       * @see android.app.Service#onDestroy()
203*90c8c64dSAndroid Build Coastguard Worker       */
204*90c8c64dSAndroid Build Coastguard Worker      @Override
onDestroy()205*90c8c64dSAndroid Build Coastguard Worker      public void onDestroy() {
206*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "onDestroy");
207*90c8c64dSAndroid Build Coastguard Worker          // Service is being killed, so make sure we release our resources
208*90c8c64dSAndroid Build Coastguard Worker          handleStopRequest(null);
209*90c8c64dSAndroid Build Coastguard Worker 
210*90c8c64dSAndroid Build Coastguard Worker          mDelayedStopHandler.removeCallbacksAndMessages(null);
211*90c8c64dSAndroid Build Coastguard Worker          // Always release the MediaSession to clean up resources
212*90c8c64dSAndroid Build Coastguard Worker          // and notify associated MediaController(s).
213*90c8c64dSAndroid Build Coastguard Worker          mSession.release();
214*90c8c64dSAndroid Build Coastguard Worker      }
215*90c8c64dSAndroid Build Coastguard Worker 
216*90c8c64dSAndroid Build Coastguard Worker      @Override
onGetRoot(String clientPackageName, int clientUid, Bundle rootHints)217*90c8c64dSAndroid Build Coastguard Worker      public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
218*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName,
219*90c8c64dSAndroid Build Coastguard Worker                  "; clientUid=" + clientUid + " ; rootHints=", rootHints);
220*90c8c64dSAndroid Build Coastguard Worker          // To ensure you are not allowing any arbitrary app to browse your app's contents, you
221*90c8c64dSAndroid Build Coastguard Worker          // need to check the origin:
222*90c8c64dSAndroid Build Coastguard Worker          if (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) {
223*90c8c64dSAndroid Build Coastguard Worker              // If the request comes from an untrusted package, return null. No further calls will
224*90c8c64dSAndroid Build Coastguard Worker              // be made to other media browsing methods.
225*90c8c64dSAndroid Build Coastguard Worker              LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package "
226*90c8c64dSAndroid Build Coastguard Worker                      + clientPackageName);
227*90c8c64dSAndroid Build Coastguard Worker              return null;
228*90c8c64dSAndroid Build Coastguard Worker          }
229*90c8c64dSAndroid Build Coastguard Worker          //noinspection StatementWithEmptyBody
230*90c8c64dSAndroid Build Coastguard Worker          if (CarHelper.isValidCarPackage(clientPackageName)) {
231*90c8c64dSAndroid Build Coastguard Worker              // Optional: if your app needs to adapt ads, music library or anything else that
232*90c8c64dSAndroid Build Coastguard Worker              // needs to run differently when connected to the car, this is where you should handle
233*90c8c64dSAndroid Build Coastguard Worker              // it.
234*90c8c64dSAndroid Build Coastguard Worker          }
235*90c8c64dSAndroid Build Coastguard Worker          return new BrowserRoot(MEDIA_ID_ROOT, null);
236*90c8c64dSAndroid Build Coastguard Worker      }
237*90c8c64dSAndroid Build Coastguard Worker 
238*90c8c64dSAndroid Build Coastguard Worker      @Override
onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result)239*90c8c64dSAndroid Build Coastguard Worker      public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
240*90c8c64dSAndroid Build Coastguard Worker          if (!mMusicProvider.isInitialized()) {
241*90c8c64dSAndroid Build Coastguard Worker              // Use result.detach to allow calling result.sendResult from another thread:
242*90c8c64dSAndroid Build Coastguard Worker              result.detach();
243*90c8c64dSAndroid Build Coastguard Worker 
244*90c8c64dSAndroid Build Coastguard Worker              mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() {
245*90c8c64dSAndroid Build Coastguard Worker                  @Override
246*90c8c64dSAndroid Build Coastguard Worker                  public void onMusicCatalogReady(boolean success) {
247*90c8c64dSAndroid Build Coastguard Worker                      if (success) {
248*90c8c64dSAndroid Build Coastguard Worker                          loadChildrenImpl(parentMediaId, result);
249*90c8c64dSAndroid Build Coastguard Worker                      } else {
250*90c8c64dSAndroid Build Coastguard Worker                          updatePlaybackState(getString(R.string.error_no_metadata));
251*90c8c64dSAndroid Build Coastguard Worker                          result.sendResult(Collections.<MediaItem>emptyList());
252*90c8c64dSAndroid Build Coastguard Worker                      }
253*90c8c64dSAndroid Build Coastguard Worker                  }
254*90c8c64dSAndroid Build Coastguard Worker              });
255*90c8c64dSAndroid Build Coastguard Worker 
256*90c8c64dSAndroid Build Coastguard Worker          } else {
257*90c8c64dSAndroid Build Coastguard Worker              // If our music catalog is already loaded/cached, load them into result immediately
258*90c8c64dSAndroid Build Coastguard Worker              loadChildrenImpl(parentMediaId, result);
259*90c8c64dSAndroid Build Coastguard Worker          }
260*90c8c64dSAndroid Build Coastguard Worker      }
261*90c8c64dSAndroid Build Coastguard Worker 
262*90c8c64dSAndroid Build Coastguard Worker      /**
263*90c8c64dSAndroid Build Coastguard Worker       * Actual implementation of onLoadChildren that assumes that MusicProvider is already
264*90c8c64dSAndroid Build Coastguard Worker       * initialized.
265*90c8c64dSAndroid Build Coastguard Worker       */
loadChildrenImpl(final String parentMediaId, final Result<List<MediaItem>> result)266*90c8c64dSAndroid Build Coastguard Worker      private void loadChildrenImpl(final String parentMediaId,
267*90c8c64dSAndroid Build Coastguard Worker                                    final Result<List<MediaItem>> result) {
268*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId);
269*90c8c64dSAndroid Build Coastguard Worker 
270*90c8c64dSAndroid Build Coastguard Worker          List<MediaItem> mediaItems = new ArrayList<>();
271*90c8c64dSAndroid Build Coastguard Worker 
272*90c8c64dSAndroid Build Coastguard Worker          if (MEDIA_ID_ROOT.equals(parentMediaId)) {
273*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "OnLoadChildren.ROOT");
274*90c8c64dSAndroid Build Coastguard Worker              mediaItems.add(new MediaItem(
275*90c8c64dSAndroid Build Coastguard Worker                      new MediaDescription.Builder()
276*90c8c64dSAndroid Build Coastguard Worker                          .setMediaId(MEDIA_ID_MUSICS_BY_GENRE)
277*90c8c64dSAndroid Build Coastguard Worker                          .setTitle(getString(R.string.browse_genres))
278*90c8c64dSAndroid Build Coastguard Worker                          .setIconUri(Uri.parse("android.resource://" +
279*90c8c64dSAndroid Build Coastguard Worker                              "com.example.android.mediabrowserservice/drawable/ic_by_genre"))
280*90c8c64dSAndroid Build Coastguard Worker                          .setSubtitle(getString(R.string.browse_genre_subtitle))
281*90c8c64dSAndroid Build Coastguard Worker                          .build(), MediaItem.FLAG_BROWSABLE
282*90c8c64dSAndroid Build Coastguard Worker              ));
283*90c8c64dSAndroid Build Coastguard Worker 
284*90c8c64dSAndroid Build Coastguard Worker          } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) {
285*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "OnLoadChildren.GENRES");
286*90c8c64dSAndroid Build Coastguard Worker              for (String genre : mMusicProvider.getGenres()) {
287*90c8c64dSAndroid Build Coastguard Worker                  MediaItem item = new MediaItem(
288*90c8c64dSAndroid Build Coastguard Worker                      new MediaDescription.Builder()
289*90c8c64dSAndroid Build Coastguard Worker                          .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre))
290*90c8c64dSAndroid Build Coastguard Worker                          .setTitle(genre)
291*90c8c64dSAndroid Build Coastguard Worker                          .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre))
292*90c8c64dSAndroid Build Coastguard Worker                          .build(), MediaItem.FLAG_BROWSABLE
293*90c8c64dSAndroid Build Coastguard Worker                  );
294*90c8c64dSAndroid Build Coastguard Worker                  mediaItems.add(item);
295*90c8c64dSAndroid Build Coastguard Worker              }
296*90c8c64dSAndroid Build Coastguard Worker 
297*90c8c64dSAndroid Build Coastguard Worker          } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) {
298*90c8c64dSAndroid Build Coastguard Worker              String genre = MediaIDHelper.getHierarchy(parentMediaId)[1];
299*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE  genre=", genre);
300*90c8c64dSAndroid Build Coastguard Worker              for (MediaMetadata track : mMusicProvider.getMusicsByGenre(genre)) {
301*90c8c64dSAndroid Build Coastguard Worker                  // Since mediaMetadata fields are immutable, we need to create a copy, so we
302*90c8c64dSAndroid Build Coastguard Worker                  // can set a hierarchy-aware mediaID. We will need to know the media hierarchy
303*90c8c64dSAndroid Build Coastguard Worker                  // when we get a onPlayFromMusicID call, so we can create the proper queue based
304*90c8c64dSAndroid Build Coastguard Worker                  // on where the music was selected from (by artist, by genre, random, etc)
305*90c8c64dSAndroid Build Coastguard Worker                  String hierarchyAwareMediaID = MediaIDHelper.createMediaID(
306*90c8c64dSAndroid Build Coastguard Worker                          track.getDescription().getMediaId(), MEDIA_ID_MUSICS_BY_GENRE, genre);
307*90c8c64dSAndroid Build Coastguard Worker                  MediaMetadata trackCopy = new MediaMetadata.Builder(track)
308*90c8c64dSAndroid Build Coastguard Worker                          .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID)
309*90c8c64dSAndroid Build Coastguard Worker                          .build();
310*90c8c64dSAndroid Build Coastguard Worker                  MediaItem bItem = new MediaItem(
311*90c8c64dSAndroid Build Coastguard Worker                          trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE);
312*90c8c64dSAndroid Build Coastguard Worker                  mediaItems.add(bItem);
313*90c8c64dSAndroid Build Coastguard Worker              }
314*90c8c64dSAndroid Build Coastguard Worker          } else {
315*90c8c64dSAndroid Build Coastguard Worker              LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId);
316*90c8c64dSAndroid Build Coastguard Worker          }
317*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "OnLoadChildren sending ", mediaItems.size(),
318*90c8c64dSAndroid Build Coastguard Worker                  " results for ", parentMediaId);
319*90c8c64dSAndroid Build Coastguard Worker          result.sendResult(mediaItems);
320*90c8c64dSAndroid Build Coastguard Worker      }
321*90c8c64dSAndroid Build Coastguard Worker 
322*90c8c64dSAndroid Build Coastguard Worker      private final class MediaSessionCallback extends MediaSession.Callback {
323*90c8c64dSAndroid Build Coastguard Worker          @Override
onPlay()324*90c8c64dSAndroid Build Coastguard Worker          public void onPlay() {
325*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "play");
326*90c8c64dSAndroid Build Coastguard Worker 
327*90c8c64dSAndroid Build Coastguard Worker              if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
328*90c8c64dSAndroid Build Coastguard Worker                  mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
329*90c8c64dSAndroid Build Coastguard Worker                  mSession.setQueue(mPlayingQueue);
330*90c8c64dSAndroid Build Coastguard Worker                  mSession.setQueueTitle(getString(R.string.random_queue_title));
331*90c8c64dSAndroid Build Coastguard Worker                  // start playing from the beginning of the queue
332*90c8c64dSAndroid Build Coastguard Worker                  mCurrentIndexOnQueue = 0;
333*90c8c64dSAndroid Build Coastguard Worker              }
334*90c8c64dSAndroid Build Coastguard Worker 
335*90c8c64dSAndroid Build Coastguard Worker              if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
336*90c8c64dSAndroid Build Coastguard Worker                  handlePlayRequest();
337*90c8c64dSAndroid Build Coastguard Worker              }
338*90c8c64dSAndroid Build Coastguard Worker          }
339*90c8c64dSAndroid Build Coastguard Worker 
340*90c8c64dSAndroid Build Coastguard Worker          @Override
onSkipToQueueItem(long queueId)341*90c8c64dSAndroid Build Coastguard Worker          public void onSkipToQueueItem(long queueId) {
342*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId);
343*90c8c64dSAndroid Build Coastguard Worker 
344*90c8c64dSAndroid Build Coastguard Worker              if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
345*90c8c64dSAndroid Build Coastguard Worker                  // set the current index on queue from the music Id:
346*90c8c64dSAndroid Build Coastguard Worker                  mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId);
347*90c8c64dSAndroid Build Coastguard Worker                  // play the music
348*90c8c64dSAndroid Build Coastguard Worker                  handlePlayRequest();
349*90c8c64dSAndroid Build Coastguard Worker              }
350*90c8c64dSAndroid Build Coastguard Worker          }
351*90c8c64dSAndroid Build Coastguard Worker 
352*90c8c64dSAndroid Build Coastguard Worker          @Override
onSeekTo(long position)353*90c8c64dSAndroid Build Coastguard Worker          public void onSeekTo(long position) {
354*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "onSeekTo:", position);
355*90c8c64dSAndroid Build Coastguard Worker              mPlayback.seekTo((int) position);
356*90c8c64dSAndroid Build Coastguard Worker          }
357*90c8c64dSAndroid Build Coastguard Worker 
358*90c8c64dSAndroid Build Coastguard Worker          @Override
onPlayFromMediaId(String mediaId, Bundle extras)359*90c8c64dSAndroid Build Coastguard Worker          public void onPlayFromMediaId(String mediaId, Bundle extras) {
360*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, "  extras=", extras);
361*90c8c64dSAndroid Build Coastguard Worker 
362*90c8c64dSAndroid Build Coastguard Worker              // The mediaId used here is not the unique musicId. This one comes from the
363*90c8c64dSAndroid Build Coastguard Worker              // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of
364*90c8c64dSAndroid Build Coastguard Worker              // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary
365*90c8c64dSAndroid Build Coastguard Worker              // so we can build the correct playing queue, based on where the track was
366*90c8c64dSAndroid Build Coastguard Worker              // selected from.
367*90c8c64dSAndroid Build Coastguard Worker              mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider);
368*90c8c64dSAndroid Build Coastguard Worker              mSession.setQueue(mPlayingQueue);
369*90c8c64dSAndroid Build Coastguard Worker              String queueTitle = getString(R.string.browse_musics_by_genre_subtitle,
370*90c8c64dSAndroid Build Coastguard Worker                      MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId));
371*90c8c64dSAndroid Build Coastguard Worker              mSession.setQueueTitle(queueTitle);
372*90c8c64dSAndroid Build Coastguard Worker 
373*90c8c64dSAndroid Build Coastguard Worker              if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
374*90c8c64dSAndroid Build Coastguard Worker                  // set the current index on queue from the media Id:
375*90c8c64dSAndroid Build Coastguard Worker                  mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, mediaId);
376*90c8c64dSAndroid Build Coastguard Worker 
377*90c8c64dSAndroid Build Coastguard Worker                  if (mCurrentIndexOnQueue < 0) {
378*90c8c64dSAndroid Build Coastguard Worker                      LogHelper.e(TAG, "playFromMediaId: media ID ", mediaId,
379*90c8c64dSAndroid Build Coastguard Worker                              " could not be found on queue. Ignoring.");
380*90c8c64dSAndroid Build Coastguard Worker                  } else {
381*90c8c64dSAndroid Build Coastguard Worker                      // play the music
382*90c8c64dSAndroid Build Coastguard Worker                      handlePlayRequest();
383*90c8c64dSAndroid Build Coastguard Worker                  }
384*90c8c64dSAndroid Build Coastguard Worker              }
385*90c8c64dSAndroid Build Coastguard Worker          }
386*90c8c64dSAndroid Build Coastguard Worker 
387*90c8c64dSAndroid Build Coastguard Worker          @Override
onPause()388*90c8c64dSAndroid Build Coastguard Worker          public void onPause() {
389*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "pause. current state=" + mPlayback.getState());
390*90c8c64dSAndroid Build Coastguard Worker              handlePauseRequest();
391*90c8c64dSAndroid Build Coastguard Worker          }
392*90c8c64dSAndroid Build Coastguard Worker 
393*90c8c64dSAndroid Build Coastguard Worker          @Override
onStop()394*90c8c64dSAndroid Build Coastguard Worker          public void onStop() {
395*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "stop. current state=" + mPlayback.getState());
396*90c8c64dSAndroid Build Coastguard Worker              handleStopRequest(null);
397*90c8c64dSAndroid Build Coastguard Worker          }
398*90c8c64dSAndroid Build Coastguard Worker 
399*90c8c64dSAndroid Build Coastguard Worker          @Override
onSkipToNext()400*90c8c64dSAndroid Build Coastguard Worker          public void onSkipToNext() {
401*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "skipToNext");
402*90c8c64dSAndroid Build Coastguard Worker              mCurrentIndexOnQueue++;
403*90c8c64dSAndroid Build Coastguard Worker              if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) {
404*90c8c64dSAndroid Build Coastguard Worker                  // This sample's behavior: skipping to next when in last song returns to the
405*90c8c64dSAndroid Build Coastguard Worker                  // first song.
406*90c8c64dSAndroid Build Coastguard Worker                  mCurrentIndexOnQueue = 0;
407*90c8c64dSAndroid Build Coastguard Worker              }
408*90c8c64dSAndroid Build Coastguard Worker              if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
409*90c8c64dSAndroid Build Coastguard Worker                  handlePlayRequest();
410*90c8c64dSAndroid Build Coastguard Worker              } else {
411*90c8c64dSAndroid Build Coastguard Worker                  LogHelper.e(TAG, "skipToNext: cannot skip to next. next Index=" +
412*90c8c64dSAndroid Build Coastguard Worker                          mCurrentIndexOnQueue + " queue length=" +
413*90c8c64dSAndroid Build Coastguard Worker                          (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
414*90c8c64dSAndroid Build Coastguard Worker                  handleStopRequest("Cannot skip");
415*90c8c64dSAndroid Build Coastguard Worker              }
416*90c8c64dSAndroid Build Coastguard Worker          }
417*90c8c64dSAndroid Build Coastguard Worker 
418*90c8c64dSAndroid Build Coastguard Worker          @Override
onSkipToPrevious()419*90c8c64dSAndroid Build Coastguard Worker          public void onSkipToPrevious() {
420*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "skipToPrevious");
421*90c8c64dSAndroid Build Coastguard Worker              mCurrentIndexOnQueue--;
422*90c8c64dSAndroid Build Coastguard Worker              if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) {
423*90c8c64dSAndroid Build Coastguard Worker                  // This sample's behavior: skipping to previous when in first song restarts the
424*90c8c64dSAndroid Build Coastguard Worker                  // first song.
425*90c8c64dSAndroid Build Coastguard Worker                  mCurrentIndexOnQueue = 0;
426*90c8c64dSAndroid Build Coastguard Worker              }
427*90c8c64dSAndroid Build Coastguard Worker              if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
428*90c8c64dSAndroid Build Coastguard Worker                  handlePlayRequest();
429*90c8c64dSAndroid Build Coastguard Worker              } else {
430*90c8c64dSAndroid Build Coastguard Worker                  LogHelper.e(TAG, "skipToPrevious: cannot skip to previous. previous Index=" +
431*90c8c64dSAndroid Build Coastguard Worker                          mCurrentIndexOnQueue + " queue length=" +
432*90c8c64dSAndroid Build Coastguard Worker                          (mPlayingQueue == null ? "null" : mPlayingQueue.size()));
433*90c8c64dSAndroid Build Coastguard Worker                  handleStopRequest("Cannot skip");
434*90c8c64dSAndroid Build Coastguard Worker              }
435*90c8c64dSAndroid Build Coastguard Worker          }
436*90c8c64dSAndroid Build Coastguard Worker 
437*90c8c64dSAndroid Build Coastguard Worker          @Override
onCustomAction(String action, Bundle extras)438*90c8c64dSAndroid Build Coastguard Worker          public void onCustomAction(String action, Bundle extras) {
439*90c8c64dSAndroid Build Coastguard Worker              if (CUSTOM_ACTION_THUMBS_UP.equals(action)) {
440*90c8c64dSAndroid Build Coastguard Worker                  LogHelper.i(TAG, "onCustomAction: favorite for current track");
441*90c8c64dSAndroid Build Coastguard Worker                  MediaMetadata track = getCurrentPlayingMusic();
442*90c8c64dSAndroid Build Coastguard Worker                  if (track != null) {
443*90c8c64dSAndroid Build Coastguard Worker                      String musicId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
444*90c8c64dSAndroid Build Coastguard Worker                      mMusicProvider.setFavorite(musicId, !mMusicProvider.isFavorite(musicId));
445*90c8c64dSAndroid Build Coastguard Worker                  }
446*90c8c64dSAndroid Build Coastguard Worker                  // playback state needs to be updated because the "Favorite" icon on the
447*90c8c64dSAndroid Build Coastguard Worker                  // custom action will change to reflect the new favorite state.
448*90c8c64dSAndroid Build Coastguard Worker                  updatePlaybackState(null);
449*90c8c64dSAndroid Build Coastguard Worker              } else {
450*90c8c64dSAndroid Build Coastguard Worker                  LogHelper.e(TAG, "Unsupported action: ", action);
451*90c8c64dSAndroid Build Coastguard Worker              }
452*90c8c64dSAndroid Build Coastguard Worker          }
453*90c8c64dSAndroid Build Coastguard Worker 
454*90c8c64dSAndroid Build Coastguard Worker          @Override
onPlayFromSearch(String query, Bundle extras)455*90c8c64dSAndroid Build Coastguard Worker          public void onPlayFromSearch(String query, Bundle extras) {
456*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "playFromSearch  query=", query);
457*90c8c64dSAndroid Build Coastguard Worker 
458*90c8c64dSAndroid Build Coastguard Worker              if (TextUtils.isEmpty(query)) {
459*90c8c64dSAndroid Build Coastguard Worker                  // A generic search like "Play music" sends an empty query
460*90c8c64dSAndroid Build Coastguard Worker                  // and it's expected that we start playing something. What will be played depends
461*90c8c64dSAndroid Build Coastguard Worker                  // on the app: favorite playlist, "I'm feeling lucky", most recent, etc.
462*90c8c64dSAndroid Build Coastguard Worker                  mPlayingQueue = QueueHelper.getRandomQueue(mMusicProvider);
463*90c8c64dSAndroid Build Coastguard Worker              } else {
464*90c8c64dSAndroid Build Coastguard Worker                  mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider);
465*90c8c64dSAndroid Build Coastguard Worker              }
466*90c8c64dSAndroid Build Coastguard Worker 
467*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "playFromSearch  playqueue.length=" + mPlayingQueue.size());
468*90c8c64dSAndroid Build Coastguard Worker              mSession.setQueue(mPlayingQueue);
469*90c8c64dSAndroid Build Coastguard Worker 
470*90c8c64dSAndroid Build Coastguard Worker              if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
471*90c8c64dSAndroid Build Coastguard Worker                  // immediately start playing from the beginning of the search results
472*90c8c64dSAndroid Build Coastguard Worker                  mCurrentIndexOnQueue = 0;
473*90c8c64dSAndroid Build Coastguard Worker 
474*90c8c64dSAndroid Build Coastguard Worker                  handlePlayRequest();
475*90c8c64dSAndroid Build Coastguard Worker              } else {
476*90c8c64dSAndroid Build Coastguard Worker                  // if nothing was found, we need to warn the user and stop playing
477*90c8c64dSAndroid Build Coastguard Worker                  handleStopRequest(getString(R.string.no_search_results));
478*90c8c64dSAndroid Build Coastguard Worker              }
479*90c8c64dSAndroid Build Coastguard Worker          }
480*90c8c64dSAndroid Build Coastguard Worker      }
481*90c8c64dSAndroid Build Coastguard Worker 
482*90c8c64dSAndroid Build Coastguard Worker      /**
483*90c8c64dSAndroid Build Coastguard Worker       * Handle a request to play music
484*90c8c64dSAndroid Build Coastguard Worker       */
handlePlayRequest()485*90c8c64dSAndroid Build Coastguard Worker      private void handlePlayRequest() {
486*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState());
487*90c8c64dSAndroid Build Coastguard Worker 
488*90c8c64dSAndroid Build Coastguard Worker          mDelayedStopHandler.removeCallbacksAndMessages(null);
489*90c8c64dSAndroid Build Coastguard Worker          if (!mServiceStarted) {
490*90c8c64dSAndroid Build Coastguard Worker              LogHelper.v(TAG, "Starting service");
491*90c8c64dSAndroid Build Coastguard Worker              // The MusicService needs to keep running even after the calling MediaBrowser
492*90c8c64dSAndroid Build Coastguard Worker              // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer
493*90c8c64dSAndroid Build Coastguard Worker              // need to play media.
494*90c8c64dSAndroid Build Coastguard Worker              startService(new Intent(getApplicationContext(), MusicService.class));
495*90c8c64dSAndroid Build Coastguard Worker              mServiceStarted = true;
496*90c8c64dSAndroid Build Coastguard Worker          }
497*90c8c64dSAndroid Build Coastguard Worker 
498*90c8c64dSAndroid Build Coastguard Worker          if (!mSession.isActive()) {
499*90c8c64dSAndroid Build Coastguard Worker              mSession.setActive(true);
500*90c8c64dSAndroid Build Coastguard Worker          }
501*90c8c64dSAndroid Build Coastguard Worker 
502*90c8c64dSAndroid Build Coastguard Worker          if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
503*90c8c64dSAndroid Build Coastguard Worker              updateMetadata();
504*90c8c64dSAndroid Build Coastguard Worker              mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue));
505*90c8c64dSAndroid Build Coastguard Worker          }
506*90c8c64dSAndroid Build Coastguard Worker      }
507*90c8c64dSAndroid Build Coastguard Worker 
508*90c8c64dSAndroid Build Coastguard Worker      /**
509*90c8c64dSAndroid Build Coastguard Worker       * Handle a request to pause music
510*90c8c64dSAndroid Build Coastguard Worker       */
handlePauseRequest()511*90c8c64dSAndroid Build Coastguard Worker      private void handlePauseRequest() {
512*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState());
513*90c8c64dSAndroid Build Coastguard Worker          mPlayback.pause();
514*90c8c64dSAndroid Build Coastguard Worker          // reset the delayed stop handler.
515*90c8c64dSAndroid Build Coastguard Worker          mDelayedStopHandler.removeCallbacksAndMessages(null);
516*90c8c64dSAndroid Build Coastguard Worker          mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
517*90c8c64dSAndroid Build Coastguard Worker      }
518*90c8c64dSAndroid Build Coastguard Worker 
519*90c8c64dSAndroid Build Coastguard Worker      /**
520*90c8c64dSAndroid Build Coastguard Worker       * Handle a request to stop music
521*90c8c64dSAndroid Build Coastguard Worker       */
handleStopRequest(String withError)522*90c8c64dSAndroid Build Coastguard Worker      private void handleStopRequest(String withError) {
523*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "handleStopRequest: mState=" + mPlayback.getState() + " error=", withError);
524*90c8c64dSAndroid Build Coastguard Worker          mPlayback.stop(true);
525*90c8c64dSAndroid Build Coastguard Worker          // reset the delayed stop handler.
526*90c8c64dSAndroid Build Coastguard Worker          mDelayedStopHandler.removeCallbacksAndMessages(null);
527*90c8c64dSAndroid Build Coastguard Worker          mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY);
528*90c8c64dSAndroid Build Coastguard Worker 
529*90c8c64dSAndroid Build Coastguard Worker          updatePlaybackState(withError);
530*90c8c64dSAndroid Build Coastguard Worker 
531*90c8c64dSAndroid Build Coastguard Worker          // service is no longer necessary. Will be started again if needed.
532*90c8c64dSAndroid Build Coastguard Worker          stopSelf();
533*90c8c64dSAndroid Build Coastguard Worker          mServiceStarted = false;
534*90c8c64dSAndroid Build Coastguard Worker      }
535*90c8c64dSAndroid Build Coastguard Worker 
updateMetadata()536*90c8c64dSAndroid Build Coastguard Worker      private void updateMetadata() {
537*90c8c64dSAndroid Build Coastguard Worker          if (!QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
538*90c8c64dSAndroid Build Coastguard Worker              LogHelper.e(TAG, "Can't retrieve current metadata.");
539*90c8c64dSAndroid Build Coastguard Worker              updatePlaybackState(getResources().getString(R.string.error_no_metadata));
540*90c8c64dSAndroid Build Coastguard Worker              return;
541*90c8c64dSAndroid Build Coastguard Worker          }
542*90c8c64dSAndroid Build Coastguard Worker          MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
543*90c8c64dSAndroid Build Coastguard Worker          String musicId = MediaIDHelper.extractMusicIDFromMediaID(
544*90c8c64dSAndroid Build Coastguard Worker                  queueItem.getDescription().getMediaId());
545*90c8c64dSAndroid Build Coastguard Worker          MediaMetadata track = mMusicProvider.getMusic(musicId);
546*90c8c64dSAndroid Build Coastguard Worker          final String trackId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
547*90c8c64dSAndroid Build Coastguard Worker          if (!musicId.equals(trackId)) {
548*90c8c64dSAndroid Build Coastguard Worker              IllegalStateException e = new IllegalStateException("track ID should match musicId.");
549*90c8c64dSAndroid Build Coastguard Worker              LogHelper.e(TAG, "track ID should match musicId.",
550*90c8c64dSAndroid Build Coastguard Worker                  " musicId=", musicId, " trackId=", trackId,
551*90c8c64dSAndroid Build Coastguard Worker                  " mediaId from queueItem=", queueItem.getDescription().getMediaId(),
552*90c8c64dSAndroid Build Coastguard Worker                  " title from queueItem=", queueItem.getDescription().getTitle(),
553*90c8c64dSAndroid Build Coastguard Worker                  " mediaId from track=", track.getDescription().getMediaId(),
554*90c8c64dSAndroid Build Coastguard Worker                  " title from track=", track.getDescription().getTitle(),
555*90c8c64dSAndroid Build Coastguard Worker                  " source.hashcode from track=", track.getString(
556*90c8c64dSAndroid Build Coastguard Worker                      MusicProvider.CUSTOM_METADATA_TRACK_SOURCE).hashCode(),
557*90c8c64dSAndroid Build Coastguard Worker                  e);
558*90c8c64dSAndroid Build Coastguard Worker              throw e;
559*90c8c64dSAndroid Build Coastguard Worker          }
560*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "Updating metadata for MusicID= " + musicId);
561*90c8c64dSAndroid Build Coastguard Worker          mSession.setMetadata(track);
562*90c8c64dSAndroid Build Coastguard Worker 
563*90c8c64dSAndroid Build Coastguard Worker          // Set the proper album artwork on the media session, so it can be shown in the
564*90c8c64dSAndroid Build Coastguard Worker          // locked screen and in other places.
565*90c8c64dSAndroid Build Coastguard Worker          if (track.getDescription().getIconBitmap() == null &&
566*90c8c64dSAndroid Build Coastguard Worker                  track.getDescription().getIconUri() != null) {
567*90c8c64dSAndroid Build Coastguard Worker              String albumUri = track.getDescription().getIconUri().toString();
568*90c8c64dSAndroid Build Coastguard Worker              AlbumArtCache.getInstance().fetch(albumUri, new AlbumArtCache.FetchListener() {
569*90c8c64dSAndroid Build Coastguard Worker                  @Override
570*90c8c64dSAndroid Build Coastguard Worker                  public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) {
571*90c8c64dSAndroid Build Coastguard Worker                      MediaSession.QueueItem queueItem = mPlayingQueue.get(mCurrentIndexOnQueue);
572*90c8c64dSAndroid Build Coastguard Worker                      MediaMetadata track = mMusicProvider.getMusic(trackId);
573*90c8c64dSAndroid Build Coastguard Worker                      track = new MediaMetadata.Builder(track)
574*90c8c64dSAndroid Build Coastguard Worker 
575*90c8c64dSAndroid Build Coastguard Worker                          // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is used, for
576*90c8c64dSAndroid Build Coastguard Worker                          // example, on the lockscreen background when the media session is active.
577*90c8c64dSAndroid Build Coastguard Worker                          .putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap)
578*90c8c64dSAndroid Build Coastguard Worker 
579*90c8c64dSAndroid Build Coastguard Worker                          // set small version of the album art in the DISPLAY_ICON. This is used on
580*90c8c64dSAndroid Build Coastguard Worker                          // the MediaDescription and thus it should be small to be serialized if
581*90c8c64dSAndroid Build Coastguard Worker                          // necessary..
582*90c8c64dSAndroid Build Coastguard Worker                          .putBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON, icon)
583*90c8c64dSAndroid Build Coastguard Worker 
584*90c8c64dSAndroid Build Coastguard Worker                          .build();
585*90c8c64dSAndroid Build Coastguard Worker 
586*90c8c64dSAndroid Build Coastguard Worker                      mMusicProvider.updateMusic(trackId, track);
587*90c8c64dSAndroid Build Coastguard Worker 
588*90c8c64dSAndroid Build Coastguard Worker                      // If we are still playing the same music
589*90c8c64dSAndroid Build Coastguard Worker                      String currentPlayingId = MediaIDHelper.extractMusicIDFromMediaID(
590*90c8c64dSAndroid Build Coastguard Worker                          queueItem.getDescription().getMediaId());
591*90c8c64dSAndroid Build Coastguard Worker                      if (trackId.equals(currentPlayingId)) {
592*90c8c64dSAndroid Build Coastguard Worker                          mSession.setMetadata(track);
593*90c8c64dSAndroid Build Coastguard Worker                      }
594*90c8c64dSAndroid Build Coastguard Worker                  }
595*90c8c64dSAndroid Build Coastguard Worker              });
596*90c8c64dSAndroid Build Coastguard Worker          }
597*90c8c64dSAndroid Build Coastguard Worker      }
598*90c8c64dSAndroid Build Coastguard Worker 
599*90c8c64dSAndroid Build Coastguard Worker      /**
600*90c8c64dSAndroid Build Coastguard Worker       * Update the current media player state, optionally showing an error message.
601*90c8c64dSAndroid Build Coastguard Worker       *
602*90c8c64dSAndroid Build Coastguard Worker       * @param error if not null, error message to present to the user.
603*90c8c64dSAndroid Build Coastguard Worker       */
updatePlaybackState(String error)604*90c8c64dSAndroid Build Coastguard Worker      private void updatePlaybackState(String error) {
605*90c8c64dSAndroid Build Coastguard Worker          LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState());
606*90c8c64dSAndroid Build Coastguard Worker          long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN;
607*90c8c64dSAndroid Build Coastguard Worker          if (mPlayback != null && mPlayback.isConnected()) {
608*90c8c64dSAndroid Build Coastguard Worker              position = mPlayback.getCurrentStreamPosition();
609*90c8c64dSAndroid Build Coastguard Worker          }
610*90c8c64dSAndroid Build Coastguard Worker 
611*90c8c64dSAndroid Build Coastguard Worker          PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
612*90c8c64dSAndroid Build Coastguard Worker                  .setActions(getAvailableActions());
613*90c8c64dSAndroid Build Coastguard Worker 
614*90c8c64dSAndroid Build Coastguard Worker          setCustomAction(stateBuilder);
615*90c8c64dSAndroid Build Coastguard Worker          int state = mPlayback.getState();
616*90c8c64dSAndroid Build Coastguard Worker 
617*90c8c64dSAndroid Build Coastguard Worker          // If there is an error message, send it to the playback state:
618*90c8c64dSAndroid Build Coastguard Worker          if (error != null) {
619*90c8c64dSAndroid Build Coastguard Worker              // Error states are really only supposed to be used for errors that cause playback to
620*90c8c64dSAndroid Build Coastguard Worker              // stop unexpectedly and persist until the user takes action to fix it.
621*90c8c64dSAndroid Build Coastguard Worker              stateBuilder.setErrorMessage(error);
622*90c8c64dSAndroid Build Coastguard Worker              state = PlaybackState.STATE_ERROR;
623*90c8c64dSAndroid Build Coastguard Worker          }
624*90c8c64dSAndroid Build Coastguard Worker          stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime());
625*90c8c64dSAndroid Build Coastguard Worker 
626*90c8c64dSAndroid Build Coastguard Worker          // Set the activeQueueItemId if the current index is valid.
627*90c8c64dSAndroid Build Coastguard Worker          if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
628*90c8c64dSAndroid Build Coastguard Worker              MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
629*90c8c64dSAndroid Build Coastguard Worker              stateBuilder.setActiveQueueItemId(item.getQueueId());
630*90c8c64dSAndroid Build Coastguard Worker          }
631*90c8c64dSAndroid Build Coastguard Worker 
632*90c8c64dSAndroid Build Coastguard Worker          mSession.setPlaybackState(stateBuilder.build());
633*90c8c64dSAndroid Build Coastguard Worker 
634*90c8c64dSAndroid Build Coastguard Worker          if (state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_PAUSED) {
635*90c8c64dSAndroid Build Coastguard Worker              mMediaNotificationManager.startNotification();
636*90c8c64dSAndroid Build Coastguard Worker          }
637*90c8c64dSAndroid Build Coastguard Worker      }
638*90c8c64dSAndroid Build Coastguard Worker 
setCustomAction(PlaybackState.Builder stateBuilder)639*90c8c64dSAndroid Build Coastguard Worker      private void setCustomAction(PlaybackState.Builder stateBuilder) {
640*90c8c64dSAndroid Build Coastguard Worker          MediaMetadata currentMusic = getCurrentPlayingMusic();
641*90c8c64dSAndroid Build Coastguard Worker          if (currentMusic != null) {
642*90c8c64dSAndroid Build Coastguard Worker              // Set appropriate "Favorite" icon on Custom action:
643*90c8c64dSAndroid Build Coastguard Worker              String musicId = currentMusic.getString(MediaMetadata.METADATA_KEY_MEDIA_ID);
644*90c8c64dSAndroid Build Coastguard Worker              int favoriteIcon = R.drawable.ic_star_off;
645*90c8c64dSAndroid Build Coastguard Worker              if (mMusicProvider.isFavorite(musicId)) {
646*90c8c64dSAndroid Build Coastguard Worker                  favoriteIcon = R.drawable.ic_star_on;
647*90c8c64dSAndroid Build Coastguard Worker              }
648*90c8c64dSAndroid Build Coastguard Worker              LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ",
649*90c8c64dSAndroid Build Coastguard Worker                      musicId, " current favorite=", mMusicProvider.isFavorite(musicId));
650*90c8c64dSAndroid Build Coastguard Worker              stateBuilder.addCustomAction(CUSTOM_ACTION_THUMBS_UP, getString(R.string.favorite),
651*90c8c64dSAndroid Build Coastguard Worker                  favoriteIcon);
652*90c8c64dSAndroid Build Coastguard Worker          }
653*90c8c64dSAndroid Build Coastguard Worker      }
654*90c8c64dSAndroid Build Coastguard Worker 
getAvailableActions()655*90c8c64dSAndroid Build Coastguard Worker      private long getAvailableActions() {
656*90c8c64dSAndroid Build Coastguard Worker          long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
657*90c8c64dSAndroid Build Coastguard Worker                  PlaybackState.ACTION_PLAY_FROM_SEARCH;
658*90c8c64dSAndroid Build Coastguard Worker          if (mPlayingQueue == null || mPlayingQueue.isEmpty()) {
659*90c8c64dSAndroid Build Coastguard Worker              return actions;
660*90c8c64dSAndroid Build Coastguard Worker          }
661*90c8c64dSAndroid Build Coastguard Worker          if (mPlayback.isPlaying()) {
662*90c8c64dSAndroid Build Coastguard Worker              actions |= PlaybackState.ACTION_PAUSE;
663*90c8c64dSAndroid Build Coastguard Worker          }
664*90c8c64dSAndroid Build Coastguard Worker          if (mCurrentIndexOnQueue > 0) {
665*90c8c64dSAndroid Build Coastguard Worker              actions |= PlaybackState.ACTION_SKIP_TO_PREVIOUS;
666*90c8c64dSAndroid Build Coastguard Worker          }
667*90c8c64dSAndroid Build Coastguard Worker          if (mCurrentIndexOnQueue < mPlayingQueue.size() - 1) {
668*90c8c64dSAndroid Build Coastguard Worker              actions |= PlaybackState.ACTION_SKIP_TO_NEXT;
669*90c8c64dSAndroid Build Coastguard Worker          }
670*90c8c64dSAndroid Build Coastguard Worker          return actions;
671*90c8c64dSAndroid Build Coastguard Worker      }
672*90c8c64dSAndroid Build Coastguard Worker 
getCurrentPlayingMusic()673*90c8c64dSAndroid Build Coastguard Worker      private MediaMetadata getCurrentPlayingMusic() {
674*90c8c64dSAndroid Build Coastguard Worker          if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) {
675*90c8c64dSAndroid Build Coastguard Worker              MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue);
676*90c8c64dSAndroid Build Coastguard Worker              if (item != null) {
677*90c8c64dSAndroid Build Coastguard Worker                  LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=",
678*90c8c64dSAndroid Build Coastguard Worker                          item.getDescription().getMediaId());
679*90c8c64dSAndroid Build Coastguard Worker                  return mMusicProvider.getMusic(
680*90c8c64dSAndroid Build Coastguard Worker                          MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId()));
681*90c8c64dSAndroid Build Coastguard Worker              }
682*90c8c64dSAndroid Build Coastguard Worker          }
683*90c8c64dSAndroid Build Coastguard Worker          return null;
684*90c8c64dSAndroid Build Coastguard Worker      }
685*90c8c64dSAndroid Build Coastguard Worker 
686*90c8c64dSAndroid Build Coastguard Worker      /**
687*90c8c64dSAndroid Build Coastguard Worker       * Implementation of the Playback.Callback interface
688*90c8c64dSAndroid Build Coastguard Worker       */
689*90c8c64dSAndroid Build Coastguard Worker      @Override
onCompletion()690*90c8c64dSAndroid Build Coastguard Worker      public void onCompletion() {
691*90c8c64dSAndroid Build Coastguard Worker          // The media player finished playing the current song, so we go ahead
692*90c8c64dSAndroid Build Coastguard Worker          // and start the next.
693*90c8c64dSAndroid Build Coastguard Worker          if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) {
694*90c8c64dSAndroid Build Coastguard Worker              // In this sample, we restart the playing queue when it gets to the end:
695*90c8c64dSAndroid Build Coastguard Worker              mCurrentIndexOnQueue++;
696*90c8c64dSAndroid Build Coastguard Worker              if (mCurrentIndexOnQueue >= mPlayingQueue.size()) {
697*90c8c64dSAndroid Build Coastguard Worker                  mCurrentIndexOnQueue = 0;
698*90c8c64dSAndroid Build Coastguard Worker              }
699*90c8c64dSAndroid Build Coastguard Worker              handlePlayRequest();
700*90c8c64dSAndroid Build Coastguard Worker          } else {
701*90c8c64dSAndroid Build Coastguard Worker              // If there is nothing to play, we stop and release the resources:
702*90c8c64dSAndroid Build Coastguard Worker              handleStopRequest(null);
703*90c8c64dSAndroid Build Coastguard Worker          }
704*90c8c64dSAndroid Build Coastguard Worker      }
705*90c8c64dSAndroid Build Coastguard Worker 
706*90c8c64dSAndroid Build Coastguard Worker      @Override
onPlaybackStatusChanged(int state)707*90c8c64dSAndroid Build Coastguard Worker      public void onPlaybackStatusChanged(int state) {
708*90c8c64dSAndroid Build Coastguard Worker          updatePlaybackState(null);
709*90c8c64dSAndroid Build Coastguard Worker      }
710*90c8c64dSAndroid Build Coastguard Worker 
711*90c8c64dSAndroid Build Coastguard Worker      @Override
onError(String error)712*90c8c64dSAndroid Build Coastguard Worker      public void onError(String error) {
713*90c8c64dSAndroid Build Coastguard Worker          updatePlaybackState(error);
714*90c8c64dSAndroid Build Coastguard Worker      }
715*90c8c64dSAndroid Build Coastguard Worker 
716*90c8c64dSAndroid Build Coastguard Worker      /**
717*90c8c64dSAndroid Build Coastguard Worker       * A simple handler that stops the service if playback is not active (playing)
718*90c8c64dSAndroid Build Coastguard Worker       */
719*90c8c64dSAndroid Build Coastguard Worker      private static class DelayedStopHandler extends Handler {
720*90c8c64dSAndroid Build Coastguard Worker          private final WeakReference<MusicService> mWeakReference;
721*90c8c64dSAndroid Build Coastguard Worker 
DelayedStopHandler(MusicService service)722*90c8c64dSAndroid Build Coastguard Worker          private DelayedStopHandler(MusicService service) {
723*90c8c64dSAndroid Build Coastguard Worker              mWeakReference = new WeakReference<>(service);
724*90c8c64dSAndroid Build Coastguard Worker          }
725*90c8c64dSAndroid Build Coastguard Worker 
726*90c8c64dSAndroid Build Coastguard Worker          @Override
handleMessage(Message msg)727*90c8c64dSAndroid Build Coastguard Worker          public void handleMessage(Message msg) {
728*90c8c64dSAndroid Build Coastguard Worker              MusicService service = mWeakReference.get();
729*90c8c64dSAndroid Build Coastguard Worker              if (service != null && service.mPlayback != null) {
730*90c8c64dSAndroid Build Coastguard Worker                  if (service.mPlayback.isPlaying()) {
731*90c8c64dSAndroid Build Coastguard Worker                      LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use.");
732*90c8c64dSAndroid Build Coastguard Worker                      return;
733*90c8c64dSAndroid Build Coastguard Worker                  }
734*90c8c64dSAndroid Build Coastguard Worker                  LogHelper.d(TAG, "Stopping service with delay handler.");
735*90c8c64dSAndroid Build Coastguard Worker                  service.stopSelf();
736*90c8c64dSAndroid Build Coastguard Worker                  service.mServiceStarted = false;
737*90c8c64dSAndroid Build Coastguard Worker              }
738*90c8c64dSAndroid Build Coastguard Worker          }
739*90c8c64dSAndroid Build Coastguard Worker      }
740*90c8c64dSAndroid Build Coastguard Worker  }
741