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 <automotiveApp> root element. For a media app, this must include 93*90c8c64dSAndroid Build Coastguard Worker * an <uses name="media"/> element as a child. 94*90c8c64dSAndroid Build Coastguard Worker * For example, in AndroidManifest.xml: 95*90c8c64dSAndroid Build Coastguard Worker * <meta-data android:name="com.google.android.gms.car.application" 96*90c8c64dSAndroid Build Coastguard Worker * android:resource="@xml/automotive_app_desc"/> 97*90c8c64dSAndroid Build Coastguard Worker * And in res/values/automotive_app_desc.xml: 98*90c8c64dSAndroid Build Coastguard Worker * <automotiveApp> 99*90c8c64dSAndroid Build Coastguard Worker * <uses name="media"/> 100*90c8c64dSAndroid Build Coastguard Worker * </automotiveApp> 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