Merge pull request #2954 from orionlee/bugfix_phantom_notification_rework_2716

Fix phantom service notification
This commit is contained in:
H. Lehmann 2019-04-04 11:59:30 +02:00 committed by GitHub
commit 643173de14
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 459 additions and 143 deletions

View File

@ -47,6 +47,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
*/ */
private boolean videoControlsShowing = true; private boolean videoControlsShowing = true;
private boolean videoSurfaceCreated = false; private boolean videoSurfaceCreated = false;
private boolean playbackStoppedUponExitVideo = false;
private boolean destroyingDueToReload = false; private boolean destroyingDueToReload = false;
private VideoControlsHider videoControlsHider = new VideoControlsHider(this); private VideoControlsHider videoControlsHider = new VideoControlsHider(this);
@ -77,6 +78,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
playbackStoppedUponExitVideo = false;
if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) { if (TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) {
playExternalMedia(getIntent(), MediaType.VIDEO); playExternalMedia(getIntent(), MediaType.VIDEO);
} else if (PlaybackService.isCasting()) { } else if (PlaybackService.isCasting()) {
@ -91,12 +93,32 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override @Override
protected void onStop() { protected void onStop() {
stopPlaybackIfUserPreferencesSpecified(); // MUST be called before super.onStop(), while it still has member variable controller
super.onStop(); super.onStop();
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) { if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
videoControlsHider.stop(); 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 @Override
public void onUserLeaveHint () { public void onUserLeaveHint () {
if (!PictureInPictureUtil.isInPictureInPictureMode(this) && UserPreferences.getVideoBackgroundBehavior() if (!PictureInPictureUtil.isInPictureInPictureMode(this) && UserPreferences.getVideoBackgroundBehavior()
@ -275,13 +297,12 @@ public class VideoplayerActivity extends MediaplayerActivity {
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "Videosurface was destroyed"); Log.d(TAG, "Videosurface was destroyed." );
Log.v(TAG, " hasController=" + (controller != null)
+ " , destroyingDueToReload=" + destroyingDueToReload
+ " , videoBackgroundBehavior=" + UserPreferences.getVideoBackgroundBehavior());
videoSurfaceCreated = false; videoSurfaceCreated = false;
if (controller != null && !destroyingDueToReload stopPlaybackIfUserPreferencesSpecified();
&& UserPreferences.getVideoBackgroundBehavior()
!= UserPreferences.VideoBackgroundBehavior.CONTINUE_PLAYING) {
controller.notifyVideoSurfaceAbandoned();
}
} }
}; };

View File

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

View File

@ -25,6 +25,7 @@ import android.preference.PreferenceManager;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaBrowserServiceCompat; import android.support.v4.media.MediaBrowserServiceCompat;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
@ -75,6 +76,11 @@ import de.greenrobot.event.EventBus;
/** /**
* Controls the MediaPlayer that plays a FeedMedia-file * 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 { public class PlaybackService extends MediaBrowserServiceCompat {
/** /**
@ -192,10 +198,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
* Is true if service is running. * Is true if service is running.
*/ */
public static boolean isRunning = false; 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 * Is true if the service was running, but paused due to headphone disconnect
*/ */
@ -264,9 +266,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Log.d(TAG, "Service created."); Log.d(TAG, "Service created.");
isRunning = true; isRunning = true;
NotificationCompat.Builder notificationBuilder = createBasicNotification();
startForeground(NOTIFICATION_ID, notificationBuilder.build());
registerReceiver(autoStateUpdated, new IntentFilter("com.google.android.gms.car.media.STATUS")); registerReceiver(autoStateUpdated, new IntentFilter("com.google.android.gms.car.media.STATUS"));
registerReceiver(headsetDisconnected, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); registerReceiver(headsetDisconnected, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
registerReceiver(shutdownReceiver, new IntentFilter(ACTION_SHUTDOWN_PLAYBACK_SERVICE)); registerReceiver(shutdownReceiver, new IntentFilter(ACTION_SHUTDOWN_PLAYBACK_SERVICE));
@ -344,7 +343,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Log.d(TAG, "Service is about to be destroyed"); Log.d(TAG, "Service is about to be destroyed");
stopForeground(true); stopForeground(true);
isRunning = false; isRunning = false;
started = false;
currentMediaType = MediaType.UNKNOWN; currentMediaType = MediaType.UNKNOWN;
PreferenceManager.getDefaultSharedPreferences(this) PreferenceManager.getDefaultSharedPreferences(this)
@ -366,11 +364,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
taskManager.shutdown(); taskManager.shutdown();
} }
private void stopService() {
stopForeground(true);
stopSelf();
}
@Override @Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) {
Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName + Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName +
@ -464,37 +457,32 @@ public class PlaybackService extends MediaBrowserServiceCompat {
final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false); final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
if (keycode == -1 && playable == null && !castDisconnect) { if (keycode == -1 && playable == null && !castDisconnect) {
Log.e(TAG, "PlaybackService was started with no arguments"); // Typical cases when the service was started with no argument
stopService(); // - 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.");
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
} }
if ((flags & Service.START_FLAG_REDELIVERY) != 0) { if (keycode != -1) {
Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); Log.d(TAG, "Received media button event");
stopForeground(true); boolean handled = handleKeycode(keycode, true);
} else { if (!handled) {
if (keycode != -1) { // Just silently ignores unsupported keycode. Whether the service will
Log.d(TAG, "Received media button event"); // continue to run is solely dependent on whether it is playing some media.
boolean handled = handleKeycode(keycode, true); return Service.START_NOT_STICKY;
if (!handled) {
stopService();
return Service.START_NOT_STICKY;
}
} else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) {
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);
//If the user asks to play External Media, the casting session, if on, should end.
flavorHelper.castDisconnect(playable instanceof ExternalMedia);
if (playable instanceof FeedMedia) {
playable = DBReader.getFeedMedia(((FeedMedia) playable).getId());
}
mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
} }
setupNotification(playable); } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) {
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);
//If the user asks to play External Media, the casting session, if on, should end.
flavorHelper.castDisconnect(playable instanceof ExternalMedia);
if (playable instanceof FeedMedia) {
playable = DBReader.getFeedMedia(((FeedMedia) playable).getId());
}
mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
} }
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
@ -568,12 +556,23 @@ public class PlaybackService extends MediaBrowserServiceCompat {
mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000);
return true; return true;
case KeyEvent.KEYCODE_MEDIA_STOP: 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) { 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); mediaPlayer.pause(true, true);
started = false; } else {
serviceManager.showUIForStopState();
} }
stopForeground(true); // gets rid of persistent notification
return true; return true;
default: default:
Log.d(TAG, "Unhandled key code: " + keycode); Log.d(TAG, "Unhandled key code: " + keycode);
@ -589,7 +588,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()); Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext());
if (playable != null) { if (playable != null) {
mediaPlayer.playMediaObject(playable, false, true, true); mediaPlayer.playMediaObject(playable, false, true, true);
started = true;
PlaybackService.this.updateMediaSessionMetadata(playable); PlaybackService.this.updateMediaSessionMetadata(playable);
} }
} }
@ -604,10 +602,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
} }
public void notifyVideoSurfaceAbandoned() { public void notifyVideoSurfaceAbandoned() {
Log.v(TAG, "notifyVideoSurfaceAbandoned()");
mediaPlayer.pause(true, false); mediaPlayer.pause(true, false);
mediaPlayer.resetVideoSurface(); mediaPlayer.resetVideoSurface();
setupNotification(getPlayable());
stopForeground(!UserPreferences.isPersistNotify());
} }
private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() {
@ -670,27 +667,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
break; break;
case PAUSED: 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(); writePlayerStatusPlaybackPreferences();
break; break;
case STOPPED: case STOPPED:
//writePlaybackPreferencesNoMediaPlaying(); //writePlaybackPreferencesNoMediaPlaying();
//stopService();
break; break;
case PLAYING: case PLAYING:
writePlayerStatusPlaybackPreferences(); writePlayerStatusPlaybackPreferences();
setupNotification(newInfo);
started = true;
// set sleep timer if auto-enabled // set sleep timer if auto-enabled
if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING &&
SleepTimerPreferences.autoEnable() && !sleepTimerActive()) { SleepTimerPreferences.autoEnable() && !sleepTimerActive()) {
@ -701,7 +686,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
case ERROR: case ERROR:
writePlaybackPreferencesNoMediaPlaying(); writePlaybackPreferencesNoMediaPlaying();
stopService();
break; break;
} }
@ -714,7 +698,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override @Override
public void shouldStop() { public void shouldStop() {
stopService(); serviceManager.stopService();
} }
@Override @Override
@ -763,7 +747,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
} }
sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
writePlaybackPreferencesNoMediaPlaying(); writePlaybackPreferencesNoMediaPlaying();
stopService();
return true; return true;
} }
@ -847,9 +830,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
if (stopPlaying) { if (stopPlaying) {
taskManager.cancelPositionSaver(); taskManager.cancelPositionSaver();
writePlaybackPreferencesNoMediaPlaying(); writePlaybackPreferencesNoMediaPlaying();
if (!isCasting) {
stopForeground(true);
}
} }
if (mediaType == null) { if (mediaType == null) {
sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
@ -1063,6 +1043,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
private void updateMediaSession(final PlayerStatus playerStatus) { private void updateMediaSession(final PlayerStatus playerStatus) {
PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder(); PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder();
@PlaybackStateCompat.State
int state; int state;
if (playerStatus != null) { if (playerStatus != null) {
switch (playerStatus) { switch (playerStatus) {
@ -1126,7 +1107,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
flavorHelper.mediaSessionSetExtraForWear(mediaSession); flavorHelper.mediaSessionSetExtraForWear(mediaSession);
mediaSession.setPlaybackState(sessionState.build()); final PlaybackStateCompat sessionStateBuilt = sessionState.build();
mediaSession.setPlaybackState(sessionStateBuilt);
serviceManager.onPlaybackStateChange(sessionStateBuilt);
} }
private static boolean useSkipToPreviousForRewindInLockscreen() { private static boolean useSkipToPreviousForRewindInLockscreen() {
@ -1180,7 +1163,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, imageLocation); builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, imageLocation);
} }
} }
if (!Thread.currentThread().isInterrupted() && started) { if (!Thread.currentThread().isInterrupted() && isStarted()) {
mediaSession.setSessionActivity(PendingIntent.getActivity(this, 0, mediaSession.setSessionActivity(PendingIntent.getActivity(this, 0,
PlaybackService.getPlayerActivityIntent(this), PlaybackService.getPlayerActivityIntent(this),
PendingIntent.FLAG_UPDATE_CURRENT)); PendingIntent.FLAG_UPDATE_CURRENT));
@ -1203,21 +1186,14 @@ public class PlaybackService extends MediaBrowserServiceCompat {
*/ */
private Thread notificationSetupThread; 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) { if (notificationSetupThread != null) {
notificationSetupThread.interrupt(); notificationSetupThread.interrupt();
} }
if (playable == null) { if (playable == null) {
Log.d(TAG, "setupNotification: playable is null"); Log.d(TAG, "setupNotification: playable is null");
if (!started) { if (!isStarted()) {
stopService(); serviceManager.stopService();
} }
return; return;
} }
@ -1226,12 +1202,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override @Override
public void run() { public void run() {
Log.d(TAG, "Starting background work"); Log.d(TAG, "notificationSetupTask: Starting background work");
if (mediaPlayer == null) { if (mediaPlayer == null) {
Log.d(TAG, "notificationSetupTask: mediaPlayer is null"); Log.d(TAG, "notificationSetupTask: mediaPlayer is null");
if (!started) { if (!isStarted()) {
stopService(); serviceManager.stopService();
} }
return; return;
} }
@ -1256,8 +1232,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
} }
PlayerStatus playerStatus = mediaPlayer.getPlayerStatus(); PlayerStatus playerStatus = mediaPlayer.getPlayerStatus();
Log.v(TAG, "notificationSetupTask: playerStatus=" + playerStatus);
if (!Thread.currentThread().isInterrupted() && started) { if (!Thread.currentThread().isInterrupted() && isStarted()) {
String contentText = playable.getEpisodeTitle(); String contentText = playable.getEpisodeTitle();
String contentTitle = playable.getFeedTitle(); String contentTitle = playable.getFeedTitle();
Notification notification; Notification notification;
@ -1349,15 +1326,33 @@ public class PlaybackService extends MediaBrowserServiceCompat {
playerStatus == PlayerStatus.PREPARING || playerStatus == PlayerStatus.PREPARING ||
playerStatus == PlayerStatus.SEEKING || playerStatus == PlayerStatus.SEEKING ||
isCasting) { isCasting) {
Log.v(TAG, "notificationSetupTask: make service foreground");
startForeground(NOTIFICATION_ID, notification); 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 { } else {
stopForeground(false); leaveNotificationAsBackground(notification);
NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
mNotificationManager.notify(NOTIFICATION_ID, notification);
} }
Log.d(TAG, "Notification set up"); 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);
}
}; };
notificationSetupThread = new Thread(notificationSetupTask); notificationSetupThread = new Thread(notificationSetupTask);
notificationSetupThread.start(); notificationSetupThread.start();
@ -1552,7 +1547,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
stopService(); serviceManager.stopService();
} }
} }
@ -1850,8 +1845,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position); void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position);
void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info);
MediaSessionCompat getMediaSession(); MediaSessionCompat getMediaSession();
Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter); Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter);
@ -1890,24 +1883,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position); 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 @Override
public MediaSessionCompat getMediaSession() { public MediaSessionCompat getMediaSession() {
return PlaybackService.this.mediaSession; return PlaybackService.this.mediaSession;
@ -1923,4 +1898,116 @@ public class PlaybackService extends MediaBrowserServiceCompat {
PlaybackService.this.unregisterReceiver(receiver); 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,213 @@
/*
* Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package de.danoeh.antennapod.core.util;
import java.util.NoSuchElementException;
import java.util.Objects;
// AntennaPod's stripped-down version of Java/Android platform's java.util.Optional
// so that it can be used on lower API level (API level 14)
// Android-changed: removed ValueBased paragraph.
/**
* A container object which may or may not contain a non-null value.
* If a value is present, {@code isPresent()} will return {@code true} and
* {@code get()} will return the value.
*
* <p>Additional methods that depend on the presence or absence of a contained
* value are provided, such as {@link #orElse(java.lang.Object) orElse()}
* (return a default value if value not present) and
* {@link #ifPresent(java.util.function.Consumer) ifPresent()} (execute a block
* of code if the value is present).
*
* @since 1.8
*/
public final class Optional<T> {
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>();
/**
* If non-null, the value; if null, indicates no value is present
*/
private final T value;
/**
* Constructs an empty instance.
*
* @implNote Generally only one empty instance, {@link Optional#EMPTY},
* should exist per VM.
*/
private Optional() {
this.value = null;
}
/**
* Returns an empty {@code Optional} instance. No value is present for this
* Optional.
*
* @apiNote Though it may be tempting to do so, avoid testing if an object
* is empty by comparing with {@code ==} against instances returned by
* {@code Option.empty()}. There is no guarantee that it is a singleton.
* Instead, use {@link #isPresent()}.
*
* @param <T> Type of the non-existent value
* @return an empty {@code Optional}
*/
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
/**
* Constructs an instance with the value present.
*
* @param value the non-null value to be present
* @throws NullPointerException if value is null
*/
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
/**
* Returns an {@code Optional} with the specified present non-null value.
*
* @param <T> the class of the value
* @param value the value to be present, which must be non-null
* @return an {@code Optional} with the value present
* @throws NullPointerException if value is null
*/
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
/**
* Returns an {@code Optional} describing the specified value, if non-null,
* otherwise returns an empty {@code Optional}.
*
* @param <T> the class of the value
* @param value the possibly-null value to describe
* @return an {@code Optional} with a present value if the specified value
* is non-null, otherwise an empty {@code Optional}
*/
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
/**
* If a value is present in this {@code Optional}, returns the value,
* otherwise throws {@code NoSuchElementException}.
*
* @return the non-null value held by this {@code Optional}
* @throws NoSuchElementException if there is no value present
*
* @see Optional#isPresent()
*/
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
/**
* Return {@code true} if there is a value present, otherwise {@code false}.
*
* @return {@code true} if there is a value present, otherwise {@code false}
*/
public boolean isPresent() {
return value != null;
}
/**
* Return the value if present, otherwise return {@code other}.
*
* @param other the value to be returned if there is no value present, may
* be null
* @return the value, if present, otherwise {@code other}
*/
public T orElse(T other) {
return value != null ? value : other;
}
/**
* Indicates whether some other object is "equal to" this Optional. The
* other object is considered equal if:
* <ul>
* <li>it is also an {@code Optional} and;
* <li>both instances have no value present or;
* <li>the present values are "equal to" each other via {@code equals()}.
* </ul>
*
* @param obj an object to be tested for equality
* @return {code true} if the other object is "equal to" this object
* otherwise {@code false}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Optional)) {
return false;
}
Optional<?> other = (Optional<?>) obj;
return (value == other.value) || (value != null && value.equals(other.value));
}
/**
* Returns the hash code value of the present value, if any, or 0 (zero) if
* no value is present.
*
* @return hash code value of the present value or 0 if no value is present
*/
@Override
public int hashCode() {
return value != null ? value.hashCode() : 0;
}
/**
* Returns a non-empty string representation of this Optional suitable for
* debugging. The exact presentation format is unspecified and may vary
* between implementations and versions.
*
* @implSpec If a value is present the result must include its string
* representation in the result. Empty and present Optionals must be
* unambiguously differentiable.
*
* @return the string representation of this instance
*/
@Override
public String toString() {
return value != null
? String.format("Optional[%s]", value)
: "Optional.empty";
}
}

View File

@ -12,8 +12,6 @@ import android.media.MediaPlayer;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@ -37,6 +35,7 @@ import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.Optional;
import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils;
import io.reactivex.Maybe; import io.reactivex.Maybe;
import io.reactivex.MaybeOnSubscribe; import io.reactivex.MaybeOnSubscribe;
@ -105,6 +104,7 @@ public abstract class PlaybackController {
} }
private synchronized void initServiceRunning() { private synchronized void initServiceRunning() {
Log.v(TAG, "initServiceRunning()");
if (initialized) { if (initialized) {
return; return;
} }
@ -187,22 +187,15 @@ public abstract class PlaybackController {
serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent) serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> { .subscribe(optionalIntent -> {
boolean bound = false; boolean bound = false;
if (!PlaybackService.started) { if (optionalIntent.isPresent()) {
if (intent != null) { Log.d(TAG, "Calling bind service");
Log.d(TAG, "Calling start service"); bound = activity.bindService(optionalIntent.get(), mConnection, 0);
ContextCompat.startForegroundService(activity, intent);
bound = activity.bindService(intent, mConnection, 0);
} else {
status = PlayerStatus.STOPPED;
setupGUI();
handleStatus();
}
} else { } else {
Log.d(TAG, "PlaybackService is running, trying to connect without start command."); status = PlayerStatus.STOPPED;
bound = activity.bindService(new Intent(activity, PlaybackService.class), setupGUI();
mConnection, 0); handleStatus();
} }
Log.d(TAG, "Result for service binding: " + bound); Log.d(TAG, "Result for service binding: " + bound);
}, error -> Log.e(TAG, Log.getStackTraceString(error))); }, error -> Log.e(TAG, Log.getStackTraceString(error)));
@ -212,24 +205,26 @@ public abstract class PlaybackController {
* Returns an intent that starts the PlaybackService and plays the last * Returns an intent that starts the PlaybackService and plays the last
* played media or null if no last played media could be found. * played media or null if no last played media could be found.
*/ */
@Nullable private Intent getPlayLastPlayedMediaIntent() { @NonNull
private Optional<Intent> getPlayLastPlayedMediaIntent() {
Log.d(TAG, "Trying to restore last played media"); Log.d(TAG, "Trying to restore last played media");
Playable media = PlayableUtils.createInstanceFromPreferences(activity); Playable media = PlayableUtils.createInstanceFromPreferences(activity);
if (media == null) { if (media == null) {
Log.d(TAG, "No last played media found"); Log.d(TAG, "No last played media found");
return null; return Optional.empty();
} }
boolean fileExists = media.localFileAvailable(); boolean fileExists = media.localFileAvailable();
boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream(); boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
if (!fileExists && !lastIsStream && media instanceof FeedMedia) { if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media); DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media);
} }
return new PlaybackServiceStarter(activity, media) return Optional.of(new PlaybackServiceStarter(activity, media)
.startWhenPrepared(false) .startWhenPrepared(false)
.shouldStream(lastIsStream || !fileExists) .shouldStream(lastIsStream || !fileExists)
.getIntent(); .getIntent());
} }
@ -587,7 +582,8 @@ public abstract class PlaybackController {
.startWhenPrepared(true) .startWhenPrepared(true)
.streamIfLastWasStream() .streamIfLastWasStream()
.start(); .start();
Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!"); Log.d(TAG, "Play/Pause button was pressed, but playbackservice was null - " +
"it is likely to have been released by Android system. Restarting it.");
return; return;
} }
switch (status) { switch (status) {
@ -764,6 +760,7 @@ public abstract class PlaybackController {
} }
public void notifyVideoSurfaceAbandoned() { public void notifyVideoSurfaceAbandoned() {
Log.v(TAG, "notifyVideoSurfaceAbandoned() - hasPlaybackService=" + (playbackService != null));
if (playbackService != null) { if (playbackService != null) {
playbackService.notifyVideoSurfaceAbandoned(); playbackService.notifyVideoSurfaceAbandoned();
} }
@ -784,6 +781,7 @@ public abstract class PlaybackController {
} }
private void initServiceNotRunning() { private void initServiceNotRunning() {
Log.v(TAG, "initServiceNotRunning()");
mediaLoader = Maybe.create((MaybeOnSubscribe<Playable>) emitter -> { mediaLoader = Maybe.create((MaybeOnSubscribe<Playable>) emitter -> {
Playable media = getMedia(); Playable media = getMedia();
if (media != null) { if (media != null) {

View File

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

View File

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