Revert "Merge pull request #2954 from orionlee/bugfix_phantom_notification_rework_2716"
This reverts commit643173de14
, reversing changes made tof2d103736d
.
This commit is contained in:
parent
b617397397
commit
3fca616e30
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
@ -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;
|
||||
|
||||
NotificationCompat.Builder notificationBuilder = createBasicNotification();
|
||||
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));
|
||||
|
@ -343,6 +344,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
|||
Log.d(TAG, "Service is about to be destroyed");
|
||||
stopForeground(true);
|
||||
isRunning = false;
|
||||
started = false;
|
||||
currentMediaType = MediaType.UNKNOWN;
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
@ -364,6 +366,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,32 +467,37 @@ 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 (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.
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
setupNotification(playable);
|
||||
}
|
||||
|
||||
return Service.START_NOT_STICKY;
|
||||
|
@ -559,23 +571,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 +592,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 +607,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 +673,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 +704,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
|||
|
||||
case ERROR:
|
||||
writePlaybackPreferencesNoMediaPlaying();
|
||||
stopService();
|
||||
break;
|
||||
|
||||
}
|
||||
|
@ -701,7 +717,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
|||
|
||||
@Override
|
||||
public void shouldStop() {
|
||||
serviceManager.stopService();
|
||||
stopService();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -750,6 +766,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
|||
}
|
||||
sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
|
||||
writePlaybackPreferencesNoMediaPlaying();
|
||||
stopService();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -833,6 +850,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 +1066,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 +1129,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 +1183,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,14 +1206,21 @@ 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();
|
||||
if (!started) {
|
||||
stopService();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -1205,12 +1229,12 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
|||
|
||||
@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;
|
||||
}
|
||||
|
@ -1235,9 +1259,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
|||
}
|
||||
|
||||
PlayerStatus playerStatus = mediaPlayer.getPlayerStatus();
|
||||
Log.v(TAG, "notificationSetupTask: playerStatus=" + playerStatus);
|
||||
|
||||
if (!Thread.currentThread().isInterrupted() && isStarted()) {
|
||||
if (!Thread.currentThread().isInterrupted() && started) {
|
||||
String contentText = playable.getEpisodeTitle();
|
||||
String contentTitle = playable.getFeedTitle();
|
||||
Notification notification;
|
||||
|
@ -1329,33 +1352,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
|||
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);
|
||||
stopForeground(false);
|
||||
NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
mNotificationManager.notify(NOTIFICATION_ID, 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);
|
||||
}
|
||||
|
||||
};
|
||||
notificationSetupThread = new Thread(notificationSetupTask);
|
||||
notificationSetupThread.start();
|
||||
|
@ -1550,7 +1555,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 +1853,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 +1893,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 +1926,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();
|
||||
|
||||
}
|
||||
|
|
|
@ -1,213 +0,0 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -12,6 +12,8 @@ import android.media.MediaPlayer;
|
|||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
@ -35,7 +37,6 @@ import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
|
|||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.util.Converter;
|
||||
import de.danoeh.antennapod.core.util.Optional;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.MaybeOnSubscribe;
|
||||
|
@ -104,7 +105,6 @@ public abstract class PlaybackController {
|
|||
}
|
||||
|
||||
private synchronized void initServiceRunning() {
|
||||
Log.v(TAG, "initServiceRunning()");
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
@ -187,15 +187,22 @@ public abstract class PlaybackController {
|
|||
serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(optionalIntent -> {
|
||||
.subscribe(intent -> {
|
||||
boolean bound = false;
|
||||
if (optionalIntent.isPresent()) {
|
||||
Log.d(TAG, "Calling bind service");
|
||||
bound = activity.bindService(optionalIntent.get(), mConnection, 0);
|
||||
if (!PlaybackService.started) {
|
||||
if (intent != null) {
|
||||
Log.d(TAG, "Calling start service");
|
||||
ContextCompat.startForegroundService(activity, intent);
|
||||
bound = activity.bindService(intent, mConnection, 0);
|
||||
} else {
|
||||
status = PlayerStatus.STOPPED;
|
||||
setupGUI();
|
||||
handleStatus();
|
||||
}
|
||||
} else {
|
||||
status = PlayerStatus.STOPPED;
|
||||
setupGUI();
|
||||
handleStatus();
|
||||
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)));
|
||||
|
@ -205,26 +212,24 @@ public abstract class PlaybackController {
|
|||
* Returns an intent that starts the PlaybackService and plays the last
|
||||
* played media or null if no last played media could be found.
|
||||
*/
|
||||
@NonNull
|
||||
private Optional<Intent> getPlayLastPlayedMediaIntent() {
|
||||
@Nullable private Intent getPlayLastPlayedMediaIntent() {
|
||||
Log.d(TAG, "Trying to restore last played media");
|
||||
Playable media = PlayableUtils.createInstanceFromPreferences(activity);
|
||||
if (media == null) {
|
||||
Log.d(TAG, "No last played media found");
|
||||
return Optional.empty();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
boolean fileExists = media.localFileAvailable();
|
||||
boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream();
|
||||
if (!fileExists && !lastIsStream && media instanceof FeedMedia) {
|
||||
DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media);
|
||||
}
|
||||
|
||||
return Optional.of(new PlaybackServiceStarter(activity, media)
|
||||
return new PlaybackServiceStarter(activity, media)
|
||||
.startWhenPrepared(false)
|
||||
.shouldStream(lastIsStream || !fileExists)
|
||||
.getIntent());
|
||||
.getIntent();
|
||||
}
|
||||
|
||||
|
||||
|
@ -582,8 +587,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 +764,6 @@ public abstract class PlaybackController {
|
|||
}
|
||||
|
||||
public void notifyVideoSurfaceAbandoned() {
|
||||
Log.v(TAG, "notifyVideoSurfaceAbandoned() - hasPlaybackService=" + (playbackService != null));
|
||||
if (playbackService != null) {
|
||||
playbackService.notifyVideoSurfaceAbandoned();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue