@Override public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { LogHelper.d( TAG, "OnGetRoot: clientPackageName=" + clientPackageName, "; clientUid=" + clientUid + " ; rootHints=", rootHints); // To ensure you are not allowing any arbitrary app to browse your app's contents, you // need to check the origin: if (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) { // If the request comes from an untrusted package, return null. No further calls will // be made to other media browsing methods. LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package " + clientPackageName); return null; } //noinspection StatementWithEmptyBody if (CarHelper.isValidCarPackage(clientPackageName)) { // Optional: if your app needs to adapt the music library to show a different subset // when connected to the car, this is where you should handle it. // If you want to adapt other runtime behaviors, like tweak ads or change some behavior // that should be different on cars, you should instead use the boolean flag // set by the BroadcastReceiver mCarConnectionReceiver (mIsConnectedToCar). } //noinspection StatementWithEmptyBody if (WearHelper.isValidWearCompanionPackage(clientPackageName)) { // Optional: if your app needs to adapt the music library for when browsing from a // Wear device, you should return a different MEDIA ROOT here, and then, // on onLoadChildren, handle it accordingly. } return new BrowserRoot(MEDIA_ID_ROOT, null); }
@Override public void onPlayFromMediaId(String mediaId, Bundle extras) { LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras); // The mediaId used here is not the unique musicId. This one comes from the // MediaBrowser, and is actually a "hierarchy-aware mediaID": a concatenation of // the hierarchy in MediaBrowser and the actual unique musicID. This is necessary // so we can build the correct playing queue, based on where the track was // selected from. mPlayingQueue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider); mSession.setQueue(mPlayingQueue); String queueTitle = getString( R.string.browse_musics_by_genre_subtitle, MediaIDHelper.extractBrowseCategoryValueFromMediaID(mediaId)); mSession.setQueueTitle(queueTitle); if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { // set the current index on queue from the media Id: mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, mediaId); if (mCurrentIndexOnQueue < 0) { LogHelper.e( TAG, "playFromMediaId: media ID ", mediaId, " could not be found on queue. Ignoring."); } else { // play the music handlePlayRequest(); } } }
@Override public void handleMessage(Message msg) { MusicService service = mWeakReference.get(); if (service != null && service.mPlayback != null) { if (service.mPlayback.isPlaying()) { LogHelper.d(TAG, "Ignoring delayed stop since the media player is in use."); return; } LogHelper.d(TAG, "Stopping service with delay handler."); service.stopSelf(); service.mServiceStarted = false; } }
@Override public void onPlayFromSearch(String query, Bundle extras) { LogHelper.d(TAG, "playFromSearch query=", query); mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, mMusicProvider); LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size()); mSession.setQueue(mPlayingQueue); if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { // start playing from the beginning of the queue mCurrentIndexOnQueue = 0; handlePlayRequest(); } }
/** * Download a JSON file from a server, parse the content and return the JSON object. * * @return result JSONObject containing the parsed representation. */ private JSONObject fetchJSONFromUrl(String urlString) { InputStream is = null; try { URL url = new URL(urlString); URLConnection urlConnection = url.openConnection(); is = new BufferedInputStream(urlConnection.getInputStream()); BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "iso-8859-1")); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } return new JSONObject(sb.toString()); } catch (Exception e) { LogHelper.e(TAG, "Failed to parse the json for media list", e); return null; } finally { if (is != null) { try { is.close(); } catch (IOException e) { // ignore } } } }
@Override public void onCustomAction(String action, Bundle extras) { if (CUSTOM_ACTION_THUMBS_UP.equals(action)) { LogHelper.i(TAG, "onCustomAction: favorite for current track"); MediaMetadata track = getCurrentPlayingMusic(); if (track != null) { String musicId = track.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); mMusicProvider.setFavorite(musicId, !mMusicProvider.isFavorite(musicId)); } // playback state needs to be updated because the "Favorite" icon on the // custom action will change to reflect the new favorite state. updatePlaybackState(null); } else { LogHelper.e(TAG, "Unsupported action: ", action); } }
@Override public void onPlayFromSearch(final String query, final Bundle extras) { LogHelper.d(TAG, "playFromSearch query=", query, " extras=", extras); mPlayback.setState(PlaybackStateCompat.STATE_CONNECTING); // Voice searches may occur before the media catalog has been // prepared. We only handle the search after the musicProvider is ready. mMusicProvider.retrieveMediaAsync( new MusicProvider.Callback() { @Override public void onMusicCatalogReady(boolean success) { mPlayingQueue = QueueHelper.getPlayingQueueFromSearch(query, extras, mMusicProvider); LogHelper.d(TAG, "playFromSearch playqueue.length=" + mPlayingQueue.size()); mSession.setQueue(mPlayingQueue); if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { // immediately start playing from the beginning of the search results mCurrentIndexOnQueue = 0; handlePlayRequest(); } else { // if nothing was found, we need to warn the user and stop playing handleStopRequest(getString(R.string.no_search_results)); } } }); }
/** * Update the current media player state, optionally showing an error message. * * @param error if not null, error message to present to the user. */ private void updatePlaybackState(String error) { LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState()); long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN; if (mPlayback != null && mPlayback.isConnected()) { position = mPlayback.getCurrentStreamPosition(); } PlaybackState.Builder stateBuilder = new PlaybackState.Builder().setActions(getAvailableActions()); setCustomAction(stateBuilder); int state = mPlayback.getState(); // If there is an error message, send it to the playback state: if (error != null) { // Error states are really only supposed to be used for errors that cause playback to // stop unexpectedly and persist until the user takes action to fix it. stateBuilder.setErrorMessage(error); state = PlaybackState.STATE_ERROR; } stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime()); // Set the activeQueueItemId if the current index is valid. if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); stateBuilder.setActiveQueueItemId(item.getQueueId()); } mSession.setPlaybackState(stateBuilder.build()); if (state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_PAUSED) { mMediaNotificationManager.startNotification(); } }
private synchronized void retrieveMedia() { try { if (mCurrentState == State.NON_INITIALIZED) { mCurrentState = State.INITIALIZING; int slashPos = CATALOG_URL.lastIndexOf('/'); String path = CATALOG_URL.substring(0, slashPos + 1); JSONObject jsonObj = fetchJSONFromUrl(CATALOG_URL); if (jsonObj == null) { return; } JSONArray tracks = jsonObj.getJSONArray(JSON_MUSIC); if (tracks != null) { for (int j = 0; j < tracks.length(); j++) { MediaMetadata item = buildFromJSON(tracks.getJSONObject(j), path); String musicId = item.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); mMusicListById.put(musicId, new MutableMediaMetadata(musicId, item)); } buildListsByGenre(); } mCurrentState = State.INITIALIZED; } } catch (JSONException e) { LogHelper.e(TAG, e, "Could not retrieve music list"); } finally { if (mCurrentState != State.INITIALIZED) { // Something bad happened, so we reset state to NON_INITIALIZED to allow // retries (eg if the network connection is temporary unavailable) mCurrentState = State.NON_INITIALIZED; } } }
/** Handle a request to pause music */ private void handlePauseRequest() { LogHelper.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState()); mPlayback.pause(); // reset the delayed stop handler. mDelayedStopHandler.removeCallbacksAndMessages(null); mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); }
private void onMetadataChanged(MediaMetadataCompat metadata) { LogHelper.d(TAG, "onMetadataChanged ", metadata); if (getActivity() == null) { LogHelper.w( TAG, "onMetadataChanged called when getActivity null," + "this should not happen if the callback was properly unregistered. Ignoring."); return; } if (metadata == null) { return; } mTitle.setText(metadata.getDescription().getTitle()); mSubtitle.setText(metadata.getDescription().getSubtitle()); String artUrl = null; if (metadata.getDescription().getIconUri() != null) { artUrl = metadata.getDescription().getIconUri().toString(); } if (!TextUtils.equals(artUrl, mArtUrl)) { mArtUrl = artUrl; Bitmap art = metadata.getDescription().getIconBitmap(); AlbumArtCache cache = AlbumArtCache.getInstance(); if (art == null) { art = cache.getIconImage(mArtUrl); } if (art != null) { mAlbumArt.setImageBitmap(art); } else { cache.fetch( artUrl, new AlbumArtCache.FetchListener() { @Override public void onFetched(String artUrl, Bitmap bitmap, Bitmap icon) { if (icon != null) { LogHelper.d( TAG, "album art icon of w=", icon.getWidth(), " h=", icon.getHeight()); if (isAdded()) { mAlbumArt.setImageBitmap(icon); } } } }); } } }
@Override public void onDisconnected() { LogHelper.d(TAG, "onDisconnected"); mSessionExtras.remove(EXTRA_CONNECTED_CAST); mSession.setExtras(mSessionExtras); Playback playback = new LocalPlayback(MusicService.this, mMusicProvider); mMediaRouter.setMediaSession(null); switchToPlayer(playback, false); }
@Override public void onStop() { super.onStop(); LogHelper.d(TAG, "fragment.onStop"); MediaControllerCompat controller = ((FragmentActivity) getActivity()).getSupportMediaController(); if (controller != null) { controller.unregisterCallback(mCallback); } }
public void onConnected() { MediaControllerCompat controller = ((FragmentActivity) getActivity()).getSupportMediaController(); LogHelper.d(TAG, "onConnected, mediaController==null? ", controller == null); if (controller != null) { onMetadataChanged(controller.getMetadata()); onPlaybackStateChanged(controller.getPlaybackState()); controller.registerCallback(mCallback); } }
private void onPlaybackStateChanged(PlaybackStateCompat state) { LogHelper.d(TAG, "onPlaybackStateChanged ", state); if (getActivity() == null) { LogHelper.w( TAG, "onPlaybackStateChanged called when getActivity null," + "this should not happen if the callback was properly unregistered. Ignoring."); return; } if (state == null) { return; } boolean enablePlay = false; switch (state.getState()) { case PlaybackStateCompat.STATE_PAUSED: case PlaybackStateCompat.STATE_STOPPED: enablePlay = true; break; case PlaybackStateCompat.STATE_ERROR: LogHelper.e(TAG, "error playbackstate: ", state.getErrorMessage()); Toast.makeText(getActivity(), state.getErrorMessage(), Toast.LENGTH_LONG).show(); break; } if (enablePlay) { mPlayPause.setImageDrawable( ContextCompat.getDrawable(getActivity(), R.drawable.ic_play_arrow_black_36dp)); } else { mPlayPause.setImageDrawable( ContextCompat.getDrawable(getActivity(), R.drawable.ic_pause_black_36dp)); } MediaControllerCompat controller = ((FragmentActivity) getActivity()).getSupportMediaController(); String extraInfo = null; if (controller != null && controller.getExtras() != null) { String castName = controller.getExtras().getString(MusicService.EXTRA_CONNECTED_CAST); if (castName != null) { extraInfo = getResources().getString(R.string.casting_to_device, castName); } } setExtraInfo(extraInfo); }
@Override public void onStart() { super.onStart(); LogHelper.d(TAG, "fragment.onStart"); MediaControllerCompat controller = ((FragmentActivity) getActivity()).getSupportMediaController(); if (controller != null) { onConnected(); } }
@Override public void onSkipToQueueItem(long queueId) { LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId); if (mPlayingQueue != null && !mPlayingQueue.isEmpty()) { // set the current index on queue from the music Id: mCurrentIndexOnQueue = QueueHelper.getMusicIndexOnQueue(mPlayingQueue, queueId); // play the music handlePlayRequest(); } }
private MediaMetadata getCurrentPlayingMusic() { if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { MediaSession.QueueItem item = mPlayingQueue.get(mCurrentIndexOnQueue); if (item != null) { LogHelper.d(TAG, "getCurrentPlayingMusic for musicId=", item.getDescription().getMediaId()); return mMusicProvider.getMusic( MediaIDHelper.extractMusicIDFromMediaID(item.getDescription().getMediaId())); } } return null; }
@Override public void onMetadataChanged(String mediaId) { LogHelper.d(TAG, "onMetadataChanged", mediaId); List<MediaSession.QueueItem> queue = QueueHelper.getPlayingQueue(mediaId, mMusicProvider); int index = QueueHelper.getMusicIndexOnQueue(queue, mediaId); if (index > -1) { mCurrentIndexOnQueue = index; mPlayingQueue = queue; updateMetadata(); } }
@Override public void onSkipToNext() { LogHelper.d(TAG, "skipToNext"); mCurrentIndexOnQueue++; if (mPlayingQueue != null && mCurrentIndexOnQueue >= mPlayingQueue.size()) { // This sample's behavior: skipping to next when in last song returns to the // first song. mCurrentIndexOnQueue = 0; } if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { handlePlayRequest(); } else { LogHelper.e( TAG, "skipToNext: cannot skip to next. next Index=" + mCurrentIndexOnQueue + " queue length=" + (mPlayingQueue == null ? "null" : mPlayingQueue.size())); handleStopRequest("Cannot skip"); } }
@Override public void onSkipToPrevious() { LogHelper.d(TAG, "skipToPrevious"); mCurrentIndexOnQueue--; if (mPlayingQueue != null && mCurrentIndexOnQueue < 0) { // This sample's behavior: skipping to previous when in first song restarts the // first song. mCurrentIndexOnQueue = 0; } if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { handlePlayRequest(); } else { LogHelper.e( TAG, "skipToPrevious: cannot skip to previous. previous Index=" + mCurrentIndexOnQueue + " queue length=" + (mPlayingQueue == null ? "null" : mPlayingQueue.size())); handleStopRequest("Cannot skip"); } }
/** * Actual implementation of onLoadChildren that assumes that MusicProvider is already initialized. */ private void loadChildrenImpl( final String parentMediaId, final Result<List<MediaBrowser.MediaItem>> result) { LogHelper.d(TAG, "OnLoadChildren: parentMediaId=", parentMediaId); List<MediaBrowser.MediaItem> mediaItems = new ArrayList<>(); if (MEDIA_ID_ROOT.equals(parentMediaId)) { LogHelper.d(TAG, "OnLoadChildren.ROOT"); mediaItems.add( new MediaBrowser.MediaItem( new MediaDescription.Builder() .setMediaId(MEDIA_ID_MUSICS_BY_GENRE) .setTitle(getString(R.string.browse_genres)) .setIconUri( Uri.parse( "android.resource://" + "com.example.android.uamp/drawable/ic_by_genre")) .setSubtitle(getString(R.string.browse_genre_subtitle)) .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE)); } else if (MEDIA_ID_MUSICS_BY_GENRE.equals(parentMediaId)) { LogHelper.d(TAG, "OnLoadChildren.GENRES"); for (String genre : mMusicProvider.getGenres()) { MediaBrowser.MediaItem item = new MediaBrowser.MediaItem( new MediaDescription.Builder() .setMediaId(createBrowseCategoryMediaID(MEDIA_ID_MUSICS_BY_GENRE, genre)) .setTitle(genre) .setSubtitle(getString(R.string.browse_musics_by_genre_subtitle, genre)) .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE); mediaItems.add(item); } } else if (parentMediaId.startsWith(MEDIA_ID_MUSICS_BY_GENRE)) { String genre = MediaIDHelper.getHierarchy(parentMediaId)[1]; LogHelper.d(TAG, "OnLoadChildren.SONGS_BY_GENRE genre=", genre); for (MediaMetadata track : mMusicProvider.getMusicsByGenre(genre)) { // Since mediaMetadata fields are immutable, we need to create a copy, so we // can set a hierarchy-aware mediaID. We will need to know the media hierarchy // when we get a onPlayFromMusicID call, so we can create the proper queue based // on where the music was selected from (by artist, by genre, random, etc) String hierarchyAwareMediaID = MediaIDHelper.createMediaID( track.getDescription().getMediaId(), MEDIA_ID_MUSICS_BY_GENRE, genre); MediaMetadata trackCopy = new MediaMetadata.Builder(track) .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID) .build(); MediaBrowser.MediaItem bItem = new MediaBrowser.MediaItem(trackCopy.getDescription(), MediaItem.FLAG_PLAYABLE); mediaItems.add(bItem); } } else { LogHelper.w(TAG, "Skipping unmatched parentMediaId: ", parentMediaId); } LogHelper.d(TAG, "OnLoadChildren sending ", mediaItems.size(), " results for ", parentMediaId); result.sendResult(mediaItems); }
@Override public void onClick(View v) { MediaControllerCompat controller = ((FragmentActivity) getActivity()).getSupportMediaController(); PlaybackStateCompat stateObj = controller.getPlaybackState(); final int state = stateObj == null ? PlaybackStateCompat.STATE_NONE : stateObj.getState(); LogHelper.d(TAG, "Button pressed, in state " + state); switch (v.getId()) { case R.id.play_pause: LogHelper.d(TAG, "Play button pressed, in state " + state); if (state == PlaybackStateCompat.STATE_PAUSED || state == PlaybackStateCompat.STATE_STOPPED || state == PlaybackStateCompat.STATE_NONE) { playMedia(); } else if (state == PlaybackStateCompat.STATE_PLAYING || state == PlaybackStateCompat.STATE_BUFFERING || state == PlaybackStateCompat.STATE_CONNECTING) { pauseMedia(); } break; } }
@Override public void onMetadataChanged(MediaMetadataCompat metadata) { if (metadata == null) { return; } LogHelper.d( TAG, "Received metadata state change to mediaId=", metadata.getDescription().getMediaId(), " song=", metadata.getDescription().getTitle()); PlaybackControlsFragment.this.onMetadataChanged(metadata); }
/** Handle a request to stop music */ private void handleStopRequest(String withError) { LogHelper.d(TAG, "handleStopRequest: mState=" + mPlayback.getState() + " error=", withError); mPlayback.stop(true); // reset the delayed stop handler. mDelayedStopHandler.removeCallbacksAndMessages(null); mDelayedStopHandler.sendEmptyMessageDelayed(0, STOP_DELAY); updatePlaybackState(withError); // service is no longer necessary. Will be started again if needed. stopSelf(); mServiceStarted = false; }
/** Handle a request to play music */ private void handlePlayRequest() { LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState()); mDelayedStopHandler.removeCallbacksAndMessages(null); if (!mServiceStarted) { LogHelper.v(TAG, "Starting service"); // The MusicService needs to keep running even after the calling MediaBrowser // is disconnected. Call startService(Intent) and then stopSelf(..) when we no longer // need to play media. startService(new Intent(getApplicationContext(), MusicService.class)); mServiceStarted = true; } if (!mSession.isActive()) { mSession.setActive(true); } if (QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { updateMetadata(); mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue)); } }
/** * (non-Javadoc) * * @see android.app.Service#onDestroy() */ @Override public void onDestroy() { LogHelper.d(TAG, "onDestroy"); // Service is being killed, so make sure we release our resources handleStopRequest(null); mCastManager = ((UAMPApplication) getApplication()).getCastManager(getApplicationContext()); mCastManager.removeVideoCastConsumer(mCastConsumer); mDelayedStopHandler.removeCallbacksAndMessages(null); // Always release the MediaSession to clean up resources // and notify associated MediaController(s). mSession.release(); }
@Override public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { LogHelper.d( TAG, "OnGetRoot: clientPackageName=" + clientPackageName, "; clientUid=" + clientUid + " ; rootHints=", rootHints); // To ensure you are not allowing any arbitrary app to browse your app's contents, you // need to check the origin: if (!mPackageValidator.isCallerAllowed(this, clientPackageName, clientUid)) { // If the request comes from an untrusted package, return null. No further calls will // be made to other media browsing methods. LogHelper.w(TAG, "OnGetRoot: IGNORING request from untrusted package " + clientPackageName); return null; } //noinspection StatementWithEmptyBody if (CarHelper.isValidCarPackage(clientPackageName)) { // Optional: if your app needs to adapt ads, music library or anything else that // needs to run differently when connected to the car, this is where you should handle // it. } return new BrowserRoot(MEDIA_ID_ROOT, null); }
/** * Helper to switch to a different Playback instance * * @param playback switch to this playback */ private void switchToPlayer(Playback playback, boolean resumePlaying) { if (playback == null) { throw new IllegalArgumentException("Playback cannot be null"); } // suspend the current one. int oldState = mPlayback.getState(); int pos = mPlayback.getCurrentStreamPosition(); String currentMediaId = mPlayback.getCurrentMediaId(); LogHelper.d(TAG, "Current position from " + playback + " is ", pos); mPlayback.stop(false); playback.setCallback(this); playback.setCurrentStreamPosition(pos < 0 ? 0 : pos); playback.setCurrentMediaId(currentMediaId); playback.start(); // finally swap the instance mPlayback = playback; switch (oldState) { case PlaybackState.STATE_BUFFERING: case PlaybackState.STATE_CONNECTING: case PlaybackState.STATE_PAUSED: mPlayback.pause(); break; case PlaybackState.STATE_PLAYING: if (resumePlaying && QueueHelper.isIndexPlayable(mCurrentIndexOnQueue, mPlayingQueue)) { mPlayback.play(mPlayingQueue.get(mCurrentIndexOnQueue)); } else if (!resumePlaying) { mPlayback.pause(); } else { mPlayback.stop(true); } break; case PlaybackState.STATE_NONE: break; default: LogHelper.d(TAG, "Default called. Old state is ", oldState); } }
/** * (non-Javadoc) * * @see android.app.Service#onDestroy() */ @Override public void onDestroy() { LogHelper.d(TAG, "onDestroy"); unregisterReceiver(mCarConnectionReceiver); // Service is being killed, so make sure we release our resources handleStopRequest(null); mCastManager = VideoCastManager.getInstance(); mCastManager.removeVideoCastConsumer(mCastConsumer); mDelayedStopHandler.removeCallbacksAndMessages(null); // Always release the MediaSessionCompat to clean up resources // and notify associated MediaControllerCompat(s). mSession.release(); }