Remove MirrorLink support.

The 2nd MediaSession badly interacts with the button receiver on Android 11 and since i have no means to debug MirrorLink, it's time to say goodbye.
This commit is contained in:
Adrian Ulrich 2021-03-04 12:42:03 +01:00
parent fdbef8d8cb
commit 9954fbe082
3 changed files with 9 additions and 838 deletions

View File

@ -158,14 +158,6 @@ THE SOFTWARE.
</intent-filter>
</service>
<service
android:name=".MirrorLinkMediaBrowserService"
android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<service
android:name=".ScheduledLibraryUpdate"
android:permission="android.permission.BIND_JOB_SERVICE"

View File

@ -1,796 +0,0 @@
/*
* Copyright (C) 2013 - 2015 Adrian Ulrich <adrian@blinkenlights.ch>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package ch.blinkenlights.android.vanilla;
import android.annotation.TargetApi;
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.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Handles Music Playback through MirrorLink(tm) by implementing a MediaBrowserService.
*/
@TargetApi(21)
public class MirrorLinkMediaBrowserService extends MediaBrowserService
implements Handler.Callback,
TimelineCallback {
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<MediaBrowser.MediaItem> mQueryResult = new ArrayList<MediaBrowser.MediaItem>();
private final List<MediaBrowser.MediaItem> mMediaRoot = new ArrayList<MediaBrowser.MediaItem>();
// 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);
mAlbumAdapter = new MediaAdapter(this, MediaUtils.TYPE_ALBUM, null, null);
mSongAdapter = new MediaAdapter(this, MediaUtils.TYPE_SONG, null, null);
mPlaylistAdapter = new MediaAdapter(this, MediaUtils.TYPE_PLAYLIST, null, null);
mGenreAdapter = new MediaAdapter(this, MediaUtils.TYPE_GENRE, 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.addTimelineCallback(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);
setSong(0, 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.removeTimelineCallback(this);
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<List<MediaItem>> result) {
// Asynchronously load the music catalog in a separate thread
final Limiter limiter = buildLimiterFromMediaID(parent);
new AsyncTask<Void, Void, Integer>() {
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 {
mQueryResult.clear();
clearLimiters();
if(parent.isTopAdapter()) {
runQuery(mQueryResult, parent.mType, mMediaAdapters[parent.mType]);
} else if (limiter != null) {
switch(limiter.type) {
case MediaUtils.TYPE_ALBUM:
mSongAdapter.setLimiter(limiter);
runQuery(mQueryResult, MediaUtils.TYPE_SONG, mSongAdapter);
break;
case MediaUtils.TYPE_ARTIST:
mAlbumAdapter.setLimiter(limiter);
runQuery(mQueryResult, MediaUtils.TYPE_ALBUM, mAlbumAdapter);
break;
case MediaUtils.TYPE_SONG:
mSongAdapter.setLimiter(limiter);
runQuery(mQueryResult, MediaUtils.TYPE_SONG, mSongAdapter);
break;
case MediaUtils.TYPE_PLAYLIST:
mPlaylistAdapter.setLimiter(limiter);
runQuery(mQueryResult, MediaUtils.TYPE_PLAYLIST, mPlaylistAdapter);
break;
case MediaUtils.TYPE_GENRE:
mSongAdapter.setLimiter(limiter);
runQuery(mQueryResult, 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<MediaBrowser.MediaItem> items = null;
if (result != null) {
items = mQueryResult;
try {
if (current == ASYNCTASK_SUCCEEDED) {
result.sendResult(items);
} else {
result.sendResult(Collections.<MediaItem>emptyList());
}
} catch (IndexOutOfBoundsException e) {
Log.v("VanillaMusic", "result.send failed: " + e);
}
}
}
}.execute();
}
private void clearLimiters() {
for(MediaAdapter adapter : mMediaAdapters) {
adapter.setLimiter(null);
}
}
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<MediaBrowser.MediaItem> populateMe, int mediaType, MediaAdapter adapter) {
populateMe.clear();
try {
Cursor cursor = adapter.query();
Context context = getApplicationContext();
if (cursor == null) {
return;
}
final int flags = (mediaType == MediaUtils.TYPE_SONG || mediaType == MediaUtils.TYPE_PLAYLIST) ? MediaBrowser.MediaItem.FLAG_PLAYABLE : MediaBrowser.MediaItem.FLAG_BROWSABLE;
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);
long mediaId = Long.parseLong(id);
Song song = MediaUtils.getSongByTypeId(context, mediaType, mediaId);
MediaBrowser.MediaItem item = new MediaBrowser.MediaItem(
new MediaDescription.Builder()
.setMediaId(MediaID.toString(mediaType, mediaId, label))
.setTitle(label)
.setSubtitle(subtitleForMediaType(mediaType))
.setIconBitmap(song.getSmallCover(context))
.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<List<MediaItem>> 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.PreviousSong, null);
}
break;
case MSG_SEEKFW:
if(PlaybackService.hasInstance()) {
PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.SeekForward, null);
}
break;
case MSG_SEEKBW:
if(PlaybackService.hasInstance()) {
PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.SeekBackward, null);
}
break;
case MSG_REPEAT:
if(PlaybackService.hasInstance()) {
PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.Repeat, null);
}
break;
case MSG_SHUFFLE:
if(PlaybackService.hasInstance()) {
PlaybackService.get(MirrorLinkMediaBrowserService.this).performAction(Action.Shuffle, 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);
}
}
@Override
public void onPlayFromSearch(final String query, final Bundle extras) {
mHandler.sendEmptyMessage(MSG_PLAY);
}
}
/**
* 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_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_PLAY_FROM_SEARCH |
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));
}
public void setState(long uptime, int state) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null));
}
public void replaceSong(int delta, Song song) {
}
public void onMediaChange() {
setSong(0, null);
}
public void recreate() {
}
public void setSong(long uptime, Song song) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATE, null));
if(song == null) {
if(PlaybackService.hasInstance()) {
song = PlaybackService.get(this).getSong(0);
}
}
if(song != null) {
long[] androidIds = MediaUtils.getAndroidMediaIds(getApplicationContext(), song);
long songId = androidIds[0];
if (songId != -1) {
MediaMetadata metadata = new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(songId))
.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(songId) + "/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);
}
}

View File

@ -1197,35 +1197,14 @@ public final class PlaybackService extends Service
}
/**
* When playing through MirrorLink(tm) don't interact
* with the User directly as this is considered distracting
* while driving
* Enqueues a Toast message to be shown.
*/
private void showMirrorLinkSafeToast(int resId, int duration) {
if(getMirrorLinkCallback() == null) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_SHOW_TOAST, duration, resId));
}
private void showToast(int resId, int duration) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_SHOW_TOAST, duration, resId));
}
private void showMirrorLinkSafeToast(CharSequence text, int duration) {
if(getMirrorLinkCallback() == null) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_SHOW_TOAST, duration, 0, text));
}
}
/**
* Returns TRUE if the mirror link service has been registered
*/
private MirrorLinkMediaBrowserService getMirrorLinkCallback() {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
return null; // does not support mirrorlink
for (Object o : sCallbacks) {
if (o instanceof MirrorLinkMediaBrowserService) {
return (MirrorLinkMediaBrowserService)o;
}
}
return null;
private void showToast(CharSequence text, int duration) {
mHandler.sendMessage(mHandler.obtainMessage(MSG_SHOW_TOAST, duration, 0, text));
}
/**
@ -1238,7 +1217,7 @@ public final class PlaybackService extends Service
synchronized (mStateLock) {
if ((mState & FLAG_EMPTY_QUEUE) != 0) {
setFinishAction(SongTimeline.FINISH_RANDOM);
showMirrorLinkSafeToast(R.string.random_enabling, Toast.LENGTH_SHORT);
showToast(R.string.random_enabling, Toast.LENGTH_SHORT);
}
int state = updateState(mState | FLAG_PLAYING);
@ -1428,7 +1407,7 @@ public final class PlaybackService extends Service
} catch (IOException e) {
mErrorMessage = getResources().getString(R.string.song_load_failed, song.path);
updateState(mState | FLAG_ERROR);
showMirrorLinkSafeToast(mErrorMessage, Toast.LENGTH_LONG);
showToast(mErrorMessage, Toast.LENGTH_LONG);
Log.e("VanillaMusic", "IOException", e);
/* Automatically advance to next song IF we are currently playing or already did skip something
@ -1469,10 +1448,6 @@ public final class PlaybackService extends Service
{
Log.e("VanillaMusic", "MediaPlayer error: " + what + ' ' + extra);
MirrorLinkMediaBrowserService service = getMirrorLinkCallback();
if(service != null) {
service.onError("MediaPlayer Error");
}
return true;
}
@ -1864,7 +1839,7 @@ public final class PlaybackService extends Service
default:
throw new IllegalArgumentException("Invalid add mode: " + query.mode);
}
showMirrorLinkSafeToast(getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT);
showToast(getResources().getQuantityString(text, count, count), Toast.LENGTH_SHORT);
}
/**
@ -2320,7 +2295,7 @@ public final class PlaybackService extends Service
break;
case ClearQueue:
clearQueue();
showMirrorLinkSafeToast(R.string.queue_cleared, Toast.LENGTH_SHORT);
showToast(R.string.queue_cleared, Toast.LENGTH_SHORT);
break;
case ToggleControls:
case ShowQueue: