Java tutorial
/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.murati.oszk.audiobook.playback; import android.content.res.Resources; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import com.murati.oszk.audiobook.R; import com.murati.oszk.audiobook.model.MusicProvider; import com.murati.oszk.audiobook.utils.FavoritesHelper; import com.murati.oszk.audiobook.utils.LogHelper; import com.murati.oszk.audiobook.utils.MediaIDHelper; import com.murati.oszk.audiobook.utils.WearHelper; /** * Manage the interactions among the container service, the queue manager and the actual playback. */ public class PlaybackManager implements Playback.Callback { private static final String TAG = LogHelper.makeLogTag(PlaybackManager.class); // Action to thumbs up a media item private static final String CUSTOM_ACTION_THUMBS_UP = "com.murati.oszk.audiobook.THUMBS_UP"; private MusicProvider mMusicProvider; private QueueManager mQueueManager; private Resources mResources; private Playback mPlayback; private PlaybackServiceCallback mServiceCallback; private MediaSessionCallback mMediaSessionCallback; public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources, MusicProvider musicProvider, QueueManager queueManager, Playback playback) { mMusicProvider = musicProvider; mServiceCallback = serviceCallback; mResources = resources; mQueueManager = queueManager; mMediaSessionCallback = new MediaSessionCallback(); mPlayback = playback; mPlayback.setCallback(this); } public Playback getPlayback() { return mPlayback; } public MediaSessionCompat.Callback getMediaSessionCallback() { return mMediaSessionCallback; } /** * Handle a request to play music */ public void handlePlayRequest() { LogHelper.d(TAG, "handlePlayRequest: mState=" + mPlayback.getState()); MediaSessionCompat.QueueItem currentMusic = mQueueManager.getCurrentTrack(); if (currentMusic != null) { mServiceCallback.onPlaybackStart(); mPlayback.play(currentMusic); //TODO: save last here //currentMusic.getDescription().getMediaId() } } /** * Handle a request to pause music */ public void handlePauseRequest() { LogHelper.d(TAG, "handlePauseRequest: mState=" + mPlayback.getState()); if (mPlayback.isPlaying()) { mPlayback.pause(); mServiceCallback.onPlaybackStop(); } } /** * Handle a request to stop music * * @param withError Error message in case the stop has an unexpected cause. The error * message will be set in the PlaybackState and will be visible to * MediaController clients. */ public void handleStopRequest(String withError) { LogHelper.d(TAG, "handleStopRequest: mState=" + mPlayback.getState() + " error=", withError); mPlayback.stop(true); //TODO: hide mServiceCallback.onPlaybackStop(); updatePlaybackState(withError); } /** * Update the current media player state, optionally showing an error message. * * @param error if not null, error message to present to the user. */ public void updatePlaybackState(String error) { LogHelper.d(TAG, "updatePlaybackState, playback state=" + mPlayback.getState()); long position = PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN; if (mPlayback != null && mPlayback.isConnected()) { position = mPlayback.getCurrentStreamPosition(); } //noinspection ResourceType PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.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 = PlaybackStateCompat.STATE_ERROR; } //noinspection ResourceType stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime()); // Set the activeQueueItemId if the current index is valid. MediaSessionCompat.QueueItem currentMusic = mQueueManager.getCurrentTrack(); if (currentMusic != null) { stateBuilder.setActiveQueueItemId(currentMusic.getQueueId()); } mServiceCallback.onPlaybackStateUpdated(stateBuilder.build()); if (state == PlaybackStateCompat.STATE_PLAYING || state == PlaybackStateCompat.STATE_PAUSED) { mServiceCallback.onNotificationRequired(); } } private void setCustomAction(PlaybackStateCompat.Builder stateBuilder) { MediaSessionCompat.QueueItem currentMusic = mQueueManager.getCurrentTrack(); if (currentMusic == null) { return; } // Set appropriate "Favorite" icon on Custom action: String mediaId = currentMusic.getDescription().getMediaId(); if (mediaId == null) { return; } String musicId = MediaIDHelper.extractMusicIDFromMediaID(mediaId); int favoriteIcon = FavoritesHelper.isFavorite(musicId) ? R.drawable.ic_star_on : R.drawable.ic_star_off; LogHelper.d(TAG, "updatePlaybackState, setting Favorite custom action of music ", musicId, " current favorite=", FavoritesHelper.isFavorite(musicId)); Bundle customActionExtras = new Bundle(); WearHelper.setShowCustomActionOnWear(customActionExtras, true); stateBuilder.addCustomAction(new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_THUMBS_UP, mResources.getString(R.string.browse_favorites), favoriteIcon).setExtras(customActionExtras) .build()); } private long getAvailableActions() { long actions = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT; if (mPlayback.isPlaying()) { actions |= PlaybackStateCompat.ACTION_PAUSE; } else { actions |= PlaybackStateCompat.ACTION_PLAY; } return actions; } /** * Implementation of the Playback.Callback interface */ @Override public void onCompletion() { // The media player finished playing the current song, so we go ahead // and start the next. if (mQueueManager.skipQueuePosition(1)) { handlePlayRequest(); mQueueManager.updateMetadata(); } else { // If skipping was not possible, we stop and release the resources: handleStopRequest(null); } } @Override public void onPlaybackStatusChanged(int state) { updatePlaybackState(null); } @Override public void onError(String error) { updatePlaybackState(error); } @Override public void setCurrentMediaId(String mediaId) { LogHelper.d(TAG, "setCurrentMediaId", mediaId); mQueueManager.setQueueFromTrack(mediaId); } /** * Switch to a different Playback instance, maintaining all playback state, if possible. * * @param playback switch to this playback */ public void switchToPlayback(Playback playback, boolean resumePlaying) { if (playback == null) { throw new IllegalArgumentException("Playback cannot be null"); } // Suspends current state. int oldState = mPlayback.getState(); long pos = mPlayback.getCurrentStreamPosition(); String currentMediaId = mPlayback.getCurrentMediaId(); mPlayback.stop(false); playback.setCallback(this); playback.setCurrentMediaId(currentMediaId); playback.seekTo(pos < 0 ? 0 : pos); playback.start(); // Swaps instance. mPlayback = playback; switch (oldState) { case PlaybackStateCompat.STATE_BUFFERING: case PlaybackStateCompat.STATE_CONNECTING: case PlaybackStateCompat.STATE_PAUSED: mPlayback.pause(); break; case PlaybackStateCompat.STATE_PLAYING: MediaSessionCompat.QueueItem currentMusic = mQueueManager.getCurrentTrack(); if (resumePlaying && currentMusic != null) { mPlayback.play(currentMusic); } else if (!resumePlaying) { mPlayback.pause(); } else { mPlayback.stop(true); } break; case PlaybackStateCompat.STATE_NONE: break; default: LogHelper.d(TAG, "Default called. Old state is ", oldState); } } private class MediaSessionCallback extends MediaSessionCompat.Callback { @Override public void onPlay() { LogHelper.d(TAG, "play"); handlePlayRequest(); } @Override public void onSkipToQueueItem(long queueId) { LogHelper.d(TAG, "OnSkipToQueueItem:" + queueId); mQueueManager.setCurrentQueueItem(queueId); mQueueManager.updateMetadata(); } @Override public void onSeekTo(long position) { LogHelper.d(TAG, "onSeekTo:", position); mPlayback.seekTo((int) position); } @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { LogHelper.d(TAG, "playFromMediaId mediaId:", mediaId, " extras=", extras); mQueueManager.setQueueFromTrack(mediaId); handlePlayRequest(); } @Override public void onPause() { LogHelper.d(TAG, "pause. current state=" + mPlayback.getState()); handlePauseRequest(); } @Override public void onStop() { LogHelper.d(TAG, "stop. current state=" + mPlayback.getState()); handleStopRequest(null); } @Override public void onSkipToNext() { LogHelper.d(TAG, "skipToNext"); if (mQueueManager.skipQueuePosition(1)) { handlePlayRequest(); } else { handleStopRequest("Cannot skip"); } mQueueManager.updateMetadata(); } @Override public void onSkipToPrevious() { if (mQueueManager.skipQueuePosition(-1)) { handlePlayRequest(); } else { handleStopRequest("Cannot skip"); } mQueueManager.updateMetadata(); } @Override public void onCustomAction(@NonNull String action, Bundle extras) { if (CUSTOM_ACTION_THUMBS_UP.equals(action)) { LogHelper.i(TAG, "onCustomAction: favorite for current track"); MediaSessionCompat.QueueItem currentMusic = mQueueManager.getCurrentTrack(); if (currentMusic != null) { String mediaId = currentMusic.getDescription().getMediaId(); if (mediaId != null) { String musicId = MediaIDHelper.extractMusicIDFromMediaID(mediaId); FavoritesHelper.setFavorite(musicId, !FavoritesHelper.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); } } /** * Handle free and contextual searches. * <p/> * All voice searches on Android Auto are sent to this method through a connected * {@link android.support.v4.media.session.MediaControllerCompat}. * <p/> * Threads and async handling: * Search, as a potentially slow operation, should run in another thread. * <p/> * Since this method runs on the main thread, most apps with non-trivial metadata * should defer the actual search to another thread (for example, by using * an {@link AsyncTask} as we do here). **/ @Override public void onPlayFromSearch(final String query, final Bundle extras) { LogHelper.d(TAG, "playFromSearch query=", query, " extras=", extras); if (query != null) return; mPlayback.setState(PlaybackStateCompat.STATE_CONNECTING); mMusicProvider.retrieveMediaAsync(new MusicProvider.Callback() { @Override public void onMusicCatalogReady(boolean success) { if (!success) { updatePlaybackState("Could not load catalog"); } boolean successSearch = mQueueManager.setQueueFromSearch(query, extras); if (successSearch) { handlePlayRequest(); mQueueManager.updateMetadata(); } else { updatePlaybackState("Could not find music"); } } }); } } public interface PlaybackServiceCallback { void onPlaybackStart(); void onNotificationRequired(); void onPlaybackStop(); void onPlaybackStateUpdated(PlaybackStateCompat newState); } }