Merge pull request #3188 from ByteHamster/revert-playbackservice-changes

Revert playbackservice changes
This commit is contained in:
H. Lehmann 2019-05-17 15:23:41 +02:00 committed by GitHub
commit 1a327a2aae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 335 additions and 377 deletions

View File

@ -47,7 +47,6 @@ public class VideoplayerActivity extends MediaplayerActivity {
*/
private boolean videoControlsShowing = true;
private boolean videoSurfaceCreated = false;
private boolean playbackStoppedUponExitVideo = false;
private boolean destroyingDueToReload = false;
private VideoControlsHider videoControlsHider = new VideoControlsHider(this);
@ -78,7 +77,6 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override
protected void onResume() {
super.onResume();
playbackStoppedUponExitVideo = false;
if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) {
playExternalMedia(getIntent(), MediaType.VIDEO);
} else if (PlaybackService.isCasting()) {
@ -93,32 +91,12 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override
protected void onStop() {
stopPlaybackIfUserPreferencesSpecified(); // MUST be called before super.onStop(), while it still has member variable controller
super.onStop();
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
videoControlsHider.stop();
}
}
void stopPlaybackIfUserPreferencesSpecified() {
// to avoid the method being called twice during leaving Videoplayer
// , which will double-pause the media
// (it is usually first called by surfaceHolderCallback.surfaceDestroyed(),
// then VideoplayerActivity.onStop() , but sometimes VideoplayerActivity.onStop()
// will first be invoked.)
if (playbackStoppedUponExitVideo) {
return;
}
playbackStoppedUponExitVideo = true;
if (controller != null && !destroyingDueToReload
&& UserPreferences.getVideoBackgroundBehavior()
!= UserPreferences.VideoBackgroundBehavior.CONTINUE_PLAYING) {
Log.v(TAG, "stop video playback per UserPreference");
controller.notifyVideoSurfaceAbandoned();
}
}
@Override
public void onUserLeaveHint () {
if (!PictureInPictureUtil.isInPictureInPictureMode(this) && UserPreferences.getVideoBackgroundBehavior()
@ -297,12 +275,13 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Videosurface was destroyed." );
Log.v(TAG, " hasController=" + (controller != null)
+ " , destroyingDueToReload=" + destroyingDueToReload
+ " , videoBackgroundBehavior=" + UserPreferences.getVideoBackgroundBehavior());
Log.d(TAG, "Videosurface was destroyed");
videoSurfaceCreated = false;
stopPlaybackIfUserPreferencesSpecified();
if (controller != null && !destroyingDueToReload
&& UserPreferences.getVideoBackgroundBehavior()
!= UserPreferences.VideoBackgroundBehavior.CONTINUE_PLAYING) {
controller.notifyVideoSurfaceAbandoned();
}
}
};

View File

@ -1,6 +1,8 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.support.annotation.NonNull;
import android.content.Intent;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
@ -28,7 +30,7 @@ import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
*/
public class DefaultActionButtonCallback implements ActionButtonCallback {
private static final String TAG = "DefaultActionBtnCb";
private static final String TAG = "DefaultActionButtonCallback";
private final Context context;
@ -82,9 +84,13 @@ public class DefaultActionButtonCallback implements ActionButtonCallback {
}
} else { // media is downloaded
if (media.isCurrentlyPlaying()) {
new PlaybackServiceStarter(context, media)
.startWhenPrepared(true)
.shouldStream(false)
.start();
IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE);
} else if (media.isCurrentlyPaused()) {
new PlaybackServiceStarter(context, media) // need to start the service in case it's been stopped by system.
new PlaybackServiceStarter(context, media)
.startWhenPrepared(true)
.shouldStream(false)
.start();

View File

@ -25,7 +25,6 @@ import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaBrowserServiceCompat;
import android.support.v4.media.MediaDescriptionCompat;
@ -76,11 +75,6 @@ import de.greenrobot.event.EventBus;
/**
* Controls the MediaPlayer that plays a FeedMedia-file
*
* Callers should connect to the service with either:
* - .bindService()
* - ContextCompat.startForegroundService(), optionally with arguments, such as media to be played, in intent extras
*
*/
public class PlaybackService extends MediaBrowserServiceCompat {
/**
@ -95,7 +89,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
/**
* True if cast session should disconnect.
*/
private static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect";
public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect";
/**
* True if media should be streamed.
*/
@ -198,6 +192,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* Is true if service is running.
*/
public static boolean isRunning = false;
/**
* Is true if service has received a valid start command.
*/
public static boolean started = false;
/**
* Is true if the service was running, but paused due to headphone disconnect
*/
@ -266,6 +264,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Log.d(TAG, "Service created.");
isRunning = true;
PlaybackServiceNotificationBuilder notificationBuilder = new PlaybackServiceNotificationBuilder(this);
startForeground(NOTIFICATION_ID, notificationBuilder.build());
registerReceiver(autoStateUpdated, new IntentFilter("com.google.android.gms.car.media.STATUS"));
registerReceiver(headsetDisconnected, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
registerReceiver(shutdownReceiver, new IntentFilter(ACTION_SHUTDOWN_PLAYBACK_SERVICE));
@ -319,30 +320,13 @@ public class PlaybackService extends MediaBrowserServiceCompat {
EventBus.getDefault().post(new ServiceEvent(ServiceEvent.Action.SERVICE_STARTED));
}
private NotificationCompat.Builder createBasicNotification() {
final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext());
final PendingIntent pIntent = PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT);
return new NotificationCompat.Builder(
this, NotificationUtils.CHANNEL_ID_PLAYING)
.setContentTitle(getString(R.string.app_name))
.setContentText("Service is running") // Just in case the notification is not updated (should not occur)
.setOngoing(false)
.setContentIntent(pIntent)
.setWhen(0) // we don't need the time
.setSmallIcon(smallIcon)
.setPriority(NotificationCompat.PRIORITY_MIN);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "Service is about to be destroyed");
stopForeground(true);
isRunning = false;
started = false;
currentMediaType = MediaType.UNKNOWN;
PreferenceManager.getDefaultSharedPreferences(this)
@ -364,6 +348,11 @@ public class PlaybackService extends MediaBrowserServiceCompat {
taskManager.shutdown();
}
private void stopService() {
stopForeground(true);
stopSelf();
}
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) {
Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName +
@ -460,23 +449,26 @@ public class PlaybackService extends MediaBrowserServiceCompat {
final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
if (keycode == -1 && playable == null && !castDisconnect) {
// Typical cases when the service was started with no argument
// - when it is first bound, and then moved to startedState, as in <code>serviceManager.moveServiceToStartedState()</code>
// - callers (e.g., Controller) explicitly
Log.d(TAG, "PlaybackService was started with no arguments.");
Log.e(TAG, "PlaybackService was started with no arguments");
stopService();
return Service.START_NOT_STICKY;
}
if ((flags & Service.START_FLAG_REDELIVERY) != 0) {
Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.");
stopForeground(true);
} else {
if (keycode != -1) {
Log.d(TAG, "Received media button event");
boolean handled = handleKeycode(keycode, true);
if (!handled) {
// Just silently ignores unsupported keycode. Whether the service will
// continue to run is solely dependent on whether it is playing some media.
stopService();
return Service.START_NOT_STICKY;
}
} else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) {
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true);
started = true;
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
true);
boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
@ -486,6 +478,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
playable = DBReader.getFeedMedia(((FeedMedia) playable).getId());
}
mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
} else {
Log.d(TAG, "Did not handle intent to PlaybackService: " + intent);
Log.d(TAG, "Extras: " + intent.getExtras());
}
}
return Service.START_NOT_STICKY;
@ -559,23 +555,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
return true;
case KeyEvent.KEYCODE_MEDIA_STOP:
// The logic gives UI illusion of stop by removing notification
// In the UI within AntennaPod, including widgets, it is seen as PAUSE, e.g.,
// users can still user on-screen widget to resume playing.
if (status == PlayerStatus.PLAYING) {
// Implementation note: Use of a state in serviceManager to tell it to
// show stop state UI (i.e., stopForeground(true)) is a bit awkward.
//
// More intuitive API would be for mediaPlayer.pause() returns a Future that
// returns after pause, including the related async notification work completes.
// However, it has its own complication, that mediaPlayer.pause() does not
// really know when all the related work completes, as they are buried into
// (asynchronous) callbacks.
serviceManager.treatNextPauseAsStopOnUI();
mediaPlayer.pause(true, true);
} else {
serviceManager.showUIForStopState();
started = false;
}
stopForeground(true); // gets rid of persistent notification
return true;
default:
Log.d(TAG, "Unhandled key code: " + keycode);
@ -591,6 +576,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
if (playable != null) {
mediaPlayer.playMediaObject(playable, false, true, true);
started = true;
PlaybackService.this.updateMediaSessionMetadata(playable);
}
}
@ -605,9 +591,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
public void notifyVideoSurfaceAbandoned() {
Log.v(TAG, "notifyVideoSurfaceAbandoned()");
mediaPlayer.pause(true, false);
mediaPlayer.resetVideoSurface();
setupNotification(getPlayable());
stopForeground(!UserPreferences.isPersistNotify());
}
private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
@ -670,15 +657,27 @@ public class PlaybackService extends MediaBrowserServiceCompat {
break;
case PAUSED:
if ((UserPreferences.isPersistNotify() || isCasting) &&
android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// do not remove notification on pause based on user pref and whether android version supports expanded notifications
// Change [Play] button to [Pause]
setupNotification(newInfo);
} else if (!UserPreferences.isPersistNotify() && !isCasting) {
// remove notification on pause
stopForeground(true);
}
writePlayerStatusPlaybackPreferences();
break;
case STOPPED:
//writePlaybackPreferencesNoMediaPlaying();
//stopService();
break;
case PLAYING:
writePlayerStatusPlaybackPreferences();
setupNotification(newInfo);
started = true;
// set sleep timer if auto-enabled
if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING &&
SleepTimerPreferences.autoEnable() && !sleepTimerActive()) {
@ -689,6 +688,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
case ERROR:
writePlaybackPreferencesNoMediaPlaying();
stopService();
break;
}
@ -701,7 +701,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public void shouldStop() {
serviceManager.stopService();
stopService();
}
@Override
@ -750,6 +750,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
writePlaybackPreferencesNoMediaPlaying();
stopService();
return true;
}
@ -833,6 +834,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (stopPlaying) {
taskManager.cancelPositionSaver();
writePlaybackPreferencesNoMediaPlaying();
if (!isCasting) {
stopForeground(true);
}
}
if (mediaType == null) {
sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
@ -1046,7 +1050,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
private void updateMediaSession(final PlayerStatus playerStatus) {
PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder();
@PlaybackStateCompat.State
int state;
if (playerStatus != null) {
switch (playerStatus) {
@ -1110,9 +1113,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
flavorHelper.mediaSessionSetExtraForWear(mediaSession);
final PlaybackStateCompat sessionStateBuilt = sessionState.build();
mediaSession.setPlaybackState(sessionStateBuilt);
serviceManager.onPlaybackStateChange(sessionStateBuilt);
mediaSession.setPlaybackState(sessionState.build());
}
private static boolean useSkipToPreviousForRewindInLockscreen() {
@ -1166,7 +1167,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, imageLocation);
}
}
if (!Thread.currentThread().isInterrupted() && isStarted()) {
if (!Thread.currentThread().isInterrupted() && started) {
mediaSession.setSessionActivity(PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT));
@ -1189,190 +1190,69 @@ public class PlaybackService extends MediaBrowserServiceCompat {
*/
private Thread notificationSetupThread;
private synchronized void setupNotification(final Playable playable, boolean treatPauseAsStop) {
/**
* Prepares notification and starts the service in the foreground.
*/
private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) {
setupNotification(info.playable);
}
private synchronized void setupNotification(final Playable playable) {
if (notificationSetupThread != null) {
notificationSetupThread.interrupt();
}
if (playable == null) {
Log.d(TAG, "setupNotification: playable is null");
if (!isStarted()) {
serviceManager.stopService();
Log.d(TAG, "setupNotification: playable is null" + Log.getStackTraceString(new Exception()));
if (!started) {
stopService();
}
return;
}
Runnable notificationSetupTask = new Runnable() {
Bitmap icon = null;
@Override
public void run() {
Log.d(TAG, "notificationSetupTask: Starting background work");
Log.d(TAG, "Starting background work");
if (mediaPlayer == null) {
Log.d(TAG, "notificationSetupTask: mediaPlayer is null");
if (!isStarted()) {
serviceManager.stopService();
if (!started) {
stopService();
}
return;
}
int iconSize = getResources().getDimensionPixelSize(
android.R.dimen.notification_large_icon_width);
try {
icon = Glide.with(PlaybackService.this)
.asBitmap()
.load(playable.getImageLocation())
.apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
.apply(new RequestOptions().centerCrop())
.submit(iconSize, iconSize)
.get();
} catch (Throwable tr) {
Log.e(TAG, "Error loading the media icon for the notification", tr);
}
if (icon == null) {
icon = BitmapFactory.decodeResource(getApplicationContext().getResources(),
ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()));
}
PlayerStatus playerStatus = mediaPlayer.getPlayerStatus();
Log.v(TAG, "notificationSetupTask: playerStatus=" + playerStatus);
PlaybackServiceNotificationBuilder notificationBuilder =
new PlaybackServiceNotificationBuilder(PlaybackService.this);
notificationBuilder.addMetadata(playable, mediaSession.getSessionToken(), playerStatus, isCasting);
if (!Thread.currentThread().isInterrupted() && isStarted()) {
String contentText = playable.getEpisodeTitle();
String contentTitle = playable.getFeedTitle();
Notification notification;
// Builder is v7, even if some not overwritten methods return its parent's v4 interface
NotificationCompat.Builder notificationBuilder = createBasicNotification();
notificationBuilder.setContentTitle(contentTitle)
.setContentText(contentText)
.setPriority(UserPreferences.getNotifyPriority())
.setLargeIcon(icon); // set notification priority
IntList compactActionList = new IntList();
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
if (isCasting) {
Intent stopCastingIntent = new Intent(PlaybackService.this, PlaybackService.class);
stopCastingIntent.putExtra(EXTRA_CAST_DISCONNECT, true);
PendingIntent stopCastingPendingIntent = PendingIntent.getService(PlaybackService.this,
numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
notificationBuilder.addAction(R.drawable.ic_media_cast_disconnect,
getString(R.string.cast_disconnect_label),
stopCastingPendingIntent);
numActions++;
if (!notificationBuilder.isIconCached(playable)) {
// To make sure that the notification is shown instantly
notificationBuilder.loadDefaultIcon();
startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
notificationBuilder.loadIcon(playable);
// always let them rewind
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
notificationBuilder.addAction(android.R.drawable.ic_media_rew,
getString(R.string.rewind_label),
rewindButtonPendingIntent);
if (UserPreferences.showRewindOnCompactNotification()) {
compactActionList.add(numActions);
}
numActions++;
if (playerStatus == PlayerStatus.PLAYING) {
PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_PAUSE, numActions);
notificationBuilder.addAction(android.R.drawable.ic_media_pause, //pause action
getString(R.string.pause_label),
pauseButtonPendingIntent);
compactActionList.add(numActions++);
} else {
PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_PLAY, numActions);
notificationBuilder.addAction(android.R.drawable.ic_media_play, //play action
getString(R.string.play_label),
playButtonPendingIntent);
compactActionList.add(numActions++);
}
// ff follows play, then we have skip (if it's present)
PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions);
notificationBuilder.addAction(android.R.drawable.ic_media_ff,
getString(R.string.fast_forward_label),
ffButtonPendingIntent);
if (UserPreferences.showFastForwardOnCompactNotification()) {
compactActionList.add(numActions);
}
numActions++;
if (UserPreferences.isFollowQueue()) {
PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_NEXT, numActions);
notificationBuilder.addAction(android.R.drawable.ic_media_next,
getString(R.string.skip_episode_label),
skipButtonPendingIntent);
if (UserPreferences.showSkipOnCompactNotification()) {
compactActionList.add(numActions);
}
numActions++;
}
PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_STOP, numActions);
notificationBuilder.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.getSessionToken())
.setShowActionsInCompactView(compactActionList.toArray())
.setShowCancelButton(true)
.setCancelButtonIntent(stopButtonPendingIntent))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setColor(NotificationCompat.COLOR_DEFAULT);
notification = notificationBuilder.build();
if (!Thread.currentThread().isInterrupted() && started) {
Notification notification = notificationBuilder.build();
if (playerStatus == PlayerStatus.PLAYING ||
playerStatus == PlayerStatus.PREPARING ||
playerStatus == PlayerStatus.SEEKING ||
isCasting) {
Log.v(TAG, "notificationSetupTask: make service foreground");
startForeground(NOTIFICATION_ID, notification);
} else if (playerStatus == PlayerStatus.PAUSED) {
if (treatPauseAsStop) {
stopForeground(true);
} else if ((UserPreferences.isPersistNotify() || isCasting) &&
android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// do not remove notification on pause based on user pref and whether android version supports expanded notifications
// Change [Play] button to [Pause]
leaveNotificationAsBackground(notification);
} else if (!UserPreferences.isPersistNotify() && !isCasting) {
// remove notification on pause
stopForeground(true);
}
} else {
leaveNotificationAsBackground(notification);
}
Log.d(TAG, "Notification set up");
}
}
private void leaveNotificationAsBackground(@NonNull Notification notification) {
stopForeground(false);
NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
mNotificationManager.notify(NOTIFICATION_ID, notification);
}
Log.d(TAG, "Notification set up");
}
}
};
notificationSetupThread = new Thread(notificationSetupTask);
notificationSetupThread.start();
}
private PendingIntent getPendingIntentForMediaAction(int keycodeValue, int requestCode) {
Intent intent = new Intent(
PlaybackService.this, PlaybackService.class);
intent.putExtra(
MediaButtonReceiver.EXTRA_KEYCODE,
keycodeValue);
return PendingIntent
.getService(PlaybackService.this, requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
/**
* Persists the current position and last played time of the media file.
*
@ -1550,7 +1430,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override
public void onReceive(Context context, Intent intent) {
if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
serviceManager.stopService();
stopService();
}
}
@ -1848,6 +1728,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position);
void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info);
MediaSessionCompat getMediaSession();
Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter);
@ -1886,6 +1768,24 @@ public class PlaybackService extends MediaBrowserServiceCompat {
PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position);
}
@Override
public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) {
if (connected) {
PlaybackService.this.setupNotification(info);
} else {
PlayerStatus status = info.playerStatus;
if ((status == PlayerStatus.PLAYING ||
status == PlayerStatus.SEEKING ||
status == PlayerStatus.PREPARING ||
UserPreferences.isPersistNotify()) &&
android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
PlaybackService.this.setupNotification(info);
} else if (!UserPreferences.isPersistNotify()) {
PlaybackService.this.stopForeground(true);
}
}
}
@Override
public MediaSessionCompat getMediaSession() {
return PlaybackService.this.mediaSession;
@ -1901,116 +1801,4 @@ public class PlaybackService extends MediaBrowserServiceCompat {
PlaybackService.this.unregisterReceiver(receiver);
}
};
private boolean isStarted() {
return serviceManager.serviceInStartedState;
}
/**
* The helper that manages PlaybackService's foreground service life cycle and the associated
* notification control.
*
* The logic is adapted from a sample app from The Android Open Source Project.
* See https://github.com/googlesamples/android-MediaBrowserService/blob/6cf01be9ef82ca2dd653f03e2a4af0b075cc0021/Application/src/main/java/com/example/android/mediasession/service/MusicService.java#L211
*
*/
private class ServiceManager {
private boolean serviceInStartedState;
private boolean toTreatNextPauseAsStopOnUI = false;
/**
*
* Entry point method for callers. Upon PlaybackState changes,
* the manager start/stop the PlaybackService as well as relevant notification
*/
void onPlaybackStateChange(PlaybackStateCompat state) {
// Report the state to the MediaSession.
Log.v(TAG, "onPlaybackStateChange(" + (state != null ? state.getState() : "null") + ")");
try {
// Manage the started state of this service.
switch (state.getState()) {
case PlaybackStateCompat.STATE_CONNECTING:
// move the service to started, aka, making it foreground
// upon STATE_CONNECTING, i.e., in preparing to play a media.
// This is done so that in case the preparation takes a long time, e.g.,
// streaming over a slow network,
// the service won't be killed by the system prematurely.
moveServiceToStartedState(state);
break;
case PlaybackStateCompat.STATE_PLAYING:
moveServiceToStartedState(state);
break;
case PlaybackStateCompat.STATE_PAUSED:
updateNotificationForPause(state);
break;
case PlaybackStateCompat.STATE_STOPPED:
moveServiceOutOfStartedState(state);
break;
case PlaybackStateCompat.STATE_ERROR:
moveServiceOutOfStartedState(state);
break;
}
} finally {
if (toTreatNextPauseAsStopOnUI) {
Log.v(TAG, "onPlaybackStateChange() - toTreatNextPauseAsStopOnUI enabled. The actual state (should be PAUSED, aka 2): " + state.getState());
toTreatNextPauseAsStopOnUI = false;
}
}
}
/**
* Tell service manager that on the next state change, if the state is STATE_PAUSED,
* give UI treatment as if it is stopped.
*
* @see #handleKeycode(int, boolean) the use case
*/
public void treatNextPauseAsStopOnUI() {
this.toTreatNextPauseAsStopOnUI = true;
}
public void showUIForStopState() {
Log.v(TAG, "serviceManager.showUIForStopState()");
stopForeground(true); // gets rid of persistent notification, to give the UI illusion of STOP
}
public void stopService() {
stopForeground(true);
stopSelf();
serviceInStartedState = false;
}
private void moveServiceToStartedState(PlaybackStateCompat state) {
if (!serviceInStartedState) {
ContextCompat.startForegroundService(
PlaybackService.this,
new Intent(PlaybackService.this, PlaybackService.class));
serviceInStartedState = true;
}
doSetupNotification();
}
private void updateNotificationForPause(PlaybackStateCompat state) {
doSetupNotification();
}
private void moveServiceOutOfStartedState(PlaybackStateCompat state) {
stopService();
}
private void doSetupNotification() {
if (mediaPlayer != null && mediaPlayer.getPlayable() != null) {
// it updates notification and set foreground status
// based on player status (similar to PlaybackState)
setupNotification(mediaPlayer.getPlayable(), toTreatNextPauseAsStopOnUI);
} else {
// should not happen unless there are bugs.
Log.e(TAG, "doSetupNotification() - unexpectedly there is no playable. No notification setup done. mediaPlayer." + mediaPlayer);
}
}
}
private final ServiceManager serviceManager = new ServiceManager();
}

View File

@ -0,0 +1,184 @@
package de.danoeh.antennapod.core.service.playback;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
import android.view.KeyEvent;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.core.ClientConfig;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.util.IntList;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import de.danoeh.antennapod.core.util.playback.Playable;
public class PlaybackServiceNotificationBuilder extends NotificationCompat.Builder {
private static final String TAG = "PlaybackSrvNotification";
private static Bitmap defaultIcon = null;
private Context context;
public PlaybackServiceNotificationBuilder(@NonNull Context context) {
super(context, NotificationUtils.CHANNEL_ID_PLAYING);
this.context = context;
final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(context);
final PendingIntent pIntent = PendingIntent.getActivity(context, 0,
PlaybackService.getPlayerActivityIntent(context),
PendingIntent.FLAG_UPDATE_CURRENT);
setContentTitle(context.getString(R.string.app_name));
setContentText("Service is running"); // Just in case the notification is not updated (should not occur)
setOngoing(false);
setContentIntent(pIntent);
setWhen(0); // we don't need the time
setSmallIcon(smallIcon);
setPriority(NotificationCompat.PRIORITY_MIN);
}
public void addMetadata(Playable playable, MediaSessionCompat.Token mediaSessionToken, PlayerStatus playerStatus, boolean isCasting) {
Log.v(TAG, "notificationSetupTask: playerStatus=" + playerStatus);
setContentTitle(playable.getFeedTitle());
setContentText(playable.getEpisodeTitle());
setPriority(UserPreferences.getNotifyPriority());
addActions(mediaSessionToken, playerStatus, isCasting);
setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
setColor(NotificationCompat.COLOR_DEFAULT);
}
public boolean isIconCached(Playable playable) {
int iconSize = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
try {
Bitmap icon = Glide.with(context)
.asBitmap()
.load(playable.getImageLocation())
.apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
.apply(new RequestOptions()
.centerCrop()
.onlyRetrieveFromCache(true))
.submit(iconSize, iconSize)
.get();
return icon != null;
} catch (Throwable tr) {
return false;
}
}
public void loadIcon(Playable playable) {
Bitmap icon = null;
int iconSize = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
try {
icon = Glide.with(context)
.asBitmap()
.load(playable.getImageLocation())
.apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
.apply(new RequestOptions().centerCrop())
.submit(iconSize, iconSize)
.get();
} catch (Throwable tr) {
Log.e(TAG, "Error loading the media icon for the notification", tr);
}
if (icon == null) {
loadDefaultIcon();
} else {
setLargeIcon(icon);
}
}
public void loadDefaultIcon() {
if (defaultIcon == null) {
defaultIcon = BitmapFactory.decodeResource(context.getResources(),
ClientConfig.playbackServiceCallbacks.getNotificationIconResource(context));
}
setLargeIcon(defaultIcon);
}
private void addActions(MediaSessionCompat.Token mediaSessionToken, PlayerStatus playerStatus, boolean isCasting) {
IntList compactActionList = new IntList();
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
if (isCasting) {
Intent stopCastingIntent = new Intent(context, PlaybackService.class);
stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true);
PendingIntent stopCastingPendingIntent = PendingIntent.getService(context,
numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
addAction(R.drawable.ic_media_cast_disconnect,
context.getString(R.string.cast_disconnect_label),
stopCastingPendingIntent);
numActions++;
}
// always let them rewind
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
addAction(android.R.drawable.ic_media_rew, context.getString(R.string.rewind_label), rewindButtonPendingIntent);
if (UserPreferences.showRewindOnCompactNotification()) {
compactActionList.add(numActions);
}
numActions++;
if (playerStatus == PlayerStatus.PLAYING) {
PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_PAUSE, numActions);
addAction(android.R.drawable.ic_media_pause, //pause action
context.getString(R.string.pause_label),
pauseButtonPendingIntent);
compactActionList.add(numActions++);
} else {
PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_PLAY, numActions);
addAction(android.R.drawable.ic_media_play, //play action
context.getString(R.string.play_label),
playButtonPendingIntent);
compactActionList.add(numActions++);
}
// ff follows play, then we have skip (if it's present)
PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions);
addAction(android.R.drawable.ic_media_ff, context.getString(R.string.fast_forward_label), ffButtonPendingIntent);
if (UserPreferences.showFastForwardOnCompactNotification()) {
compactActionList.add(numActions);
}
numActions++;
if (UserPreferences.isFollowQueue()) {
PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_NEXT, numActions);
addAction(android.R.drawable.ic_media_next,
context.getString(R.string.skip_episode_label),
skipButtonPendingIntent);
if (UserPreferences.showSkipOnCompactNotification()) {
compactActionList.add(numActions);
}
numActions++;
}
PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_STOP, numActions);
setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSessionToken)
.setShowActionsInCompactView(compactActionList.toArray())
.setShowCancelButton(true)
.setCancelButtonIntent(stopButtonPendingIntent));
}
private PendingIntent getPendingIntentForMediaAction(int keycodeValue, int requestCode) {
Intent intent = new Intent(context, PlaybackService.class);
intent.putExtra(MediaButtonReceiver.EXTRA_KEYCODE, keycodeValue);
return PendingIntent .getService(context, requestCode,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}

View File

@ -12,6 +12,7 @@ import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@ -104,7 +105,6 @@ public abstract class PlaybackController {
}
private synchronized void initServiceRunning() {
Log.v(TAG, "initServiceRunning()");
if (initialized) {
return;
}
@ -189,14 +189,21 @@ public abstract class PlaybackController {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(optionalIntent -> {
boolean bound = false;
if (!PlaybackService.started) {
if (optionalIntent.isPresent()) {
Log.d(TAG, "Calling bind service");
Log.d(TAG, "Calling start service");
ContextCompat.startForegroundService(activity, optionalIntent.get());
bound = activity.bindService(optionalIntent.get(), mConnection, 0);
} else {
status = PlayerStatus.STOPPED;
setupGUI();
handleStatus();
}
} else {
Log.d(TAG, "PlaybackService is running, trying to connect without start command.");
bound = activity.bindService(new Intent(activity, PlaybackService.class),
mConnection, 0);
}
Log.d(TAG, "Result for service binding: " + bound);
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@ -214,7 +221,6 @@ public abstract class PlaybackController {
return Optional.empty();
}
boolean fileExists = media.localFileAvailable();
boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
@ -582,8 +588,7 @@ public abstract class PlaybackController {
.startWhenPrepared(true)
.streamIfLastWasStream()
.start();
Log.d(TAG, "Play/Pause button was pressed, but playbackservice was null - " +
"it is likely to have been released by Android system. Restarting it.");
Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!");
return;
}
switch (status) {
@ -760,7 +765,6 @@ public abstract class PlaybackController {
}
public void notifyVideoSurfaceAbandoned() {
Log.v(TAG, "notifyVideoSurfaceAbandoned() - hasPlaybackService=" + (playbackService != null));
if (playbackService != null) {
playbackService.notifyVideoSurfaceAbandoned();
}

View File

@ -2,15 +2,14 @@ package de.danoeh.antennapod.core.util.playback;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
public class PlaybackServiceStarter {
private static final String TAG = "PlaybackServiceStarter";
private final Context context;
private final Playable media;
private boolean startWhenPrepared = false;
@ -67,10 +66,6 @@ public class PlaybackServiceStarter {
launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, shouldStream);
launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, prepareImmediately);
if (media == null) {
Log.e(TAG, "getIntent() - media is unexpectedly null. intent:" + launchIntent);
}
return launchIntent;
}

View File

@ -153,6 +153,7 @@ public class PlaybackServiceFlavorHelper {
// hardware volume buttons control the local device volume
mediaRouter.setMediaSessionCompat(null);
unregisterWifiBroadcastReceiver();
callback.setupNotification(false, info);
}
};
}
@ -182,6 +183,7 @@ public class PlaybackServiceFlavorHelper {
// hardware volume buttons control the remote device volume
mediaRouter.setMediaSessionCompat(callback.getMediaSession());
registerWifiBroadcastReceiver();
callback.setupNotification(true, info);
}
private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer,