diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 37858054..0ae84b6e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -34,6 +34,7 @@ THE SOFTWARE. + @@ -132,6 +133,15 @@ THE SOFTWARE. + + + + + + + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.vanilla; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.provider.MediaStore; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.browse.MediaBrowser; +import android.media.browse.MediaBrowser.MediaItem; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.service.media.MediaBrowserService; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Message; +import android.os.Looper; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.os.SystemClock; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Handles Music Playback through MirrorLink(tm) by implementing a MediaBrowserService. + */ +public class MirrorLinkMediaBrowserService extends MediaBrowserService implements Handler.Callback { + + private static final String TAG = "MirrorLinkMediaBrowserService"; + // Action to change the repeat mode + private static final String CUSTOM_ACTION_REPEAT = "ch.blinkenlights.android.vanilla.REPEAT"; + // Action to change the repeat mode + private static final String CUSTOM_ACTION_SHUFFLE = "ch.blinkenlights.android.vanilla.SHUFFLE"; + + // Media managers + private MediaAdapter mArtistAdapter; + private MediaAdapter mAlbumAdapter; + private MediaAdapter mSongAdapter; + private MediaAdapter mPlaylistAdapter; + private MediaAdapter mGenreAdapter; + private MediaAdapter[] mMediaAdapters = new MediaAdapter[MediaUtils.TYPE_GENRE + 1]; + private List mAlbums = new ArrayList(); + private List mArtists = new ArrayList(); + private List mSongs = new ArrayList(); + private List mPlaylists = new ArrayList(); + private List mGenres = new ArrayList(); + private List mFiltered = new ArrayList(); + private boolean mCatalogReady = false; + + private final List mMediaRoot = new ArrayList(); + + // Media Session + private MediaSession mSession; + private Bundle mSessionExtras; + + // Indicates whether the service was started. + private boolean mServiceStarted; + + private Looper mLooper; + private Handler mHandler; + + @Override + public void onCreate() { + Log.d("VanillaMusic", "MediaBrowserService#onCreate"); + super.onCreate(); + + HandlerThread thread = new HandlerThread("MediaBrowserService", Process.THREAD_PRIORITY_DEFAULT); + thread.start(); + + // Prep the Media Adapters (caches the top categories) + mArtistAdapter = new MediaAdapter(this, MediaUtils.TYPE_ARTIST, null ,null, null); + mAlbumAdapter = new MediaAdapter(this, MediaUtils.TYPE_ALBUM, null, null, null); + mSongAdapter = new MediaAdapter(this, MediaUtils.TYPE_SONG, null, null, null); + mPlaylistAdapter = new MediaAdapter(this, MediaUtils.TYPE_PLAYLIST, null, null, null); + mGenreAdapter = new MediaAdapter(this, MediaUtils.TYPE_GENRE, null, null, null); + mMediaAdapters[MediaUtils.TYPE_ARTIST] = mArtistAdapter; + mMediaAdapters[MediaUtils.TYPE_ALBUM] = mAlbumAdapter; + mMediaAdapters[MediaUtils.TYPE_SONG] = mSongAdapter; + mMediaAdapters[MediaUtils.TYPE_PLAYLIST] = mPlaylistAdapter; + mMediaAdapters[MediaUtils.TYPE_GENRE] = mGenreAdapter; + + // Fill and cache the top queries + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_ARTIST)) + .setTitle(getString(R.string.artists)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.artists)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_ALBUM)) + .setTitle(getString(R.string.albums)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.albums)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_SONG)) + .setTitle(getString(R.string.songs)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.songs)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_GENRE)) + .setTitle(getString(R.string.genres)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.genres)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + mMediaRoot.add(new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(Integer.toString(MediaUtils.TYPE_PLAYLIST)) + .setTitle(getString(R.string.playlists)) + .setIconUri(Uri.parse("android.resource://" + + "ch.blinkenlights.android.vanilla/drawable/ic_menu_music_library")) + .setSubtitle(getString(R.string.playlists)) + .build(), MediaBrowser.MediaItem.FLAG_BROWSABLE + )); + + + // Start a new MediaSession + mSession = new MediaSession(this, "VanillaMediaBrowserService"); + setSessionToken(mSession.getSessionToken()); + mSession.setCallback(new MediaSessionCallback()); + mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); + mSessionExtras = new Bundle(); + mSession.setExtras(mSessionExtras); + + // Register with the PlaybackService + PlaybackService.registerService(this); + + // Make sure the PlaybackService is running + if(!PlaybackService.hasInstance()) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + PlaybackService.get(MirrorLinkMediaBrowserService.this); + } + }); + t.start(); + } + + mLooper = thread.getLooper(); + mHandler = new Handler(mLooper, this); + + updatePlaybackState(null); + } + + @Override + public int onStartCommand(Intent startIntent, int flags, int startId) { + Log.d("VanillaMusic", "MediaBrowserService#onStartCommand"); + return START_STICKY; + } + + @Override + public void onDestroy() { + Log.d("VanillaMusic", "MediaBrowserService#onDestroy"); + mServiceStarted = false; + PlaybackService.unregisterService(); + mSession.release(); + } + + /** + * Helper class to encode/decode item references + * derived from queries in a string + */ + private static class MediaID { + // Separators used to build MediaIDs for the MediaBrowserService + public static final String ID_TYPE_ROOT = Integer.toString(MediaUtils.TYPE_INVALID); + public static final String MEDIATYPE_SEPARATOR = "/"; + public static final String FILTER_SEPARATOR = "#"; + + final int mType; + final long mId; + final String mLabel; + + public MediaID(int type, long id, String label) { + mType = type; + mId = id; + mLabel = label; + } + + public MediaID(String mediaId) { + int type = MediaUtils.TYPE_INVALID; + long id = -1; + String label = null; + if(mediaId != null) { + String[] items = mediaId.split(MEDIATYPE_SEPARATOR); + type = items.length > 0 ? Integer.parseInt(items[0]) : MediaUtils.TYPE_INVALID; + if(items.length > 1) { + items = items[1].split(FILTER_SEPARATOR); + if(items.length >= 2) { + label = items[1]; + id = Long.parseLong(items[0]); + } + } + } + mType = type; + mId = id; + mLabel = label; + } + + public boolean isTopAdapter() { + return mId == -1; + } + + public boolean isInvalid() { + return mType == MediaUtils.TYPE_INVALID; + } + + @Override + public String toString() { + return toString(mType, mId, mLabel); + } + + public static boolean isTopAdapter(String mediaId) { + return mediaId.indexOf(MEDIATYPE_SEPARATOR) == -1; + } + + public static String toString(int type, long id, String label) { + return Integer.toString(type) + + (id == -1 ? "" : ( + MEDIATYPE_SEPARATOR + + id + + (label == null ? "" : + FILTER_SEPARATOR + + label + ) + ) + ); + } + } + + private static Limiter buildLimiterFromMediaID(MediaID parent) { + Limiter limiter = null; + String[] fields; + Object data; + if(!parent.isInvalid() && !parent.isTopAdapter()) { + switch(parent.mType) { + case MediaUtils.TYPE_ARTIST: + // expand using a album query limited by artist + fields = new String[] { parent.mLabel }; + data = String.format("%s=%d", MediaStore.Audio.Media.ARTIST_ID, parent.mId); + limiter = new Limiter(MediaUtils.TYPE_ARTIST, fields, data); + break; + case MediaUtils.TYPE_ALBUM: + // expand using a song query limited by album + fields = new String[] { parent.mLabel }; + data = String.format("%s=%d", MediaStore.Audio.Media.ALBUM_ID, parent.mId); + limiter = new Limiter(MediaUtils.TYPE_SONG, fields, data); + break; + case MediaUtils.TYPE_GENRE: + // expand using an artist limiter by genere + fields = new String[] { parent.mLabel }; + data = parent.mId; + limiter = new Limiter(MediaUtils.TYPE_GENRE, fields, data); + break; + case MediaUtils.TYPE_PLAYLIST: + // don't build much, a a playlist is playable but not expandable + case MediaUtils.TYPE_SONG: + // don't build much, a song is playable but not expandable + case MediaUtils.TYPE_INVALID: + break; + } + } + return limiter; + } + + private QueryTask buildQueryFromMediaID(MediaID parent, boolean empty, boolean all) + { + String[] projection; + + if (parent.mType == MediaUtils.TYPE_PLAYLIST) { + projection = empty ? Song.EMPTY_PLAYLIST_PROJECTION : Song.FILLED_PLAYLIST_PROJECTION; + } else { + projection = empty ? Song.EMPTY_PROJECTION : Song.FILLED_PROJECTION; + } + + QueryTask query; + if (all && (parent.mType != MediaUtils.TYPE_PLAYLIST)) { + query = (mMediaAdapters[parent.mType]).buildSongQuery(projection); + query.data = parent.mId; + query.mode = SongTimeline.MODE_PLAY_ID_FIRST; + } else { + query = MediaUtils.buildQuery(parent.mType, parent.mId, projection, null); + query.mode = SongTimeline.MODE_PLAY; + } + + return query; + } + + private void loadChildrenAsync( final MediaID parent, + final Result> result) { + + // Asynchronously load the music catalog in a separate thread + final Limiter limiter = buildLimiterFromMediaID(parent); + new AsyncTask() { + private static final int ASYNCTASK_SUCCEEDED = 1; + private static final int ASYNCTASK_FAILED = 0; + + @Override + protected Integer doInBackground(Void... params) { + int result = ASYNCTASK_FAILED; + try { + if(!mCatalogReady) { + runQuery(mArtists, MediaUtils.TYPE_ARTIST , mArtistAdapter); + runQuery(mAlbums, MediaUtils.TYPE_ALBUM, mAlbumAdapter); + runQuery(mSongs, MediaUtils.TYPE_SONG, mSongAdapter); + runQuery(mGenres, MediaUtils.TYPE_GENRE, mGenreAdapter); + runQuery(mPlaylists, MediaUtils.TYPE_PLAYLIST, mPlaylistAdapter); + mCatalogReady = true; + } + if(limiter != null) { + mFiltered.clear(); + switch(limiter.type) { + case MediaUtils.TYPE_ALBUM: + mSongAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_SONG, mSongAdapter); + break; + case MediaUtils.TYPE_ARTIST: + mAlbumAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_ALBUM, mAlbumAdapter); + break; + case MediaUtils.TYPE_SONG: + mSongAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_SONG, mSongAdapter); + break; + case MediaUtils.TYPE_PLAYLIST: + mPlaylistAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_PLAYLIST, mPlaylistAdapter); + break; + case MediaUtils.TYPE_GENRE: + mSongAdapter.setLimiter(limiter); + runQuery(mFiltered, MediaUtils.TYPE_SONG, mSongAdapter); + break; + } + } + result = ASYNCTASK_SUCCEEDED; + } catch (Exception e) { + Log.d("VanillaMusic","Failed retrieving Media"); + } + return Integer.valueOf(result); + } + + @Override + protected void onPostExecute(Integer current) { + List items = null; + if (result != null) { + if(parent.isTopAdapter()) { + switch(parent.mType) { + case MediaUtils.TYPE_ALBUM: + items = mAlbums; + mAlbumAdapter.setLimiter(null); + break; + case MediaUtils.TYPE_ARTIST: + items = mArtists; + mArtistAdapter.setLimiter(null); + break; + case MediaUtils.TYPE_SONG: + items = mSongs; + mSongAdapter.setLimiter(null); + break; + case MediaUtils.TYPE_PLAYLIST: + items = mPlaylists; + mPlaylistAdapter.setLimiter(null); + break; + case MediaUtils.TYPE_GENRE: + items = mGenres; + mGenreAdapter.setLimiter(null); + break; + } + } else { + items = mFiltered; + } + if (current == ASYNCTASK_SUCCEEDED) { + result.sendResult(items); + } else { + result.sendResult(Collections.emptyList()); + } + } + } + }.execute(); + } + + private Uri getArtUri(int mediaType, String id) { + switch(mediaType) { + case MediaUtils.TYPE_SONG: + return Uri.parse("content://media/external/audio/media/" + id + "/albumart"); + case MediaUtils.TYPE_ALBUM: + return Uri.parse("content://media/external/audio/albumart/" + id); + } + return Uri.parse("android.resource://ch.blinkenlights.android.vanilla/drawable/fallback_cover"); + } + + private String subtitleForMediaType(int mediaType) { + switch(mediaType) { + case MediaUtils.TYPE_ARTIST: + return getString(R.string.artists); + case MediaUtils.TYPE_SONG: + return getString(R.string.songs); + case MediaUtils.TYPE_PLAYLIST: + return getString(R.string.playlists); + case MediaUtils.TYPE_GENRE: + return getString(R.string.genres); + case MediaUtils.TYPE_ALBUM: + return getString(R.string.albums); + } + return ""; + } + + private void runQuery(List populateMe, int mediaType, MediaAdapter adapter) { + populateMe.clear(); + try { + Cursor cursor = adapter.query(); + + if (cursor == null) { + return; + } + + final int flags = (mediaType != MediaUtils.TYPE_SONG) + && (mediaType != MediaUtils.TYPE_PLAYLIST) ? + MediaBrowser.MediaItem.FLAG_BROWSABLE : MediaBrowser.MediaItem.FLAG_PLAYABLE; + + final int count = cursor.getCount(); + + for (int j = 0; j != count; ++j) { + cursor.moveToPosition(j); + final String id = cursor.getString(0); + final String label = cursor.getString(2); + MediaBrowser.MediaItem item = new MediaBrowser.MediaItem( + new MediaDescription.Builder() + .setMediaId(MediaID.toString(mediaType, Long.parseLong(id), label)) + .setTitle(label) + .setSubtitle(subtitleForMediaType(mediaType)) + .setIconUri(getArtUri(mediaType, id)) + .build(), + flags); + populateMe.add(item); + } + + cursor.close(); + } catch (Exception e) { + Log.d("VanillaMusic","Failed retrieving Media"); + } + } + + /* + ** MediaBrowserService APIs + */ + + @Override + public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { + return new BrowserRoot(MediaID.ID_TYPE_ROOT, null); + } + + @Override + public void onLoadChildren(final String parentMediaId, final Result> result) { + // Use result.detach to allow calling result.sendResult from another thread: + result.detach(); + if (!MediaID.ID_TYPE_ROOT.equals(parentMediaId)) { + loadChildrenAsync(new MediaID(parentMediaId), result); + } else { + result.sendResult(mMediaRoot); + } + } + + private void setSessionActive() { + if (!mServiceStarted) { + // The MirrorLinkMediaBrowserService 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(), MirrorLinkMediaBrowserService.class)); + mServiceStarted = true; + } + + if (!mSession.isActive()) { + mSession.setActive(true); + } + } + + private void setSessionInactive() { + if(mServiceStarted) { + // service is no longer necessary. Will be started again if needed. + MirrorLinkMediaBrowserService.this.stopSelf(); + mServiceStarted = false; + } + + if(mSession.isActive()) { + mSession.setActive(false); + } + } + + private static final int MSG_PLAY = 1; + private static final int MSG_PLAY_QUERY = 2; + private static final int MSG_PAUSE = 3; + private static final int MSG_STOP = 4; + private static final int MSG_SEEKTO = 5; + private static final int MSG_NEXTSONG = 6; + private static final int MSG_PREVSONG = 7; + private static final int MSG_SEEKFW = 8; + private static final int MSG_SEEKBW = 9; + private static final int MSG_REPEAT = 10; + private static final int MSG_SHUFFLE = 11; + private static final int MSG_UPDATE_STATE = 12; + + @Override + public boolean handleMessage(Message message) + { + switch (message.what) { + case MSG_PLAY: + setSessionActive(); + + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).play(); + } + break; + case MSG_PLAY_QUERY: + setSessionActive(); + if(PlaybackService.hasInstance()) { + QueryTask query = buildQueryFromMediaID(new MediaID((String)message.obj), false, true); + PlaybackService.get(MirrorLinkMediaBrowserService.this).addSongs(query); + } + break; + case MSG_PAUSE: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).pause(); + } + break; + case MSG_STOP: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).pause(); + } + setSessionInactive(); + break; + case MSG_SEEKTO: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).seekToProgress(message.arg1); + } + break; + case MSG_NEXTSONG: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.NextSong, null); + } + break; + case MSG_PREVSONG: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.SeekForward, null); + } + break; + case MSG_SEEKFW: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.SeekBackward, null); + } + break; + case MSG_SEEKBW: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.Repeat, null); + } + break; + case MSG_REPEAT: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.Shuffle, null); + } + break; + case MSG_SHUFFLE: + if(PlaybackService.hasInstance()) { + PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.NextSong, null); + } + break; + case MSG_UPDATE_STATE: + updatePlaybackState((String)message.obj); + break; + default: + return false; + } + + return true; + } + /* + ** MediaSession.Callback + */ + private final class MediaSessionCallback extends MediaSession.Callback { + + @Override + public void onPlay() { + mHandler.sendEmptyMessage(MSG_PLAY); + } + + @Override + public void onSeekTo(long position) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_SEEKTO, (int) position ,0)); + } + + @Override + public void onPlayFromMediaId(final String mediaId, Bundle extras) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_PLAY_QUERY, mediaId)); + } + + @Override + public void onPause() { + mHandler.sendEmptyMessage(MSG_PAUSE); + } + + @Override + public void onStop() { + mHandler.sendEmptyMessage(MSG_STOP); + } + + @Override + public void onSkipToNext() { + mHandler.sendEmptyMessage(MSG_NEXTSONG); + } + + @Override + public void onSkipToPrevious() { + mHandler.sendEmptyMessage(MSG_PREVSONG); + } + + @Override + public void onFastForward() { + mHandler.sendEmptyMessage(MSG_SEEKFW); + } + + @Override + public void onRewind() { + mHandler.sendEmptyMessage(MSG_SEEKBW); + } + + @Override + public void onCustomAction(String action, Bundle extras) { + if (CUSTOM_ACTION_REPEAT.equals(action)) { + mHandler.sendEmptyMessage(MSG_REPEAT); + } else if (CUSTOM_ACTION_SHUFFLE.equals(action)) { + mHandler.sendEmptyMessage(MSG_SHUFFLE); + } + } + } + + /** + * 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) { + long position = PlaybackState.PLAYBACK_POSITION_UNKNOWN; + int state = PlaybackState.STATE_PAUSED; + + if(PlaybackService.hasInstance()) { + if (PlaybackService.get(this).isPlaying()) { + state = PlaybackState.STATE_PLAYING; + } + position = PlaybackService.get(this).getPosition(); + } + + PlaybackState.Builder stateBuilder = new PlaybackState.Builder() + .setActions(getAvailableActions()); + + setCustomAction(stateBuilder); + + // 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()); + mSession.setPlaybackState(stateBuilder.build()); + + } + // 'DriveSafe' icons need to meet contrast requirement, and as such are usually + // monochrome in nature, hence the new repeat_inactive_service and shuffle_inactive_service + // artwork + + private static final int[] FINISH_ICONS = + { R.drawable.repeat_inactive_service + , R.drawable.repeat_active + , R.drawable.repeat_current_active + , R.drawable.stop_current_active + , R.drawable.random_active }; + + private static final int[] SHUFFLE_ICONS = + { R.drawable.shuffle_inactive_service + , R.drawable.shuffle_active + , R.drawable.shuffle_active + , R.drawable.shuffle_album_active }; + + private void setCustomAction(PlaybackState.Builder stateBuilder) { + if(PlaybackService.hasInstance()) { + Bundle customActionExtras = new Bundle(); + final int finishMode = PlaybackService.finishAction(PlaybackService.get(this).getState()); + final int shuffleMode = PlaybackService.shuffleMode(PlaybackService.get(this).getState()); + + stateBuilder.addCustomAction(new PlaybackState.CustomAction.Builder( + CUSTOM_ACTION_REPEAT, getString(R.string.cycle_repeat_mode), FINISH_ICONS[finishMode]) + .setExtras(customActionExtras) + .build()); + + stateBuilder.addCustomAction(new PlaybackState.CustomAction.Builder( + CUSTOM_ACTION_SHUFFLE, getString(R.string.cycle_shuffle_mode), SHUFFLE_ICONS[shuffleMode]) + .setExtras(customActionExtras) + .build()); + } + } + + private long getAvailableActions() { + long actions = PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackState.ACTION_SKIP_TO_PREVIOUS | PlaybackState.ACTION_SKIP_TO_NEXT; + + if(PlaybackService.hasInstance()) { + if (PlaybackService.get(this).isPlaying()) { + actions |= PlaybackState.ACTION_PAUSE; + actions |= PlaybackState.ACTION_FAST_FORWARD; + actions |= PlaybackState.ACTION_REWIND; + } + } + return actions; + } + + /** + * Implementation of the PlaybackService callbacks + */ + public void onTimelineChanged() { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null)); + // updatePlaybackState(null); + } + + public void setState(long uptime, int state) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null)); + // updatePlaybackState(null); + } + + public void setSong(long uptime, Song song) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null)); +// updatePlaybackState(null); + if(song == null) { + if(PlaybackService.hasInstance()) { + song = PlaybackService.get(this).getSong(0); + } + } + + if(song != null) { + MediaMetadata metadata = new MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(song.id)) + .putString(MediaMetadata.METADATA_KEY_ALBUM, song.album) + .putString(MediaMetadata.METADATA_KEY_ARTIST, song.artist) + .putLong(MediaMetadata.METADATA_KEY_DURATION, song.duration) + .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, "content://media/external/audio/media/" + Long.toString(song.id) + "/albumart") + .putString(MediaMetadata.METADATA_KEY_TITLE, song.title) + .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, song.trackNumber) + .build(); + mSession.setMetadata(metadata); + } + } + + public void onPositionInfoChanged() { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null)); + // updatePlaybackState(null); + } + + public void onError(String error) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, error)); + // updatePlaybackState(error); + } + + public void onMediaChanged() { + if(PlaybackService.hasInstance()) { + setSong(0,PlaybackService.get(this).getSong(0)); + } + + } +} diff --git a/src/ch/blinkenlights/android/vanilla/PlaybackService.java b/src/ch/blinkenlights/android/vanilla/PlaybackService.java index b62e5f7d..e9391218 100644 --- a/src/ch/blinkenlights/android/vanilla/PlaybackService.java +++ b/src/ch/blinkenlights/android/vanilla/PlaybackService.java @@ -72,12 +72,12 @@ import java.util.ArrayList; */ public final class PlaybackService extends Service implements Handler.Callback - , MediaPlayer.OnCompletionListener - , MediaPlayer.OnErrorListener - , SharedPreferences.OnSharedPreferenceChangeListener - , SongTimeline.Callback - , SensorEventListener - , AudioManager.OnAudioFocusChangeListener + , MediaPlayer.OnCompletionListener + , MediaPlayer.OnErrorListener + , SharedPreferences.OnSharedPreferenceChangeListener + , SongTimeline.Callback + , SensorEventListener + , AudioManager.OnAudioFocusChangeListener { /** * Name of the state file. @@ -99,7 +99,7 @@ public final class PlaybackService extends Service * Rewind song if we already played more than 2.5 sec */ private static final int REWIND_AFTER_PLAYED_MS = 2500; - + /** * Action for startService: toggle playback on/off. */ @@ -255,12 +255,12 @@ public final class PlaybackService extends Service * g: {@link PlaybackService#FLAG_DUCKING} */ int mState; - + /** * How many broken songs we did already skip */ int mSkipBroken; - + /** * Object used for state-related locking. */ @@ -277,6 +277,10 @@ public final class PlaybackService extends Service * Static referenced-array to PlaybackActivities, used for callbacks */ private static final ArrayList sActivities = new ArrayList(5); + /** + * Static reference to MirrorLinkMediaBrowserService, used for callbacks + */ + private static MirrorLinkMediaBrowserService sMirrorLinkMediaBrowserService = null; /** * Cached app-wide SharedPreferences instance. */ @@ -432,7 +436,7 @@ public final class PlaybackService extends Service mBastpUtil = new BastpUtil(); mReadahead = new ReadaheadThread(); mReadahead.start(); - + mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); mAudioManager = (AudioManager)getSystemService(AUDIO_SERVICE); @@ -618,14 +622,14 @@ public final class PlaybackService extends Service mp.setOnErrorListener(this); return mp; } - + public void prepareMediaPlayer(VanillaMediaPlayer mp, String path) throws IOException{ mp.setDataSource(path); mp.prepare(); applyReplayGain(mp); } - - + + /** * Make sure that the current ReplayGain volume matches * the (maybe just changed) user settings @@ -646,20 +650,20 @@ public final class PlaybackService extends Service * and adjusts the volume */ private void applyReplayGain(VanillaMediaPlayer mp) { - + float[] rg = getReplayGainValues(mp.getDataSource()); /* track, album */ float adjust = 0f; - + if(mReplayGainAlbumEnabled) { adjust = (rg[0] != 0 ? rg[0] : adjust); /* do we have track adjustment ? */ adjust = (rg[1] != 0 ? rg[1] : adjust); /* ..or, even better, album adj? */ } - + if(mReplayGainTrackEnabled || (mReplayGainAlbumEnabled && adjust == 0)) { adjust = (rg[1] != 0 ? rg[1] : adjust); /* do we have album adjustment ? */ adjust = (rg[0] != 0 ? rg[0] : adjust); /* ..or, even better, track adj? */ } - + if(adjust == 0) { /* No RG value found: decrease volume for untagged song if requested by user */ adjust = (mReplayGainUntaggedDeBump-150)/10f; @@ -669,12 +673,12 @@ public final class PlaybackService extends Service ** But we want -15 <-> +15, so 75 shall be zero */ adjust += 2*(mReplayGainBump-75)/10f; /* 2* -> we want +-15, not +-7.5 */ } - + if(mReplayGainAlbumEnabled == false && mReplayGainTrackEnabled == false) { /* Feature is disabled: Make sure that we are going to 100% volume */ adjust = 0f; } - + float rg_result = ((float)Math.pow(10, (adjust/20) ))*mFadeOut; if(rg_result > 1.0f) { rg_result = 1.0f; /* android would IGNORE the change if this is > 1 and we would end up with the wrong volume */ @@ -685,7 +689,7 @@ public final class PlaybackService extends Service } /** - * Returns the (hopefully cached) replaygain + * Returns the (hopefully cached) replaygain * values of given file */ public float[] getReplayGainValues(String path) { @@ -718,7 +722,7 @@ public final class PlaybackService extends Service private void triggerGaplessUpdate() { if(mMediaPlayerInitialized != true) return; - + if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) return; /* setNextMediaPlayer is supported since JB */ @@ -1017,6 +1021,11 @@ public final class PlaybackService extends Service ArrayList list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).setState(uptime, state); + + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.setState(uptime, state); + } } if (song != null) { @@ -1025,6 +1034,11 @@ public final class PlaybackService extends Service list.get(i).setSong(uptime, song); } + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.setSong(uptime, song); + } + updateWidgets(); if (mReadaheadEnabled) @@ -1105,6 +1119,23 @@ public final class PlaybackService extends Service mNotificationManager.cancel(NOTIFICATION_ID); } + /** + * When playing through MirrorLink(tm) don't interact + * with the User directly as this is considered distracting + * while driving + */ + private void showMirrorLinkSafeToast(int resId, int duration) { + if(sMirrorLinkMediaBrowserService == null) { + Toast.makeText(this, resId, duration).show(); + } + } + + private void showMirrorLinkSafeToast(CharSequence text, int duration) { + if(sMirrorLinkMediaBrowserService == null) { + Toast.makeText(this, text, duration).show(); + } + } + /** * Start playing if currently paused. * @@ -1116,7 +1147,7 @@ public final class PlaybackService extends Service if ((mState & FLAG_EMPTY_QUEUE) != 0) { setFinishAction(SongTimeline.FINISH_RANDOM); setCurrentSong(0); - Toast.makeText(this, R.string.random_enabling, Toast.LENGTH_SHORT).show(); + showMirrorLinkSafeToast(R.string.random_enabling, Toast.LENGTH_SHORT); } int state = updateState(mState | FLAG_PLAYING); @@ -1270,7 +1301,7 @@ public final class PlaybackService extends Service { /* Save our 'current' state as the try block may set the ERROR flag (which clears the PLAYING flag */ boolean playing = (mState & FLAG_PLAYING) != 0; - + try { mMediaPlayerInitialized = false; mMediaPlayer.reset(); @@ -1287,7 +1318,7 @@ public final class PlaybackService extends Service else if(song.path != null) { prepareMediaPlayer(mMediaPlayer, song.path); } - + mMediaPlayerInitialized = true; // Cancel any pending gapless updates and re-send them @@ -1310,16 +1341,16 @@ public final class PlaybackService extends Service } catch (IOException e) { mErrorMessage = getResources().getString(R.string.song_load_failed, song.path); updateState(mState | FLAG_ERROR); - Toast.makeText(this, mErrorMessage, Toast.LENGTH_LONG).show(); + showMirrorLinkSafeToast(mErrorMessage, Toast.LENGTH_LONG); Log.e("VanillaMusic", "IOException", e); - + /* Automatically advance to next song IF we are currently playing or already did skip something * This will stop after skipping 10 songs to avoid endless loops (queue full of broken stuff */ if(mTimeline.isEndOfQueue() == false && getSong(1) != null && (playing || (mSkipBroken > 0 && mSkipBroken < 10))) { mSkipBroken++; mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SKIP_BROKEN_SONG, getTimelinePosition(), 0), 1000); } - + } updateNotification(); @@ -1351,6 +1382,10 @@ public final class PlaybackService extends Service public boolean onError(MediaPlayer player, int what, int extra) { Log.e("VanillaMusic", "MediaPlayer error: " + what + ' ' + extra); + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.onError("MediaPlayer Error"); + } return true; } @@ -1689,8 +1724,7 @@ public final class PlaybackService extends Service default: throw new IllegalArgumentException("Invalid add mode: " + query.mode); } - - Toast.makeText(this, getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT).show(); + showMirrorLinkSafeToast(getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT); } /** @@ -1781,6 +1815,11 @@ public final class PlaybackService extends Service ArrayList list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).onTimelineChanged(); + + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.onTimelineChanged(); + } } @Override @@ -1789,6 +1828,11 @@ public final class PlaybackService extends Service ArrayList list = sActivities; for (int i = list.size(); --i != -1; ) list.get(i).onPositionInfoChanged(); + + MirrorLinkMediaBrowserService service = sMirrorLinkMediaBrowserService; + if(service != null) { + service.onPositionInfoChanged(); + } } private final ContentObserver mObserver = new ContentObserver(null) { @@ -1849,6 +1893,24 @@ public final class PlaybackService extends Service sActivities.remove(activity); } + /** + * Register a MirrorLinkMediaBrowserService instance + * + * @param service the Service to be registered + */ + public static void registerService(MirrorLinkMediaBrowserService service) { + sMirrorLinkMediaBrowserService = service; + } + + /** + * Deregister a MirrorLinkMediaBrowserService instance + * + * @param service the Service to be deregistered + */ + public static void unregisterService() { + sMirrorLinkMediaBrowserService = null; + } + /** * Initializes the service state, loading songs saved from the disk into the * song timeline. @@ -2171,7 +2233,7 @@ public final class PlaybackService extends Service break; case ClearQueue: clearQueue(); - Toast.makeText(this, R.string.queue_cleared, Toast.LENGTH_SHORT).show(); + showMirrorLinkSafeToast(R.string.queue_cleared, Toast.LENGTH_SHORT); break; case ShowQueue: Intent intentShowQueue = new Intent(this, ShowQueueActivity.class); @@ -2203,6 +2265,13 @@ public final class PlaybackService extends Service } } + /** + * Returns the playing status of the current song + */ + public boolean isPlaying() { + return (mState & FLAG_PLAYING) != 0; + } + /** * Returns the position of the current song in the song timeline. */ @@ -2218,14 +2287,14 @@ public final class PlaybackService extends Service { return mTimeline.getLength(); } - + /** * Returns 'Song' with given id from timeline */ public Song getSongByQueuePosition(int id) { return mTimeline.getSongByQueuePosition(id); } - + /** * Do a 'hard' jump to given queue position */