diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2024-03-29 21:05:02 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-29 21:05:02 +0100 |
commit | 8accb546850e5d66aaab310c4cd4a528c058386e (patch) | |
tree | cc6ea8bd7304db5d2dae0467bae04d491137cef2 /core/src | |
parent | 2fd73b148d012fba7308c86494689103b8aaace4 (diff) | |
download | AntennaPod-8accb546850e5d66aaab310c4cd4a528c058386e.zip |
Move playback service to module (#7042)
Diffstat (limited to 'core/src')
21 files changed, 1 insertions, 5017 deletions
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index fc673ef5f..73c47d636 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -12,60 +12,4 @@ <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> - <application - android:allowBackup="true" - android:icon="@mipmap/ic_launcher" - android:supportsRtl="true"> - - <service android:name=".service.playback.PlaybackService" - android:label="@string/app_name" - android:enabled="true" - android:exported="true" - android:foregroundServiceType="mediaPlayback" - tools:ignore="ExportedService"> - - <intent-filter> - <action android:name="android.media.browse.MediaBrowserService"/> - <action android:name="de.danoeh.antennapod.intents.PLAYBACK_SERVICE" /> - </intent-filter> - </service> - - <receiver - android:name=".receiver.MediaButtonReceiver" - android:exported="true"> - <intent-filter> - <action android:name="android.intent.action.MEDIA_BUTTON" /> - </intent-filter> - <intent-filter> - <action android:name="de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER" /> - </intent-filter> - </receiver> - - <receiver android:name=".receiver.FeedUpdateReceiver" - android:label="@string/feed_update_receiver_name" - android:exported="true" - tools:ignore="ExportedReceiver" /> <!-- allow feeds update to be triggered by external apps --> - - <service - android:name=".service.QuickSettingsTileService" - android:enabled="true" - android:exported="true" - android:label="@string/app_name" - android:icon="@drawable/ic_notification" - android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> - <intent-filter> - <action android:name="android.service.quicksettings.action.QS_TILE" /> - </intent-filter> - <meta-data android:name="android.service.quicksettings.ACTIVE_TILE" android:value="true" /> - <meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" /> - </service> - </application> - - <queries> - <intent> - <action android:name="android.intent.action.VIEW" /> - <data android:scheme="https" /> - </intent> - </queries> - </manifest> diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java deleted file mode 100644 index 6b9644c41..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ /dev/null @@ -1,23 +0,0 @@ -package de.danoeh.antennapod.core.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; - -/** - * Refreshes all feeds when it receives an intent - */ -public class FeedUpdateReceiver extends BroadcastReceiver { - - private static final String TAG = "FeedUpdateReceiver"; - - @Override - public void onReceive(Context context, Intent intent) { - Log.d(TAG, "Received intent"); - FeedUpdateManager.getInstance().runOnce(context); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java deleted file mode 100644 index 20621fd45..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.danoeh.antennapod.core.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import androidx.core.content.ContextCompat; -import android.util.Log; -import android.view.KeyEvent; - -/** - * Receives media button events. - */ -public class MediaButtonReceiver extends BroadcastReceiver { - private static final String TAG = "MediaButtonReceiver"; - public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.KEYCODE"; - public static final String EXTRA_CUSTOM_ACTION = - "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.CUSTOM_ACTION"; - public static final String EXTRA_SOURCE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.SOURCE"; - public static final String EXTRA_HARDWAREBUTTON - = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.HARDWAREBUTTON"; - public static final String PLAYBACK_SERVICE_INTENT = "de.danoeh.antennapod.intents.PLAYBACK_SERVICE"; - - @Override - public void onReceive(Context context, Intent intent) { - Log.d(TAG, "Received intent"); - if (intent == null || intent.getExtras() == null) { - return; - } - KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - if (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { - Intent serviceIntent = new Intent(PLAYBACK_SERVICE_INTENT); - serviceIntent.setPackage(context.getPackageName()); - serviceIntent.putExtra(EXTRA_KEYCODE, event.getKeyCode()); - serviceIntent.putExtra(EXTRA_SOURCE, event.getSource()); - serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, event.getEventTime() > 0 || event.getDownTime() > 0); - try { - ContextCompat.startForegroundService(context, serviceIntent); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/QuickSettingsTileService.java b/core/src/main/java/de/danoeh/antennapod/core/service/QuickSettingsTileService.java deleted file mode 100644 index f5f096329..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/QuickSettingsTileService.java +++ /dev/null @@ -1,61 +0,0 @@ -package de.danoeh.antennapod.core.service; - -import android.content.ComponentName; -import android.content.Intent; -import android.os.Build; -import android.os.IBinder; -import android.service.quicksettings.Tile; -import android.service.quicksettings.TileService; -import android.util.Log; -import android.view.KeyEvent; - -import androidx.annotation.RequiresApi; - -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; - -@RequiresApi(api = Build.VERSION_CODES.N) -public class QuickSettingsTileService extends TileService { - - private static final String TAG = "QuickSettingsTileSvc"; - - @Override - public void onTileAdded() { - super.onTileAdded(); - updateTile(); - } - - @Override - public void onClick() { - super.onClick(); - sendBroadcast(MediaButtonStarter.createIntent(this, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); - } - - // Update the tile status when TileService.requestListeningState() is called elsewhere - @Override - public void onStartListening() { - super.onStartListening(); - updateTile(); - } - - // Without this, the tile may not be in the correct state after boot - @Override - public IBinder onBind(Intent intent) { - TileService.requestListeningState(this, new ComponentName(this, QuickSettingsTileService.class)); - return super.onBind(intent); - } - - public void updateTile() { - Tile qsTile = getQsTile(); - if (qsTile == null) { - Log.d(TAG, "Ignored call to update QS tile: getQsTile() returned null."); - } else { - boolean isPlaying = PlaybackService.isRunning - && PlaybackPreferences.getCurrentPlayerStatus() - == PlaybackPreferences.PLAYER_STATUS_PLAYING; - qsTile.setState(isPlaying ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); - qsTile.updateTile(); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java deleted file mode 100644 index 9d455ebcc..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java +++ /dev/null @@ -1,405 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.content.Context; -import android.media.audiofx.LoudnessEnhancer; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; -import android.view.SurfaceHolder; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Consumer; - -import androidx.media3.common.C; -import androidx.media3.common.PlaybackException; -import androidx.media3.database.StandaloneDatabaseProvider; -import androidx.media3.datasource.DataSource; -import androidx.media3.datasource.DefaultDataSource; -import androidx.media3.datasource.HttpDataSource; -import androidx.media3.datasource.cache.CacheDataSource; -import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor; -import androidx.media3.datasource.cache.SimpleCache; -import androidx.media3.datasource.okhttp.OkHttpDataSource; -import androidx.media3.exoplayer.DefaultLoadControl; -import androidx.media3.exoplayer.DefaultRenderersFactory; -import androidx.media3.common.Format; -import androidx.media3.common.MediaItem; -import androidx.media3.common.PlaybackParameters; -import androidx.media3.common.Player; -import androidx.media3.exoplayer.SeekParameters; -import androidx.media3.exoplayer.ExoPlayer; -import androidx.media3.common.AudioAttributes; -import androidx.media3.exoplayer.source.MediaSource; -import androidx.media3.exoplayer.source.ProgressiveMediaSource; -import androidx.media3.exoplayer.source.TrackGroupArray; -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; -import androidx.media3.exoplayer.trackselection.ExoTrackSelection; -import androidx.media3.exoplayer.trackselection.MappingTrackSelector; -import androidx.media3.exoplayer.trackselection.TrackSelectionArray; - -import androidx.media3.extractor.DefaultExtractorsFactory; -import androidx.media3.extractor.mp3.Mp3Extractor; -import androidx.media3.ui.DefaultTrackNameProvider; -import androidx.media3.ui.TrackNameProvider; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.net.common.UserAgentInterceptor; -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import de.danoeh.antennapod.net.common.HttpCredentialEncoder; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.model.playback.Playable; -import io.reactivex.Observable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import okhttp3.Call; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.TimeUnit; - -public class ExoPlayerWrapper { - public static final int BUFFERING_STARTED = -1; - public static final int BUFFERING_ENDED = -2; - private static final String TAG = "ExoPlayerWrapper"; - - private final Context context; - private final Disposable bufferingUpdateDisposable; - private ExoPlayer exoPlayer; - private MediaSource mediaSource; - private Runnable audioSeekCompleteListener; - private Runnable audioCompletionListener; - private Consumer<String> audioErrorListener; - private Consumer<Integer> bufferingUpdateListener; - private PlaybackParameters playbackParameters; - private DefaultTrackSelector trackSelector; - private SimpleCache simpleCache; - @Nullable - private LoudnessEnhancer loudnessEnhancer = null; - - ExoPlayerWrapper(Context context) { - this.context = context; - createPlayer(); - playbackParameters = exoPlayer.getPlaybackParameters(); - bufferingUpdateDisposable = Observable.interval(2, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(tickNumber -> { - if (bufferingUpdateListener != null) { - bufferingUpdateListener.accept(exoPlayer.getBufferedPercentage()); - } - }); - } - - private void createPlayer() { - DefaultLoadControl.Builder loadControl = new DefaultLoadControl.Builder(); - loadControl.setBufferDurationsMs(30000, 120000, - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); - loadControl.setBackBuffer(UserPreferences.getRewindSecs() * 1000 + 500, true); - trackSelector = new DefaultTrackSelector(context); - exoPlayer = new ExoPlayer.Builder(context, new DefaultRenderersFactory(context)) - .setTrackSelector(trackSelector) - .setLoadControl(loadControl.build()) - .build(); - exoPlayer.setSeekParameters(SeekParameters.EXACT); - exoPlayer.addListener(new Player.Listener() { - @Override - public void onPlaybackStateChanged(@Player.State int playbackState) { - if (audioCompletionListener != null && playbackState == Player.STATE_ENDED) { - audioCompletionListener.run(); - } else if (bufferingUpdateListener != null && playbackState == Player.STATE_BUFFERING) { - bufferingUpdateListener.accept(BUFFERING_STARTED); - } else if (bufferingUpdateListener != null) { - bufferingUpdateListener.accept(BUFFERING_ENDED); - } - } - - @Override - public void onPlayerError(@NonNull PlaybackException error) { - if (audioErrorListener != null) { - if (NetworkUtils.wasDownloadBlocked(error)) { - audioErrorListener.accept(context.getString(R.string.download_error_blocked)); - } else { - Throwable cause = error.getCause(); - if (cause instanceof HttpDataSource.HttpDataSourceException) { - if (cause.getCause() != null) { - cause = cause.getCause(); - } - } - if (cause != null && "Source error".equals(cause.getMessage())) { - cause = cause.getCause(); - } - audioErrorListener.accept(cause != null ? cause.getMessage() : error.getMessage()); - } - } - } - - @Override - public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, - @NonNull Player.PositionInfo newPosition, - @Player.DiscontinuityReason int reason) { - if (audioSeekCompleteListener != null && reason == Player.DISCONTINUITY_REASON_SEEK) { - audioSeekCompleteListener.run(); - } - } - - @Override - public void onAudioSessionIdChanged(int audioSessionId) { - initLoudnessEnhancer(audioSessionId); - } - }); - simpleCache = new SimpleCache(new File(context.getCacheDir(), "streaming"), - new LeastRecentlyUsedCacheEvictor(50 * 1024 * 1024), new StandaloneDatabaseProvider(context)); - initLoudnessEnhancer(exoPlayer.getAudioSessionId()); - } - - public int getCurrentPosition() { - return (int) exoPlayer.getCurrentPosition(); - } - - public float getCurrentSpeedMultiplier() { - return playbackParameters.speed; - } - - public boolean getCurrentSkipSilence() { - return exoPlayer.getSkipSilenceEnabled(); - } - - public int getDuration() { - if (exoPlayer.getDuration() == C.TIME_UNSET) { - return Playable.INVALID_TIME; - } - return (int) exoPlayer.getDuration(); - } - - public boolean isPlaying() { - return exoPlayer.getPlayWhenReady(); - } - - public void pause() { - exoPlayer.pause(); - } - - public void prepare() throws IllegalStateException { - exoPlayer.setMediaSource(mediaSource, false); - exoPlayer.prepare(); - } - - public void release() { - bufferingUpdateDisposable.dispose(); - if (exoPlayer != null) { - exoPlayer.release(); - } - if (simpleCache != null) { - simpleCache.release(); - simpleCache = null; - } - audioSeekCompleteListener = null; - audioCompletionListener = null; - audioErrorListener = null; - bufferingUpdateListener = null; - } - - public void reset() { - exoPlayer.release(); - if (simpleCache != null) { - simpleCache.release(); - simpleCache = null; - } - createPlayer(); - } - - public void seekTo(int i) throws IllegalStateException { - exoPlayer.seekTo(i); - if (audioSeekCompleteListener != null) { - audioSeekCompleteListener.run(); - } - } - - public void setAudioStreamType(int i) { - AudioAttributes a = exoPlayer.getAudioAttributes(); - AudioAttributes.Builder b = new AudioAttributes.Builder(); - b.setContentType(i); - b.setFlags(a.flags); - b.setUsage(a.usage); - exoPlayer.setAudioAttributes(b.build(), false); - } - - public void setDataSource(String s, String user, String password) - throws IllegalArgumentException, IllegalStateException { - Log.d(TAG, "setDataSource: " + s); - final OkHttpDataSource.Factory httpDataSourceFactory = - new OkHttpDataSource.Factory((Call.Factory) AntennapodHttpClient.getHttpClient()) - .setUserAgent(UserAgentInterceptor.USER_AGENT); - - if (!TextUtils.isEmpty(user) && !TextUtils.isEmpty(password)) { - final HashMap<String, String> requestProperties = new HashMap<>(); - requestProperties.put( - "Authorization", - HttpCredentialEncoder.encode(user, password, "ISO-8859-1") - ); - httpDataSourceFactory.setDefaultRequestProperties(requestProperties); - } - DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, httpDataSourceFactory); - if (s.startsWith("http")) { - dataSourceFactory = new CacheDataSource.Factory() - .setCache(simpleCache) - .setUpstreamDataSourceFactory(httpDataSourceFactory); - } - DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); - extractorsFactory.setConstantBitrateSeekingEnabled(true); - extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_DISABLE_ID3_METADATA); - ProgressiveMediaSource.Factory f = new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory); - final MediaItem mediaItem = MediaItem.fromUri(Uri.parse(s)); - mediaSource = f.createMediaSource(mediaItem); - } - - public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { - setDataSource(s, null, null); - } - - public void setDisplay(SurfaceHolder sh) { - exoPlayer.setVideoSurfaceHolder(sh); - } - - public void setPlaybackParams(float speed, boolean skipSilence) { - playbackParameters = new PlaybackParameters(speed, playbackParameters.pitch); - exoPlayer.setSkipSilenceEnabled(skipSilence); - exoPlayer.setPlaybackParameters(playbackParameters); - } - - public void setVolume(float v, float v1) { - if (v > 1) { - exoPlayer.setVolume(1f); - if (loudnessEnhancer != null) { - loudnessEnhancer.setEnabled(true); - loudnessEnhancer.setTargetGain((int) (1000 * (v - 1))); - } - } else { - exoPlayer.setVolume(v); - if (loudnessEnhancer != null) { - loudnessEnhancer.setEnabled(false); - } - } - } - - public void start() { - exoPlayer.play(); - // Can't set params when paused - so always set it on start in case they changed - exoPlayer.setPlaybackParameters(playbackParameters); - } - - public void stop() { - exoPlayer.stop(); - } - - public List<String> getAudioTracks() { - List<String> trackNames = new ArrayList<>(); - TrackNameProvider trackNameProvider = new DefaultTrackNameProvider(context.getResources()); - for (Format format : getFormats()) { - trackNames.add(trackNameProvider.getTrackName(format)); - } - return trackNames; - } - - private List<Format> getFormats() { - List<Format> formats = new ArrayList<>(); - MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (trackInfo == null) { - return Collections.emptyList(); - } - TrackGroupArray trackGroups = trackInfo.getTrackGroups(getAudioRendererIndex()); - for (int i = 0; i < trackGroups.length; i++) { - formats.add(trackGroups.get(i).getFormat(0)); - } - return formats; - } - - public void setAudioTrack(int track) { - MappingTrackSelector.MappedTrackInfo trackInfo = trackSelector.getCurrentMappedTrackInfo(); - if (trackInfo == null) { - return; - } - TrackGroupArray trackGroups = trackInfo.getTrackGroups(getAudioRendererIndex()); - DefaultTrackSelector.SelectionOverride override = new DefaultTrackSelector.SelectionOverride(track, 0); - DefaultTrackSelector.Parameters params = trackSelector.buildUponParameters() - .setSelectionOverride(getAudioRendererIndex(), trackGroups, override).build(); - trackSelector.setParameters(params); - } - - private int getAudioRendererIndex() { - for (int i = 0; i < exoPlayer.getRendererCount(); i++) { - if (exoPlayer.getRendererType(i) == C.TRACK_TYPE_AUDIO) { - return i; - } - } - return -1; - } - - public int getSelectedAudioTrack() { - TrackSelectionArray trackSelections = exoPlayer.getCurrentTrackSelections(); - List<Format> availableFormats = getFormats(); - for (int i = 0; i < trackSelections.length; i++) { - ExoTrackSelection track = (ExoTrackSelection) trackSelections.get(i); - if (track == null) { - continue; - } - if (availableFormats.contains(track.getSelectedFormat())) { - return availableFormats.indexOf(track.getSelectedFormat()); - } - } - return -1; - } - - void setOnCompletionListener(Runnable audioCompletionListener) { - this.audioCompletionListener = audioCompletionListener; - } - - void setOnSeekCompleteListener(Runnable audioSeekCompleteListener) { - this.audioSeekCompleteListener = audioSeekCompleteListener; - } - - void setOnErrorListener(Consumer<String> audioErrorListener) { - this.audioErrorListener = audioErrorListener; - } - - int getVideoWidth() { - if (exoPlayer.getVideoFormat() == null) { - return 0; - } - return exoPlayer.getVideoFormat().width; - } - - int getVideoHeight() { - if (exoPlayer.getVideoFormat() == null) { - return 0; - } - return exoPlayer.getVideoFormat().height; - } - - void setOnBufferingUpdateListener(Consumer<Integer> bufferingUpdateListener) { - this.bufferingUpdateListener = bufferingUpdateListener; - } - - private void initLoudnessEnhancer(int audioStreamId) { - if (!VolumeAdaptionSetting.isBoostSupported()) { - return; - } - - LoudnessEnhancer newEnhancer = new LoudnessEnhancer(audioStreamId); - LoudnessEnhancer oldEnhancer = this.loudnessEnhancer; - if (oldEnhancer != null) { - newEnhancer.setEnabled(oldEnhancer.getEnabled()); - if (oldEnhancer.getEnabled()) { - newEnhancer.setTargetGain((int) oldEnhancer.getTargetGain()); - } - oldEnhancer.release(); - } - - this.loudnessEnhancer = newEnhancer; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java deleted file mode 100644 index 9ec1b5265..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ /dev/null @@ -1,773 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.app.UiModeManager; -import android.content.Context; -import android.content.res.Configuration; -import android.media.AudioManager; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.util.Pair; -import android.view.SurfaceHolder; -import androidx.annotation.NonNull; -import androidx.media.AudioAttributesCompat; -import androidx.media.AudioFocusRequestCompat; -import androidx.media.AudioManagerCompat; -import de.danoeh.antennapod.event.PlayerErrorEvent; -import de.danoeh.antennapod.event.playback.BufferUpdateEvent; -import de.danoeh.antennapod.event.playback.SpeedChangedEvent; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.playback.base.PlayerStatus; -import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils; -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Manages the MediaPlayer object of the PlaybackService. - */ -public class LocalPSMP extends PlaybackServiceMediaPlayer { - private static final String TAG = "LclPlaybackSvcMPlayer"; - - private final AudioManager audioManager; - - private volatile PlayerStatus statusBeforeSeeking; - private volatile ExoPlayerWrapper mediaPlayer; - private volatile Playable media; - - private volatile boolean stream; - private volatile MediaType mediaType; - private final AtomicBoolean startWhenPrepared; - private volatile boolean pausedBecauseOfTransientAudiofocusLoss; - private volatile Pair<Integer, Integer> videoSize; - private final AudioFocusRequestCompat audioFocusRequest; - private final Handler audioFocusCanceller; - private boolean isShutDown = false; - private CountDownLatch seekLatch; - - public LocalPSMP(@NonNull Context context, - @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) { - super(context, callback); - this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - this.startWhenPrepared = new AtomicBoolean(false); - audioFocusCanceller = new Handler(Looper.getMainLooper()); - mediaPlayer = null; - statusBeforeSeeking = null; - pausedBecauseOfTransientAudiofocusLoss = false; - mediaType = MediaType.UNKNOWN; - videoSize = null; - - AudioAttributesCompat audioAttributes = new AudioAttributesCompat.Builder() - .setUsage(AudioAttributesCompat.USAGE_MEDIA) - .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH) - .build(); - audioFocusRequest = new AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) - .setAudioAttributes(audioAttributes) - .setOnAudioFocusChangeListener(audioFocusChangeListener) - .setWillPauseWhenDucked(true) - .build(); - } - - /** - * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing - * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will - * not do anything. - * Whether playback starts immediately depends on the given parameters. See below for more details. - * <p/> - * States: - * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. - * <p/> - * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If - * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. - * <p/> - * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object - * will enter the ERROR state. - * <p/> - * This method is executed on an internal executor service. - * - * @param playable The Playable object that is supposed to be played. This parameter must not be null. - * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via - * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by - * the Android MediaPlayer via getStreamUrl. - * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the - * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared - * for playback immediately (see 'prepareImmediately' parameter for more details) - * @param prepareImmediately Set to true if the method should also prepare the episode for playback. - */ - @Override - public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - Log.d(TAG, "playMediaObject(...)"); - try { - playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); - } catch (RuntimeException e) { - e.printStackTrace(); - throw e; - } - } - - /** - * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if - * the given playable parameter is the same object as the currently playing media. - * <p/> - * This method requires the playerLock and is executed on the caller's thread. - * - * @see #playMediaObject(Playable, boolean, boolean, boolean) - */ - private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - if (media != null) { - if (!forceReset && media.getIdentifier().equals(playable.getIdentifier()) - && playerStatus == PlayerStatus.PLAYING) { - // episode is already playing -> ignore method call - Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); - return; - } else { - // stop playback of this episode - if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { - mediaPlayer.stop(); - } - // set temporarily to pause in order to update list with current position - if (playerStatus == PlayerStatus.PLAYING) { - callback.onPlaybackPause(media, getPosition()); - } - - if (!media.getIdentifier().equals(playable.getIdentifier())) { - final Playable oldMedia = media; - callback.onPostPlayback(oldMedia, false, false, true); - } - - setPlayerStatus(PlayerStatus.INDETERMINATE, null); - } - } - - this.media = playable; - this.stream = stream; - this.mediaType = media.getMediaType(); - this.videoSize = null; - createMediaPlayer(); - LocalPSMP.this.startWhenPrepared.set(startWhenPrepared); - setPlayerStatus(PlayerStatus.INITIALIZING, media); - try { - callback.ensureMediaInfoLoaded(media); - callback.onMediaChanged(false); - setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), - PlaybackSpeedUtils.getCurrentSkipSilencePreference(media) - == FeedPreferences.SkipSilence.AGGRESSIVE); - if (stream) { - if (playable instanceof FeedMedia) { - FeedMedia feedMedia = (FeedMedia) playable; - FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences(); - mediaPlayer.setDataSource( - media.getStreamUrl(), - preferences.getUsername(), - preferences.getPassword()); - } else { - mediaPlayer.setDataSource(media.getStreamUrl()); - } - } else if (media.getLocalFileUrl() != null && new File(media.getLocalFileUrl()).canRead()) { - mediaPlayer.setDataSource(media.getLocalFileUrl()); - } else { - throw new IOException("Unable to read local file " + media.getLocalFileUrl()); - } - UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); - if (uiModeManager.getCurrentModeType() != Configuration.UI_MODE_TYPE_CAR) { - setPlayerStatus(PlayerStatus.INITIALIZED, media); - } - - if (prepareImmediately) { - setPlayerStatus(PlayerStatus.PREPARING, media); - mediaPlayer.prepare(); - onPrepared(startWhenPrepared); - } - - } catch (IOException | IllegalStateException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - EventBus.getDefault().postSticky(new PlayerErrorEvent(e.getLocalizedMessage())); - } - } - - /** - * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. - * nothing will happen. - * <p/> - * This method is executed on an internal executor service. - */ - @Override - public void resume() { - if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - int focusGained = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest); - - if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Log.d(TAG, "Audiofocus successfully requested"); - Log.d(TAG, "Resuming/Starting playback"); - acquireWifiLockIfNecessary(); - - setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), - PlaybackSpeedUtils.getCurrentSkipSilencePreference(media) - == FeedPreferences.SkipSilence.AGGRESSIVE); - setVolume(1.0f, 1.0f); - - if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { - int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( - media.getPosition(), media.getLastPlayedTime()); - seekTo(newPosition); - } - mediaPlayer.start(); - - setPlayerStatus(PlayerStatus.PLAYING, media); - pausedBecauseOfTransientAudiofocusLoss = false; - } else { - Log.e(TAG, "Failed to request audio focus"); - } - } else { - Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); - } - } - - - /** - * Saves the current position and pauses playback. Note that, if audiofocus - * is abandoned, the lockscreen controls will also disapear. - * <p/> - * This method is executed on an internal executor service. - * - * @param abandonFocus is true if the service should release audio focus - * @param reinit is true if service should reinit after pausing if the media - * file is being streamed - */ - @Override - public void pause(final boolean abandonFocus, final boolean reinit) { - releaseWifiLockIfNecessary(); - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Pausing playback."); - mediaPlayer.pause(); - setPlayerStatus(PlayerStatus.PAUSED, media, getPosition()); - - if (abandonFocus) { - abandonAudioFocus(); - pausedBecauseOfTransientAudiofocusLoss = false; - } - if (stream && reinit) { - reinit(); - } - } else { - Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); - } - } - - private void abandonAudioFocus() { - AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequest); - } - - /** - * Prepares media player for playback if the service is in the INITALIZED - * state. - * <p/> - * This method is executed on an internal executor service. - */ - @Override - public void prepare() { - if (playerStatus == PlayerStatus.INITIALIZED) { - Log.d(TAG, "Preparing media player"); - setPlayerStatus(PlayerStatus.PREPARING, media); - mediaPlayer.prepare(); - onPrepared(startWhenPrepared.get()); - } - } - - /** - * Called after media player has been prepared. This method is executed on the caller's thread. - */ - private void onPrepared(final boolean startWhenPrepared) { - if (playerStatus != PlayerStatus.PREPARING) { - throw new IllegalStateException("Player is not in PREPARING state"); - } - Log.d(TAG, "Resource prepared"); - - if (mediaType == MediaType.VIDEO) { - videoSize = new Pair<>(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight()); - } - - // TODO this call has no effect! - if (media.getPosition() > 0) { - seekTo(media.getPosition()); - } - - if (media.getDuration() <= 0) { - Log.d(TAG, "Setting duration of media"); - media.setDuration(mediaPlayer.getDuration()); - } - setPlayerStatus(PlayerStatus.PREPARED, media); - - if (startWhenPrepared) { - resume(); - } - } - - /** - * Resets the media player and moves it into INITIALIZED state. - * <p/> - * This method is executed on an internal executor service. - */ - @Override - public void reinit() { - Log.d(TAG, "reinit()"); - releaseWifiLockIfNecessary(); - if (media != null) { - playMediaObject(media, true, stream, startWhenPrepared.get(), false); - } else if (mediaPlayer != null) { - mediaPlayer.reset(); - } else { - Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); - } - } - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * Invalid time values (< 0) will be ignored. - * <p/> - * This method is executed on an internal executor service. - */ - @Override - public void seekTo(int t) { - if (t < 0) { - t = 0; - } - - if (t >= getDuration()) { - Log.d(TAG, "Seek reached end of file, skipping to next episode"); - endPlayback(true, true, true, true); - return; - } - - if (playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { - if(seekLatch != null && seekLatch.getCount() > 0) { - try { - seekLatch.await(3, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - seekLatch = new CountDownLatch(1); - statusBeforeSeeking = playerStatus; - setPlayerStatus(PlayerStatus.SEEKING, media, getPosition()); - mediaPlayer.seekTo(t); - if (statusBeforeSeeking == PlayerStatus.PREPARED) { - media.setPosition(t); - } - try { - seekLatch.await(3, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } else if (playerStatus == PlayerStatus.INITIALIZED) { - media.setPosition(t); - startWhenPrepared.set(false); - prepare(); - } - } - - /** - * Seek a specific position from the current position - * - * @param d offset from current position (positive or negative) - */ - @Override - public void seekDelta(final int d) { - int currentPosition = getPosition(); - if (currentPosition != Playable.INVALID_TIME) { - seekTo(currentPosition + d); - } else { - Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); - } - } - - /** - * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. - */ - @Override - public int getDuration() { - int retVal = Playable.INVALID_TIME; - if (playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { - retVal = mediaPlayer.getDuration(); - } - if (retVal <= 0 && media != null && media.getDuration() > 0) { - retVal = media.getDuration(); - } - return retVal; - } - - /** - * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. - */ - @Override - public int getPosition() { - int retVal = Playable.INVALID_TIME; - if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) { - retVal = mediaPlayer.getCurrentPosition(); - } - if (retVal <= 0 && media != null && media.getPosition() >= 0) { - retVal = media.getPosition(); - } - return retVal; - } - - @Override - public boolean isStartWhenPrepared() { - return startWhenPrepared.get(); - } - - @Override - public void setStartWhenPrepared(boolean startWhenPrepared) { - this.startWhenPrepared.set(startWhenPrepared); - } - - /** - * Sets the playback speed. - * This method is executed on an internal executor service. - */ - @Override - public void setPlaybackParams(final float speed, final boolean skipSilence) { - Log.d(TAG, "Playback speed was set to " + speed); - EventBus.getDefault().post(new SpeedChangedEvent(speed)); - mediaPlayer.setPlaybackParams(speed, skipSilence); - } - - /** - * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. - */ - @Override - public float getPlaybackSpeed() { - float retVal = 1; - if ((playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.INITIALIZED - || playerStatus == PlayerStatus.PREPARED)) { - retVal = mediaPlayer.getCurrentSpeedMultiplier(); - } - return retVal; - } - - @Override - public boolean getSkipSilence() { - boolean retVal = false; - if ((playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.INITIALIZED - || playerStatus == PlayerStatus.PREPARED)) { - retVal = mediaPlayer.getCurrentSkipSilence(); - } - return retVal; - } - - /** - * Sets the playback volume. - * This method is executed on an internal executor service. - */ - @Override - public void setVolume(float volumeLeft, float volumeRight) { - Playable playable = getPlayable(); - if (playable instanceof FeedMedia) { - FeedMedia feedMedia = (FeedMedia) playable; - FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences(); - VolumeAdaptionSetting volumeAdaptionSetting = preferences.getVolumeAdaptionSetting(); - float adaptionFactor = volumeAdaptionSetting.getAdaptionFactor(); - volumeLeft *= adaptionFactor; - volumeRight *= adaptionFactor; - } - mediaPlayer.setVolume(volumeLeft, volumeRight); - Log.d(TAG, "Media player volume was set to " + volumeLeft + " " + volumeRight); - } - - @Override - public MediaType getCurrentMediaType() { - return mediaType; - } - - @Override - public boolean isStreaming() { - return stream; - } - - /** - * Releases internally used resources. This method should only be called when the object is not used anymore. - */ - @Override - public void shutdown() { - if (mediaPlayer != null) { - try { - clearMediaPlayerListeners(); - if (mediaPlayer.isPlaying()) { - mediaPlayer.stop(); - } - } catch (Exception e) { - e.printStackTrace(); - } - mediaPlayer.release(); - mediaPlayer = null; - playerStatus = PlayerStatus.STOPPED; - } - isShutDown = true; - abandonAudioFocus(); - releaseWifiLockIfNecessary(); - } - - @Override - public void setVideoSurface(final SurfaceHolder surface) { - if (mediaPlayer != null) { - mediaPlayer.setDisplay(surface); - } - } - - @Override - public void resetVideoSurface() { - if (mediaType == MediaType.VIDEO) { - Log.d(TAG, "Resetting video surface"); - mediaPlayer.setDisplay(null); - reinit(); - } else { - Log.e(TAG, "Resetting video surface for media of Audio type"); - } - } - - /** - * Return width and height of the currently playing video as a pair. - * - * @return Width and height as a Pair or null if the video size could not be determined. The method might still - * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return - * invalid values. - */ - @Override - public Pair<Integer, Integer> getVideoSize() { - if (mediaPlayer != null && playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO) { - videoSize = new Pair<>(mediaPlayer.getVideoWidth(), mediaPlayer.getVideoHeight()); - } - return videoSize; - } - - /** - * Returns the current media, if you need the media and the player status together, you should - * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition - * could result in nonsensical results (like a status of PLAYING, but a null playable) - * @return the current media. May be null - */ - @Override - public Playable getPlayable() { - return media; - } - - @Override - protected void setPlayable(Playable playable) { - media = playable; - } - - public List<String> getAudioTracks() { - return mediaPlayer.getAudioTracks(); - } - - public void setAudioTrack(int track) { - mediaPlayer.setAudioTrack(track); - } - - public int getSelectedAudioTrack() { - return mediaPlayer.getSelectedAudioTrack(); - } - - private void createMediaPlayer() { - if (mediaPlayer != null) { - mediaPlayer.release(); - } - if (media == null) { - mediaPlayer = null; - playerStatus = PlayerStatus.STOPPED; - return; - } - - mediaPlayer = new ExoPlayerWrapper(context); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - setMediaPlayerListeners(mediaPlayer); - } - - private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { - - @Override - public void onAudioFocusChange(final int focusChange) { - if (isShutDown) { - return; - } - if (!PlaybackService.isRunning) { - abandonAudioFocus(); - Log.d(TAG, "onAudioFocusChange: PlaybackService is no longer running"); - return; - } - - if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { - Log.d(TAG, "Lost audio focus"); - pause(true, false); - callback.shouldStop(); - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK - && !UserPreferences.shouldPauseForFocusLoss()) { - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - setVolume(0.25f, 0.25f); - pausedBecauseOfTransientAudiofocusLoss = false; - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT - || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - if (playerStatus == PlayerStatus.PLAYING) { - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); - mediaPlayer.pause(); // Pause without telling the PlaybackService - pausedBecauseOfTransientAudiofocusLoss = true; - - audioFocusCanceller.removeCallbacksAndMessages(null); - audioFocusCanceller.postDelayed(() -> { - if (pausedBecauseOfTransientAudiofocusLoss) { - // Still did not get back the audio focus. Now actually pause. - pause(true, false); - } - }, 30000); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - Log.d(TAG, "Gained audio focus"); - audioFocusCanceller.removeCallbacksAndMessages(null); - if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now - mediaPlayer.start(); - } else { // we ducked => raise audio level back - setVolume(1.0f, 1.0f); - } - pausedBecauseOfTransientAudiofocusLoss = false; - } - } - }; - - - @Override - protected void endPlayback(final boolean hasEnded, final boolean wasSkipped, - final boolean shouldContinue, final boolean toStoppedState) { - releaseWifiLockIfNecessary(); - - boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - - // we're relying on the position stored in the Playable object for post-playback processing - if (media != null) { - int position = getPosition(); - if (position >= 0) { - media.setPosition(position); - } - } - - if (mediaPlayer != null) { - mediaPlayer.reset(); - } - - abandonAudioFocus(); - - final Playable currentMedia = media; - Playable nextMedia = null; - - if (shouldContinue) { - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - nextMedia = callback.getNextInQueue(currentMedia); - if (nextMedia != null) { - callback.onPlaybackEnded(nextMedia.getMediaType(), false); - // setting media to null signals to playMediaObject() that - // we're taking care of post-playback processing - media = null; - playMediaObject(nextMedia, false, !nextMedia.localFileAvailable(), isPlaying, isPlaying); - } - } - if (shouldContinue || toStoppedState) { - if (nextMedia == null) { - callback.onPlaybackEnded(null, true); - stop(); - } - final boolean hasNext = nextMedia != null; - - callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext); - } else if (isPlaying) { - callback.onPlaybackPause(currentMedia, currentMedia.getPosition()); - } - } - - /** - * Moves the LocalPSMP into STOPPED state. This call is only valid if the player is currently in - * INDETERMINATE state, for example after a call to endPlayback. - * This method will only take care of changing the PlayerStatus of this object! Other tasks like - * abandoning audio focus have to be done with other methods. - */ - private void stop() { - releaseWifiLockIfNecessary(); - - if (playerStatus == PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.STOPPED, null); - } else { - Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); - } - } - - @Override - protected boolean shouldLockWifi() { - return stream; - } - - private void setMediaPlayerListeners(ExoPlayerWrapper mp) { - if (mp == null || media == null) { - return; - } - mp.setOnCompletionListener(() -> endPlayback(true, false, true, true)); - mp.setOnSeekCompleteListener(this::genericSeekCompleteListener); - mp.setOnBufferingUpdateListener(percent -> { - if (percent == ExoPlayerWrapper.BUFFERING_STARTED) { - EventBus.getDefault().post(BufferUpdateEvent.started()); - } else if (percent == ExoPlayerWrapper.BUFFERING_ENDED) { - EventBus.getDefault().post(BufferUpdateEvent.ended()); - } else { - EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)); - } - }); - mp.setOnErrorListener(message -> EventBus.getDefault().postSticky(new PlayerErrorEvent(message))); - } - - private void clearMediaPlayerListeners() { - mediaPlayer.setOnCompletionListener(() -> { }); - mediaPlayer.setOnSeekCompleteListener(() -> { }); - mediaPlayer.setOnBufferingUpdateListener(percent -> { }); - mediaPlayer.setOnErrorListener(x -> { }); - } - - private void genericSeekCompleteListener() { - Log.d(TAG, "genericSeekCompleteListener"); - if (seekLatch != null) { - seekLatch.countDown(); - } - if (playerStatus == PlayerStatus.PLAYING) { - callback.onPlaybackStart(media, getPosition()); - } - if (playerStatus == PlayerStatus.SEEKING) { - setPlayerStatus(statusBeforeSeeking, media, getPosition()); - } - } - - @Override - public boolean isCasting() { - return false; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java deleted file mode 100644 index 4cd4931e1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ /dev/null @@ -1,2001 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.app.UiModeManager; -import android.bluetooth.BluetoothA2dp; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Looper; -import android.os.Vibrator; -import android.service.quicksettings.TileService; -import android.support.v4.media.MediaBrowserCompat; -import android.support.v4.media.MediaDescriptionCompat; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; -import android.view.KeyEvent; -import android.view.SurfaceHolder; -import android.view.ViewConfiguration; -import android.webkit.URLUtil; -import android.widget.Toast; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.ContextCompat; -import androidx.media.MediaBrowserServiceCompat; - -import de.danoeh.antennapod.event.PlayerStatusEvent; -import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; -import de.danoeh.antennapod.ui.notifications.NotificationUtils; -import de.danoeh.antennapod.ui.widget.WidgetUpdater; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences; -import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.core.service.QuickSettingsTileService; -import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager.SleepTimer; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.core.util.FeedUtil; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.core.util.playback.PlayableUtils; -import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.event.PlayerErrorEvent; -import de.danoeh.antennapod.event.playback.BufferUpdateEvent; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; -import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; -import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent; -import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent; -import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; -import de.danoeh.antennapod.model.feed.Chapter; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.playback.base.PlayerStatus; -import de.danoeh.antennapod.playback.cast.CastPsmp; -import de.danoeh.antennapod.playback.cast.CastStateListener; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; -import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; -import io.reactivex.Completable; -import io.reactivex.Observable; -import io.reactivex.Single; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; - -/** - * Controls the MediaPlayer that plays a FeedMedia-file - */ -public class PlaybackService extends MediaBrowserServiceCompat { - /** - * Logging tag - */ - private static final String TAG = "PlaybackService"; - - public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged"; - private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; - private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; - - /** - * Custom actions used by Android Wear, Android Auto, and Android (API 33+ only) - */ - private static final String CUSTOM_ACTION_SKIP_TO_NEXT = "action.de.danoeh.antennapod.core.service.skipToNext"; - private static final String CUSTOM_ACTION_FAST_FORWARD = "action.de.danoeh.antennapod.core.service.fastForward"; - private static final String CUSTOM_ACTION_REWIND = "action.de.danoeh.antennapod.core.service.rewind"; - private static final String CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED = - "action.de.danoeh.antennapod.core.service.changePlaybackSpeed"; - private static final String CUSTOM_ACTION_TOGGLE_SLEEP_TIMER = - "action.de.danoeh.antennapod.core.service.toggleSleepTimer"; - public static final String CUSTOM_ACTION_NEXT_CHAPTER = "action.de.danoeh.antennapod.core.service.next_chapter"; - - /** - * Set a max number of episodes to load for Android Auto, otherwise there could be performance issues - */ - public static final int MAX_ANDROID_AUTO_EPISODES_PER_FEED = 100; - - /** - * Is true if service is running. - */ - public static boolean isRunning = false; - /** - * Is true if the service was running, but paused due to headphone disconnect - */ - private static boolean transientPause = false; - /** - * Is true if a Cast Device is connected to the service. - */ - private static volatile boolean isCasting = false; - - private PlaybackServiceMediaPlayer mediaPlayer; - private PlaybackServiceTaskManager taskManager; - private PlaybackServiceStateManager stateManager; - private Disposable positionEventTimer; - private PlaybackServiceNotificationBuilder notificationBuilder; - private CastStateListener castStateListener; - - private String autoSkippedFeedMediaId = null; - private int clickCount = 0; - private final Handler clickHandler = new Handler(Looper.getMainLooper()); - - /** - * Used for Lollipop notifications, Android Wear, and Android Auto. - */ - private MediaSessionCompat mediaSession; - - private static volatile MediaType currentMediaType = MediaType.UNKNOWN; - - private final IBinder mBinder = new LocalBinder(); - - public class LocalBinder extends Binder { - public PlaybackService getService() { - return PlaybackService.this; - } - } - - @Override - public boolean onUnbind(Intent intent) { - Log.d(TAG, "Received onUnbind event"); - return super.onUnbind(intent); - } - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. If the playbackservice is not - * running, the type of the last played media will be looked up. - */ - public static Intent getPlayerActivityIntent(Context context) { - boolean showVideoPlayer; - - if (isRunning) { - showVideoPlayer = currentMediaType == MediaType.VIDEO && !isCasting; - } else { - showVideoPlayer = PlaybackPreferences.getCurrentEpisodeIsVideo(); - } - - if (showVideoPlayer) { - return new VideoPlayerActivityStarter(context).getIntent(); - } else { - return new MainActivityStarter(context).withOpenPlayer().getIntent(); - } - } - - /** - * Same as {@link #getPlayerActivityIntent(Context)}, but here the type of activity - * depends on the FeedMedia that is provided as an argument. - */ - public static Intent getPlayerActivityIntent(Context context, Playable media) { - if (media.getMediaType() == MediaType.VIDEO && !isCasting) { - return new VideoPlayerActivityStarter(context).getIntent(); - } else { - return new MainActivityStarter(context).withOpenPlayer().getIntent(); - } - } - - @Override - public void onCreate() { - super.onCreate(); - Log.d(TAG, "Service created."); - isRunning = true; - - stateManager = new PlaybackServiceStateManager(this); - notificationBuilder = new PlaybackServiceNotificationBuilder(this); - - registerReceiver(autoStateUpdated, new IntentFilter("com.google.android.gms.car.media.STATUS")); - registerReceiver(headsetDisconnected, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); - registerReceiver(shutdownReceiver, new IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - registerReceiver(bluetoothStateUpdated, new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)); - registerReceiver(audioBecomingNoisy, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - EventBus.getDefault().register(this); - taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); - - recreateMediaSessionIfNeeded(); - castStateListener = new CastStateListener(this) { - @Override - public void onSessionStartedOrEnded() { - recreateMediaPlayer(); - } - }; - EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED)); - } - - void recreateMediaSessionIfNeeded() { - if (mediaSession != null) { - // Media session was not destroyed, so we can re-use it. - if (!mediaSession.isActive()) { - mediaSession.setActive(true); - } - return; - } - ComponentName eventReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class); - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(eventReceiver); - PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, - PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0)); - - mediaSession = new MediaSessionCompat(getApplicationContext(), TAG, eventReceiver, buttonReceiverIntent); - setSessionToken(mediaSession.getSessionToken()); - - try { - mediaSession.setCallback(sessionCallback); - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); - } catch (NullPointerException npe) { - // on some devices (Huawei) setting active can cause a NullPointerException - // even with correct use of the api. - // See http://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat - // and https://plus.google.com/+IanLake/posts/YgdTkKFxz7d - Log.e(TAG, "NullPointerException while setting up MediaSession"); - npe.printStackTrace(); - } - - recreateMediaPlayer(); - mediaSession.setActive(true); - } - - void recreateMediaPlayer() { - Playable media = null; - boolean wasPlaying = false; - if (mediaPlayer != null) { - media = mediaPlayer.getPlayable(); - wasPlaying = mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING; - mediaPlayer.pause(true, false); - mediaPlayer.shutdown(); - } - mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback); - if (mediaPlayer == null) { - mediaPlayer = new LocalPSMP(this, mediaPlayerCallback); // Cast not supported or not connected - } - if (media != null) { - mediaPlayer.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true); - } - isCasting = mediaPlayer.isCasting(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.d(TAG, "Service is about to be destroyed"); - - if (notificationBuilder.getPlayerStatus() == PlayerStatus.PLAYING) { - notificationBuilder.setPlayerStatus(PlayerStatus.STOPPED); - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED) { - notificationManager.notify(R.id.notification_playing, notificationBuilder.build()); - } - } - stateManager.stopForeground(!UserPreferences.isPersistNotify()); - isRunning = false; - currentMediaType = MediaType.UNKNOWN; - castStateListener.destroy(); - - cancelPositionObserver(); - if (mediaSession != null) { - mediaSession.release(); - mediaSession = null; - } - unregisterReceiver(autoStateUpdated); - unregisterReceiver(headsetDisconnected); - unregisterReceiver(shutdownReceiver); - unregisterReceiver(bluetoothStateUpdated); - unregisterReceiver(audioBecomingNoisy); - mediaPlayer.shutdown(); - taskManager.shutdown(); - EventBus.getDefault().unregister(this); - } - - @Override - public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, Bundle rootHints) { - Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName + - "; clientUid=" + clientUid + " ; rootHints=" + rootHints); - if (rootHints != null && rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) { - Bundle extras = new Bundle(); - extras.putBoolean(BrowserRoot.EXTRA_RECENT, true); - Log.d(TAG, "OnGetRoot: Returning BrowserRoot " + R.string.current_playing_episode); - return new BrowserRoot(getResources().getString(R.string.current_playing_episode), extras); - } - - // Name visible in Android Auto - return new BrowserRoot(getResources().getString(R.string.app_name), null); - } - - private void loadQueueForMediaSession() { - Single.<List<MediaSessionCompat.QueueItem>>create(emitter -> { - List<MediaSessionCompat.QueueItem> queueItems = new ArrayList<>(); - for (FeedItem feedItem : DBReader.getQueue()) { - if (feedItem.getMedia() != null) { - MediaDescriptionCompat mediaDescription = feedItem.getMedia().getMediaItem().getDescription(); - queueItems.add(new MediaSessionCompat.QueueItem(mediaDescription, feedItem.getId())); - } - } - emitter.onSuccess(queueItems); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace); - } - - private MediaBrowserCompat.MediaItem createBrowsableMediaItem( - @StringRes int title, @DrawableRes int icon, int numEpisodes) { - Uri uri = new Uri.Builder() - .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - .authority(getResources().getResourcePackageName(icon)) - .appendPath(getResources().getResourceTypeName(icon)) - .appendPath(getResources().getResourceEntryName(icon)) - .build(); - - MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() - .setIconUri(uri) - .setMediaId(getResources().getString(title)) - .setTitle(getResources().getString(title)) - .setSubtitle(getResources().getQuantityString(R.plurals.num_episodes, numEpisodes, numEpisodes)) - .build(); - return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); - } - - private MediaBrowserCompat.MediaItem createBrowsableMediaItemForFeed(Feed feed) { - MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder() - .setMediaId("FeedId:" + feed.getId()) - .setTitle(feed.getTitle()) - .setDescription(feed.getDescription()) - .setSubtitle(feed.getCustomTitle()); - if (feed.getImageUrl() != null) { - builder.setIconUri(Uri.parse(feed.getImageUrl())); - } - if (feed.getLink() != null) { - builder.setMediaUri(Uri.parse(feed.getLink())); - } - MediaDescriptionCompat description = builder.build(); - return new MediaBrowserCompat.MediaItem(description, - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); - } - - @Override - public void onLoadChildren(@NonNull String parentId, - @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) { - Log.d(TAG, "OnLoadChildren: parentMediaId=" + parentId); - result.detach(); - - Completable.create(emitter -> { - result.sendResult(loadChildrenSynchronous(parentId)); - emitter.onComplete(); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - () -> { - }, e -> { - e.printStackTrace(); - result.sendResult(null); - }); - } - - private List<MediaBrowserCompat.MediaItem> loadChildrenSynchronous(@NonNull String parentId) { - List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>(); - if (parentId.equals(getResources().getString(R.string.app_name))) { - long currentlyPlaying = PlaybackPreferences.getCurrentPlayerStatus(); - if (currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PLAYING - || currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PAUSED) { - mediaItems.add(createBrowsableMediaItem(R.string.current_playing_episode, R.drawable.ic_play_48dp, 1)); - } - mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_play_black, - DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.QUEUED)))); - mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black, - DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED)))); - mediaItems.add(createBrowsableMediaItem(R.string.episodes_label, R.drawable.ic_feed_black, - DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.UNPLAYED)))); - List<Feed> feeds = DBReader.getFeedList(); - for (Feed feed : feeds) { - mediaItems.add(createBrowsableMediaItemForFeed(feed)); - } - return mediaItems; - } - - List<FeedItem> feedItems; - if (parentId.equals(getResources().getString(R.string.queue_label))) { - feedItems = DBReader.getQueue(); - } else if (parentId.equals(getResources().getString(R.string.downloads_label))) { - feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, - new FeedItemFilter(FeedItemFilter.DOWNLOADED), UserPreferences.getDownloadsSortedOrder()); - } else if (parentId.equals(getResources().getString(R.string.episodes_label))) { - feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, - new FeedItemFilter(FeedItemFilter.UNPLAYED), UserPreferences.getAllEpisodesSortOrder()); - } else if (parentId.startsWith("FeedId:")) { - long feedId = Long.parseLong(parentId.split(":")[1]); - Feed feed = DBReader.getFeed(feedId); - feedItems = DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(), feed.getSortOrder()); - } else if (parentId.equals(getString(R.string.current_playing_episode))) { - FeedMedia playable = DBReader.getFeedMedia(PlaybackPreferences.getCurrentlyPlayingFeedMediaId()); - if (playable != null) { - feedItems = Collections.singletonList(playable.getItem()); - } else { - return null; - } - } else { - Log.e(TAG, "Parent ID not found: " + parentId); - return null; - } - int count = 0; - for (FeedItem feedItem : feedItems) { - if (feedItem.getMedia() != null && feedItem.getMedia().getMediaItem() != null) { - mediaItems.add(feedItem.getMedia().getMediaItem()); - if (++count >= MAX_ANDROID_AUTO_EPISODES_PER_FEED) { - break; - } - } - } - return mediaItems; - } - - @Override - public IBinder onBind(Intent intent) { - Log.d(TAG, "Received onBind event"); - if (intent.getAction() != null && TextUtils.equals(intent.getAction(), MediaBrowserServiceCompat.SERVICE_INTERFACE)) { - return super.onBind(intent); - } else { - return mBinder; - } - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - Log.d(TAG, "OnStartCommand called"); - - stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()); - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - notificationManager.cancel(R.id.notification_streaming_confirmation); - - final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); - final String customAction = intent.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION); - final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false); - Playable playable = intent.getParcelableExtra(PlaybackServiceInterface.EXTRA_PLAYABLE); - if (keycode == -1 && playable == null && customAction == null) { - Log.e(TAG, "PlaybackService was started with no arguments"); - stateManager.stopService(); - return Service.START_NOT_STICKY; - } - - if ((flags & Service.START_FLAG_REDELIVERY) != 0) { - Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); - stateManager.stopForeground(true); - } else { - if (keycode != -1) { - boolean notificationButton; - if (hardwareButton) { - Log.d(TAG, "Received hardware button event"); - notificationButton = false; - } else { - Log.d(TAG, "Received media button event"); - notificationButton = true; - } - boolean handled = handleKeycode(keycode, notificationButton); - if (!handled && !stateManager.hasReceivedValidStartCommand()) { - stateManager.stopService(); - return Service.START_NOT_STICKY; - } - } else if (playable != null) { - stateManager.validStartCommandWasReceived(); - boolean allowStreamThisTime = intent.getBooleanExtra( - PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false); - boolean allowStreamAlways = intent.getBooleanExtra( - PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false); - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0); - if (allowStreamAlways) { - UserPreferences.setAllowMobileStreaming(true); - } - Observable.fromCallable( - () -> { - if (playable instanceof FeedMedia) { - return DBReader.getFeedMedia(((FeedMedia) playable).getId()); - } else { - return playable; - } - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - loadedPlayable -> startPlaying(loadedPlayable, allowStreamThisTime), - error -> { - Log.d(TAG, "Playable was not found. Stopping service."); - error.printStackTrace(); - stateManager.stopService(); - }); - return Service.START_NOT_STICKY; - } else { - mediaSession.getController().getTransportControls().sendCustomAction(customAction, null); - } - } - - return Service.START_NOT_STICKY; - } - - private void skipIntro(Playable playable) { - if (! (playable instanceof FeedMedia)) { - return; - } - - FeedMedia feedMedia = (FeedMedia) playable; - FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences(); - int skipIntro = preferences.getFeedSkipIntro(); - - Context context = getApplicationContext(); - if (skipIntro > 0 && playable.getPosition() < skipIntro * 1000) { - int duration = getDuration(); - if (skipIntro * 1000 < duration || duration <= 0) { - Log.d(TAG, "skipIntro " + playable.getEpisodeTitle()); - mediaPlayer.seekTo(skipIntro * 1000); - String skipIntroMesg = context.getString(R.string.pref_feed_skip_intro_toast, - skipIntro); - Toast toast = Toast.makeText(context, skipIntroMesg, - Toast.LENGTH_LONG); - toast.show(); - } - } - } - - @SuppressLint("LaunchActivityFromNotification") - private void displayStreamingNotAllowedNotification(Intent originalIntent) { - if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { - EventBus.getDefault().post(new MessageEvent( - getString(R.string.confirm_mobile_streaming_notification_message))); - return; - } - - Intent intentAllowThisTime = new Intent(originalIntent); - intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME); - intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true); - PendingIntent pendingIntentAllowThisTime; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - pendingIntentAllowThisTime = PendingIntent.getForegroundService(this, - R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } else { - pendingIntentAllowThisTime = PendingIntent.getService(this, - R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - Intent intentAlwaysAllow = new Intent(intentAllowThisTime); - intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS); - intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true); - PendingIntent pendingIntentAlwaysAllow; - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - pendingIntentAlwaysAllow = PendingIntent.getForegroundService(this, - R.id.pending_intent_allow_stream_always, intentAlwaysAllow, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } else { - pendingIntentAlwaysAllow = PendingIntent.getService(this, - R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - NotificationCompat.Builder builder = new NotificationCompat.Builder(this, - NotificationUtils.CHANNEL_ID_USER_ACTION) - .setSmallIcon(R.drawable.ic_notification_stream) - .setContentTitle(getString(R.string.confirm_mobile_streaming_notification_title)) - .setContentText(getString(R.string.confirm_mobile_streaming_notification_message)) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(getString(R.string.confirm_mobile_streaming_notification_message))) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setContentIntent(pendingIntentAllowThisTime) - .addAction(R.drawable.ic_notification_stream, - getString(R.string.confirm_mobile_streaming_button_once), - pendingIntentAllowThisTime) - .addAction(R.drawable.ic_notification_stream, - getString(R.string.confirm_mobile_streaming_button_always), - pendingIntentAlwaysAllow) - .setAutoCancel(true); - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED) { - notificationManager.notify(R.id.notification_streaming_confirmation, builder.build()); - } - } - - /** - * Handles media button events - * return: keycode was handled - */ - private boolean handleKeycode(int keycode, boolean notificationButton) { - Log.d(TAG, "Handling keycode: " + keycode); - final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - final PlayerStatus status = info.playerStatus; - switch (keycode) { - case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(!UserPreferences.isPersistNotify(), false); - } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (status == PlayerStatus.PREPARING) { - mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); - } else if (status == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } else if (mediaPlayer.getPlayable() == null) { - startPlayingFromPreferences(); - } else { - return false; - } - taskManager.restartSleepTimer(); - return true; - case KeyEvent.KEYCODE_MEDIA_PLAY: - if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (status == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } else if (mediaPlayer.getPlayable() == null) { - startPlayingFromPreferences(); - } else { - return false; - } - taskManager.restartSleepTimer(); - return true; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(!UserPreferences.isPersistNotify(), false); - return true; - } - return false; - case KeyEvent.KEYCODE_MEDIA_NEXT: - if (!notificationButton) { - // Handle remapped button as notification button which is not remapped again. - return handleKeycode(UserPreferences.getHardwareForwardButton(), true); - } else if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) { - mediaPlayer.skip(); - return true; - } - return false; - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) { - mediaPlayer.seekDelta(UserPreferences.getFastForwardSecs() * 1000); - return true; - } - return false; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - if (!notificationButton) { - // Handle remapped button as notification button which is not remapped again. - return handleKeycode(UserPreferences.getHardwarePreviousButton(), true); - } else if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) { - mediaPlayer.seekTo(0); - return true; - } - return false; - case KeyEvent.KEYCODE_MEDIA_REWIND: - if (getStatus() == PlayerStatus.PLAYING || getStatus() == PlayerStatus.PAUSED) { - mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); - return true; - } - return false; - case KeyEvent.KEYCODE_MEDIA_STOP: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, true); - } - - stateManager.stopForeground(true); // gets rid of persistent notification - return true; - default: - Log.d(TAG, "Unhandled key code: " + keycode); - if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something - String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); - Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); - } - } - return false; - } - - private void startPlayingFromPreferences() { - Observable.fromCallable(() -> DBReader.getFeedMedia(PlaybackPreferences.getCurrentlyPlayingFeedMediaId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - playable -> startPlaying(playable, false), - error -> { - Log.d(TAG, "Playable was not loaded from preferences. Stopping service."); - error.printStackTrace(); - stateManager.stopService(); - }); - } - - private void startPlaying(Playable playable, boolean allowStreamThisTime) { - boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl()); - boolean stream = !playable.localFileAvailable() || localFeed; - if (stream && !localFeed && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime) { - displayStreamingNotAllowedNotification( - new PlaybackServiceStarter(this, playable) - .getIntent()); - PlaybackPreferences.writeNoMediaPlaying(); - stateManager.stopService(); - return; - } - - if (!playable.getIdentifier().equals(PlaybackPreferences.getCurrentlyPlayingFeedMediaId())) { - PlaybackPreferences.clearCurrentlyPlayingTemporaryPlaybackSettings(); - } - - mediaPlayer.playMediaObject(playable, stream, true, true); - stateManager.validStartCommandWasReceived(); - stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()); - recreateMediaSessionIfNeeded(); - updateNotificationAndMediaSession(playable); - addPlayableToQueue(playable); - } - - /** - * Called by a mediaplayer Activity as soon as it has prepared its - * mediaplayer. - */ - public void setVideoSurface(SurfaceHolder sh) { - Log.d(TAG, "Setting display"); - mediaPlayer.setVideoSurface(sh); - } - - public void notifyVideoSurfaceAbandoned() { - mediaPlayer.pause(true, false); - mediaPlayer.resetVideoSurface(); - updateNotificationAndMediaSession(getPlayable()); - stateManager.stopForeground(!UserPreferences.isPersistNotify()); - } - - private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { - @Override - public void positionSaverTick() { - saveCurrentPosition(true, null, Playable.INVALID_TIME); - } - - @Override - public WidgetUpdater.WidgetState requestWidgetState() { - return new WidgetUpdater.WidgetState(getPlayable(), getStatus(), - getCurrentPosition(), getDuration(), getCurrentPlaybackSpeed()); - } - - @Override - public void onChapterLoaded(Playable media) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0); - updateMediaSession(mediaPlayer.getPlayerStatus()); - } - }; - - private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { - @Override - public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { - if (mediaPlayer != null) { - currentMediaType = mediaPlayer.getCurrentMediaType(); - } else { - currentMediaType = MediaType.UNKNOWN; - } - - updateMediaSession(newInfo.playerStatus); - switch (newInfo.playerStatus) { - case INITIALIZED: - if (mediaPlayer.getPSMPInfo().playable != null) { - PlaybackPreferences.writeMediaPlaying(mediaPlayer.getPSMPInfo().playable); - } - updateNotificationAndMediaSession(newInfo.playable); - break; - case PREPARED: - if (mediaPlayer.getPSMPInfo().playable != null) { - PlaybackPreferences.writeMediaPlaying(mediaPlayer.getPSMPInfo().playable); - } - taskManager.startChapterLoader(newInfo.playable); - break; - case PAUSED: - updateNotificationAndMediaSession(newInfo.playable); - if (!isCasting) { - stateManager.stopForeground(!UserPreferences.isPersistNotify()); - } - cancelPositionObserver(); - break; - case STOPPED: - //writePlaybackPreferencesNoMediaPlaying(); - //stopService(); - break; - case PLAYING: - saveCurrentPosition(true, null, Playable.INVALID_TIME); - recreateMediaSessionIfNeeded(); - updateNotificationAndMediaSession(newInfo.playable); - setupPositionObserver(); - stateManager.validStartCommandWasReceived(); - stateManager.startForeground(R.id.notification_playing, notificationBuilder.build()); - // set sleep timer if auto-enabled - boolean autoEnableByTime = true; - int fromSetting = SleepTimerPreferences.autoEnableFrom(); - int toSetting = SleepTimerPreferences.autoEnableTo(); - if (fromSetting != toSetting) { - Calendar now = new GregorianCalendar(); - now.setTimeInMillis(System.currentTimeMillis()); - int currentHour = now.get(Calendar.HOUR_OF_DAY); - autoEnableByTime = SleepTimerPreferences.isInTimeRange(fromSetting, toSetting, currentHour); - } - - if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING - && SleepTimerPreferences.autoEnable() && autoEnableByTime && !sleepTimerActive()) { - setSleepTimer(SleepTimerPreferences.timerMillis()); - EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label), - (ctx) -> disableSleepTimer(), getString(R.string.undo))); - } - loadQueueForMediaSession(); - break; - case ERROR: - PlaybackPreferences.writeNoMediaPlaying(); - stateManager.stopService(); - break; - default: - break; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - TileService.requestListeningState(getApplicationContext(), - new ComponentName(getApplicationContext(), QuickSettingsTileService.class)); - } - - IntentUtils.sendLocalBroadcast(getApplicationContext(), ACTION_PLAYER_STATUS_CHANGED); - bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); - bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); - taskManager.requestWidgetUpdate(); - EventBus.getDefault().post(new PlayerStatusEvent()); - } - - @Override - public void shouldStop() { - stateManager.stopForeground(!UserPreferences.isPersistNotify()); - } - - @Override - public void onMediaChanged(boolean reloadUI) { - Log.d(TAG, "reloadUI callback reached"); - if (reloadUI) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0); - } - updateNotificationAndMediaSession(getPlayable()); - } - - @Override - public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, - boolean playingNext) { - PlaybackService.this.onPostPlayback(media, ended, skipped, playingNext); - } - - @Override - public void onPlaybackStart(@NonNull Playable playable, int position) { - taskManager.startWidgetUpdater(); - if (position != Playable.INVALID_TIME) { - playable.setPosition(position); - } else { - skipIntro(playable); - } - playable.onPlaybackStart(); - taskManager.startPositionSaver(); - } - - @Override - public void onPlaybackPause(Playable playable, int position) { - taskManager.cancelPositionSaver(); - cancelPositionObserver(); - saveCurrentPosition(position == Playable.INVALID_TIME || playable == null, playable, position); - taskManager.cancelWidgetUpdater(); - if (playable != null) { - if (playable instanceof FeedMedia) { - SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(getApplicationContext(), - (FeedMedia) playable, false); - } - playable.onPlaybackPause(getApplicationContext()); - } - } - - @Override - public Playable getNextInQueue(Playable currentMedia) { - return PlaybackService.this.getNextInQueue(currentMedia); - } - - @Nullable - @Override - public Playable findMedia(@NonNull String url) { - FeedItem item = DBReader.getFeedItemByGuidOrEpisodeUrl(null, url); - return item != null ? item.getMedia() : null; - } - - @Override - public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { - PlaybackService.this.onPlaybackEnded(mediaType, stopPlaying); - } - - @Override - public void ensureMediaInfoLoaded(@NonNull Playable media) { - if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) { - ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId())); - } - } - }; - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void playerError(PlayerErrorEvent event) { - if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, false); - } - stateManager.stopService(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void bufferUpdate(BufferUpdateEvent event) { - if (event.hasEnded()) { - Playable playable = getPlayable(); - if (getPlayable() instanceof FeedMedia - && playable.getDuration() <= 0 && mediaPlayer.getDuration() > 0) { - // Playable is being streamed and does not have a duration specified in the feed - playable.setDuration(mediaPlayer.getDuration()); - DBWriter.setFeedMedia((FeedMedia) playable); - updateNotificationAndMediaSession(playable); - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void sleepTimerUpdate(SleepTimerUpdatedEvent event) { - if (event.isOver()) { - updateMediaSession(mediaPlayer.getPlayerStatus()); - mediaPlayer.pause(true, true); - mediaPlayer.setVolume(1.0f, 1.0f); - int newPosition = mediaPlayer.getPosition() - (int) SleepTimer.NOTIFICATION_THRESHOLD / 2; - newPosition = Math.max(newPosition, 0); - seekTo(newPosition); - } else if (event.getTimeLeft() < SleepTimer.NOTIFICATION_THRESHOLD) { - final float[] multiplicators = {0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f}; - float multiplicator = multiplicators[Math.max(0, (int) event.getTimeLeft() / 1000)]; - Log.d(TAG, "onSleepTimerAlmostExpired: " + multiplicator); - mediaPlayer.setVolume(multiplicator, multiplicator); - } else if (event.isCancelled()) { - updateMediaSession(mediaPlayer.getPlayerStatus()); - mediaPlayer.setVolume(1.0f, 1.0f); - } else if (event.wasJustEnabled()) { - updateMediaSession(mediaPlayer.getPlayerStatus()); - } - } - - private Playable getNextInQueue(final Playable currentMedia) { - if (!(currentMedia instanceof FeedMedia)) { - Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding"); - PlaybackPreferences.writeNoMediaPlaying(); - return null; - } - Log.d(TAG, "getNextInQueue()"); - FeedMedia media = (FeedMedia) currentMedia; - if (media.getItem() == null) { - media.setItem(DBReader.getFeedItem(media.getItemId())); - } - FeedItem item = media.getItem(); - if (item == null) { - Log.w(TAG, "getNextInQueue() with FeedMedia object whose FeedItem is null"); - PlaybackPreferences.writeNoMediaPlaying(); - return null; - } - FeedItem nextItem; - nextItem = DBReader.getNextInQueue(item); - - if (nextItem == null || nextItem.getMedia() == null) { - PlaybackPreferences.writeNoMediaPlaying(); - return null; - } - - if (!UserPreferences.isFollowQueue()) { - Log.d(TAG, "getNextInQueue(), but follow queue is not enabled."); - PlaybackPreferences.writeMediaPlaying(nextItem.getMedia()); - updateNotificationAndMediaSession(nextItem.getMedia()); - return null; - } - - if (!nextItem.getMedia().localFileAvailable() && !NetworkUtils.isStreamingAllowed() - && UserPreferences.isFollowQueue() && !nextItem.getFeed().isLocalFeed()) { - displayStreamingNotAllowedNotification( - new PlaybackServiceStarter(this, nextItem.getMedia()) - .getIntent()); - PlaybackPreferences.writeNoMediaPlaying(); - stateManager.stopService(); - return null; - } - return nextItem.getMedia(); - } - - /** - * Set of instructions to be performed when playback ends. - */ - private void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { - Log.d(TAG, "Playback ended"); - PlaybackPreferences.clearCurrentlyPlayingTemporaryPlaybackSettings(); - if (stopPlaying) { - taskManager.cancelPositionSaver(); - cancelPositionObserver(); - if (!isCasting) { - stateManager.stopForeground(true); - stateManager.stopService(); - } - } - if (mediaType == null) { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_PLAYBACK_END, 0); - } else { - sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, - isCasting ? PlaybackServiceInterface.EXTRA_CODE_CAST : - (mediaType == MediaType.VIDEO) ? PlaybackServiceInterface.EXTRA_CODE_VIDEO : - PlaybackServiceInterface.EXTRA_CODE_AUDIO); - } - } - - /** - * This method processes the media object after its playback ended, either because it completed - * or because a different media object was selected for playback. - * <p> - * Even though these tasks aren't supposed to be resource intensive, a good practice is to - * usually call this method on a background thread. - * - * @param playable the media object that was playing. It is assumed that its position - * property was updated before this method was called. - * @param ended if true, it signals that {@param playable} was played until its end. - * In such case, the position property of the media becomes irrelevant for - * most of the tasks (although it's still a good practice to keep it - * accurate). - * @param skipped if the user pressed a skip >| button. - * @param playingNext if true, it means another media object is being loaded in place of this - * one. - * Instances when we'd set it to false would be when we're not following the - * queue or when the queue has ended. - */ - private void onPostPlayback(final Playable playable, boolean ended, boolean skipped, - boolean playingNext) { - if (playable == null) { - Log.e(TAG, "Cannot do post-playback processing: media was null"); - return; - } - Log.d(TAG, "onPostPlayback(): media=" + playable.getEpisodeTitle()); - - if (!(playable instanceof FeedMedia)) { - Log.d(TAG, "Not doing post-playback processing: media not of type FeedMedia"); - if (ended) { - playable.onPlaybackCompleted(getApplicationContext()); - } else { - playable.onPlaybackPause(getApplicationContext()); - } - return; - } - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); - boolean almostEnded = media.getDuration() > 0 - && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000; - if (!ended && almostEnded) { - Log.d(TAG, "smart mark as played"); - } - - boolean autoSkipped = false; - if (autoSkippedFeedMediaId != null && autoSkippedFeedMediaId.equals(item.getIdentifyingValue())) { - autoSkippedFeedMediaId = null; - autoSkipped = true; - } - - if (ended || almostEnded) { - SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( - getApplicationContext(), media, true); - media.onPlaybackCompleted(getApplicationContext()); - } else { - SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( - getApplicationContext(), media, false); - media.onPlaybackPause(getApplicationContext()); - } - - if (item != null) { - if (ended || almostEnded - || autoSkipped - || (skipped && !UserPreferences.shouldSkipKeepEpisode())) { - // only mark the item as played if we're not keeping it anyways - DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended || (skipped && almostEnded)); - // don't know if it actually matters to not autodownload when smart mark as played is triggered - DBWriter.removeQueueItem(PlaybackService.this, ended, item); - // Delete episode if enabled - FeedPreferences.AutoDeleteAction action = - item.getFeed().getPreferences().getCurrentAutoDelete(); - boolean shouldAutoDelete = action == FeedPreferences.AutoDeleteAction.ALWAYS - || (action == FeedPreferences.AutoDeleteAction.GLOBAL - && FeedUtil.shouldAutoDeleteItemsOnThatFeed(item.getFeed())); - if (shouldAutoDelete && (!item.isTagged(FeedItem.TAG_FAVORITE) - || !UserPreferences.shouldFavoriteKeepEpisode())) { - DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media); - Log.d(TAG, "Episode Deleted"); - } - notifyChildrenChanged(getString(R.string.queue_label)); - } - } - - if (ended || skipped || playingNext) { - DBWriter.addItemToPlaybackHistory(media); - } - } - - public void setSleepTimer(long waitingTime) { - Log.d(TAG, "Setting sleep timer to " + waitingTime + " milliseconds"); - taskManager.setSleepTimer(waitingTime); - } - - public void disableSleepTimer() { - taskManager.disableSleepTimer(); - } - - private void sendNotificationBroadcast(int type, int code) { - Intent intent = new Intent(PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION); - intent.putExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_TYPE, type); - intent.putExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_CODE, code); - intent.setPackage(getPackageName()); - sendBroadcast(intent); - } - - private void skipEndingIfNecessary() { - Playable playable = mediaPlayer.getPlayable(); - if (! (playable instanceof FeedMedia)) { - return; - } - - int duration = getDuration(); - int remainingTime = duration - getCurrentPosition(); - - FeedMedia feedMedia = (FeedMedia) playable; - FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences(); - int skipEnd = preferences.getFeedSkipEnding(); - if (skipEnd > 0 - && skipEnd * 1000 < getDuration() - && (remainingTime - (skipEnd * 1000) > 0) - && ((remainingTime - skipEnd * 1000) < (getCurrentPlaybackSpeed() * 1000))) { - Log.d(TAG, "skipEndingIfNecessary: Skipping the remaining " + remainingTime + " " + skipEnd * 1000 + " speed " + getCurrentPlaybackSpeed()); - Context context = getApplicationContext(); - String skipMesg = context.getString(R.string.pref_feed_skip_ending_toast, skipEnd); - Toast toast = Toast.makeText(context, skipMesg, Toast.LENGTH_LONG); - toast.show(); - - this.autoSkippedFeedMediaId = feedMedia.getItem().getIdentifyingValue(); - mediaPlayer.skip(); - } - } - - /** - * Updates the Media Session for the corresponding status. - * - * @param playerStatus the current {@link PlayerStatus} - */ - private void updateMediaSession(final PlayerStatus playerStatus) { - PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder(); - - int state; - if (playerStatus != null) { - switch (playerStatus) { - case PLAYING: - state = PlaybackStateCompat.STATE_PLAYING; - break; - case PREPARED: - case PAUSED: - state = PlaybackStateCompat.STATE_PAUSED; - break; - case STOPPED: - state = PlaybackStateCompat.STATE_STOPPED; - break; - case SEEKING: - state = PlaybackStateCompat.STATE_FAST_FORWARDING; - break; - case PREPARING: - case INITIALIZING: - state = PlaybackStateCompat.STATE_CONNECTING; - break; - case ERROR: - state = PlaybackStateCompat.STATE_ERROR; - break; - case INITIALIZED: // Deliberate fall-through - case INDETERMINATE: - default: - state = PlaybackStateCompat.STATE_NONE; - break; - } - } else { - state = PlaybackStateCompat.STATE_NONE; - } - - sessionState.setState(state, getCurrentPosition(), getCurrentPlaybackSpeed()); - long capabilities = PlaybackStateCompat.ACTION_PLAY - | PlaybackStateCompat.ACTION_PLAY_PAUSE - | PlaybackStateCompat.ACTION_REWIND - | PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_FAST_FORWARD - | PlaybackStateCompat.ACTION_SEEK_TO - | PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED; - - sessionState.setActions(capabilities); - - // On Android Auto, custom actions are added in the following order around the play button, if no default - // actions are present: Near left, near right, far left, far right, additional actions panel - PlaybackStateCompat.CustomAction.Builder rewindBuilder = new PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_REWIND, - getString(R.string.rewind_label), - R.drawable.ic_notification_fast_rewind - ); - WearMediaSession.addWearExtrasToAction(rewindBuilder); - sessionState.addCustomAction(rewindBuilder.build()); - - PlaybackStateCompat.CustomAction.Builder fastForwardBuilder = new PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_FAST_FORWARD, - getString(R.string.fast_forward_label), - R.drawable.ic_notification_fast_forward - ); - WearMediaSession.addWearExtrasToAction(fastForwardBuilder); - sessionState.addCustomAction(fastForwardBuilder.build()); - - if (UserPreferences.showPlaybackSpeedOnFullNotification()) { - sessionState.addCustomAction( - new PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED, - getString(R.string.playback_speed), - R.drawable.ic_notification_playback_speed - ).build() - ); - } - - if (UserPreferences.showSleepTimerOnFullNotification()) { - @DrawableRes int icon = R.drawable.ic_notification_sleep; - if (sleepTimerActive()) { - icon = R.drawable.ic_notification_sleep_off; - } - sessionState.addCustomAction( - new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_TOGGLE_SLEEP_TIMER, - getString(R.string.sleep_timer_label), icon).build()); - } - - if (UserPreferences.showNextChapterOnFullNotification()) { - if (getPlayable() != null && getPlayable().getChapters() != null) { - sessionState.addCustomAction( - new PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_NEXT_CHAPTER, - getString(R.string.next_chapter), R.drawable.ic_notification_next_chapter) - .build()); - } - } - - if (UserPreferences.showSkipOnFullNotification()) { - sessionState.addCustomAction( - new PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_SKIP_TO_NEXT, - getString(R.string.skip_episode_label), - R.drawable.ic_notification_skip - ).build() - ); - } - - WearMediaSession.mediaSessionSetExtraForWear(mediaSession); - - mediaSession.setPlaybackState(sessionState.build()); - } - - private void updateNotificationAndMediaSession(final Playable p) { - setupNotification(p); - updateMediaSessionMetadata(p); - } - - private void updateMediaSessionMetadata(final Playable p) { - if (p == null || mediaSession == null) { - return; - } - - MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); - builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle()); - builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle()); - builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle()); - builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration()); - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle()); - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle()); - - - if (notificationBuilder.isIconCached()) { - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, notificationBuilder.getCachedIcon()); - } else { - String iconUri = p.getImageLocation(); - if (p instanceof FeedMedia) { // Don't use embedded cover etc, which Android can't load - FeedMedia m = (FeedMedia) p; - if (m.getItem() != null) { - FeedItem item = m.getItem(); - if (item.getImageUrl() != null) { - iconUri = item.getImageUrl(); - } else if (item.getFeed() != null) { - iconUri = item.getFeed().getImageUrl(); - } - } - } - if (!TextUtils.isEmpty(iconUri)) { - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, iconUri); - } - } - - if (stateManager.hasReceivedValidStartCommand()) { - mediaSession.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, - PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0))); - try { - mediaSession.setMetadata(builder.build()); - } catch (OutOfMemoryError e) { - Log.e(TAG, "Setting media session metadata", e); - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null); - mediaSession.setMetadata(builder.build()); - } - } - } - - /** - * Used by setupNotification to load notification data in another thread. - */ - private Thread playableIconLoaderThread; - - /** - * Prepares notification and starts the service in the foreground. - */ - private synchronized void setupNotification(final Playable playable) { - Log.d(TAG, "setupNotification"); - if (playableIconLoaderThread != null) { - playableIconLoaderThread.interrupt(); - } - if (playable == null || mediaPlayer == null) { - Log.d(TAG, "setupNotification: playable=" + playable); - Log.d(TAG, "setupNotification: mediaPlayer=" + mediaPlayer); - if (!stateManager.hasReceivedValidStartCommand()) { - stateManager.stopService(); - } - return; - } - - PlayerStatus playerStatus = mediaPlayer.getPlayerStatus(); - notificationBuilder.setPlayable(playable); - notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken()); - notificationBuilder.setPlayerStatus(playerStatus); - notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed()); - - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED) { - notificationManager.notify(R.id.notification_playing, notificationBuilder.build()); - } - - if (!notificationBuilder.isIconCached()) { - playableIconLoaderThread = new Thread(() -> { - Log.d(TAG, "Loading notification icon"); - notificationBuilder.loadIcon(); - if (!Thread.currentThread().isInterrupted()) { - if (ContextCompat.checkSelfPermission(getApplicationContext(), - Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { - notificationManager.notify(R.id.notification_playing, notificationBuilder.build()); - } - updateMediaSessionMetadata(playable); - } - }); - playableIconLoaderThread.start(); - } - } - - /** - * Persists the current position and last played time of the media file. - * - * @param fromMediaPlayer if true, the information is gathered from the current Media Player - * and {@param playable} and {@param position} become irrelevant. - * @param playable the playable for which the current position should be saved, unless - * {@param fromMediaPlayer} is true. - * @param position the position that should be saved, unless {@param fromMediaPlayer} is true. - */ - private synchronized void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) { - int duration; - if (fromMediaPlayer) { - position = getCurrentPosition(); - duration = getDuration(); - playable = mediaPlayer.getPlayable(); - } else { - duration = playable.getDuration(); - } - if (position != Playable.INVALID_TIME && duration != Playable.INVALID_TIME && playable != null) { - Log.d(TAG, "Saving current position to " + position); - PlayableUtils.saveCurrentPosition(playable, position, System.currentTimeMillis()); - } - } - - public boolean sleepTimerActive() { - return taskManager.isSleepTimerActive(); - } - - public long getSleepTimerTimeLeft() { - return taskManager.getSleepTimerTimeLeft(); - } - - private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { - boolean isPlaying = false; - - if (info.playerStatus == PlayerStatus.PLAYING) { - isPlaying = true; - } - - if (info.playable != null) { - Intent i = new Intent(whatChanged); - i.putExtra("id", 1L); - i.putExtra("artist", ""); - i.putExtra("album", info.playable.getFeedTitle()); - i.putExtra("track", info.playable.getEpisodeTitle()); - i.putExtra("playing", isPlaying); - i.putExtra("duration", (long) info.playable.getDuration()); - i.putExtra("position", (long) info.playable.getPosition()); - sendBroadcast(i); - } - } - - private final BroadcastReceiver autoStateUpdated = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String status = intent.getStringExtra("media_connection_status"); - boolean isConnectedToCar = "media_connected".equals(status); - Log.d(TAG, "Received Auto Connection update: " + status); - if (!isConnectedToCar) { - Log.d(TAG, "Car was unplugged during playback."); - } else { - PlayerStatus playerStatus = mediaPlayer.getPlayerStatus(); - if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (playerStatus == PlayerStatus.PREPARING) { - mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); - } else if (playerStatus == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } - } - } - }; - - /** - * Pauses playback when the headset is disconnected and the preference is - * set - */ - private final BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { - private static final String TAG = "headsetDisconnected"; - private static final int UNPLUGGED = 0; - private static final int PLUGGED = 1; - - @Override - public void onReceive(Context context, Intent intent) { - if (isInitialStickyBroadcast()) { - // Don't pause playback after we just started, just because the receiver - // delivers the current headset state (instead of a change) - return; - } - - if (TextUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { - int state = intent.getIntExtra("state", -1); - Log.d(TAG, "Headset plug event. State is " + state); - if (state != -1) { - if (state == UNPLUGGED) { - Log.d(TAG, "Headset was unplugged during playback."); - } else if (state == PLUGGED) { - Log.d(TAG, "Headset was plugged in during playback."); - unpauseIfPauseOnDisconnect(false); - } - } else { - Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); - } - } - } - }; - - private final BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (TextUtils.equals(intent.getAction(), BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { - int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); - if (state == BluetoothA2dp.STATE_CONNECTED) { - Log.d(TAG, "Received bluetooth connection intent"); - unpauseIfPauseOnDisconnect(true); - } - } - } - }; - - private final BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - // sound is about to change, eg. bluetooth -> speaker - Log.d(TAG, "Pausing playback because audio is becoming noisy"); - pauseIfPauseOnDisconnect(); - } - }; - - /** - * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. - */ - private void pauseIfPauseOnDisconnect() { - Log.d(TAG, "pauseIfPauseOnDisconnect()"); - transientPause = (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING); - if (UserPreferences.isPauseOnHeadsetDisconnect() && !isCasting()) { - mediaPlayer.pause(!UserPreferences.isPersistNotify(), false); - } - } - - /** - * @param bluetooth true if the event for unpausing came from bluetooth - */ - private void unpauseIfPauseOnDisconnect(boolean bluetooth) { - if (mediaPlayer.isAudioChannelInUse()) { - Log.d(TAG, "unpauseIfPauseOnDisconnect() audio is in use"); - return; - } - if (transientPause) { - transientPause = false; - if (Build.VERSION.SDK_INT >= 31) { - stateManager.stopService(); - return; - } - if (!bluetooth && UserPreferences.isUnpauseOnHeadsetReconnect()) { - mediaPlayer.resume(); - } else if (bluetooth && UserPreferences.isUnpauseOnBluetoothReconnect()) { - // let the user know we've started playback again... - Vibrator v = (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); - if (v != null) { - v.vibrate(500); - } - mediaPlayer.resume(); - } - } - } - - private final BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (TextUtils.equals(intent.getAction(), PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)); - stateManager.stopService(); - } - } - - }; - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void volumeAdaptionChanged(VolumeAdaptionChangedEvent event) { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, event.getFeedId(), event.getVolumeAdaptionSetting()); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void speedPresetChanged(SpeedPresetChangedEvent event) { - if (getPlayable() instanceof FeedMedia) { - FeedMedia playable = (FeedMedia) getPlayable(); - if (playable.getItem().getFeed().getId() == event.getFeedId()) { - if (event.getSpeed() == SPEED_USE_GLOBAL) { - setSpeed(UserPreferences.getPlaybackSpeed()); - } else { - setSpeed(event.getSpeed()); - } - if (event.getSkipSilence() == FeedPreferences.SkipSilence.GLOBAL) { - setSkipSilence(UserPreferences.isSkipSilence()); - } else { - setSkipSilence(event.getSkipSilence() == FeedPreferences.SkipSilence.AGGRESSIVE); - } - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - @SuppressWarnings("unused") - public void skipIntroEndingPresetChanged(SkipIntroEndingChangedEvent event) { - if (getPlayable() instanceof FeedMedia) { - FeedMedia playable = (FeedMedia) getPlayable(); - if (playable.getItem().getFeed().getId() == event.getFeedId()) { - if (event.getSkipEnding() != 0) { - FeedPreferences feedPreferences = playable.getItem().getFeed().getPreferences(); - feedPreferences.setFeedSkipIntro(event.getSkipIntro()); - feedPreferences.setFeedSkipEnding(event.getSkipEnding()); - } - } - } - } - - public static MediaType getCurrentMediaType() { - return currentMediaType; - } - - public static boolean isCasting() { - return isCasting; - } - - public void resume() { - mediaPlayer.resume(); - taskManager.restartSleepTimer(); - } - - public void prepare() { - mediaPlayer.prepare(); - taskManager.restartSleepTimer(); - } - - public void pause(boolean abandonAudioFocus, boolean reinit) { - mediaPlayer.pause(abandonAudioFocus, reinit); - } - - public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { - return mediaPlayer.getPSMPInfo(); - } - - public PlayerStatus getStatus() { - return mediaPlayer.getPlayerStatus(); - } - - public Playable getPlayable() { - return mediaPlayer.getPlayable(); - } - - public void setSpeed(float speed) { - PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(speed); - mediaPlayer.setPlaybackParams(speed, getCurrentSkipSilence()); - } - - public void setSkipSilence(boolean skipSilence) { - PlaybackPreferences.setCurrentlyPlayingTemporarySkipSilence(skipSilence); - mediaPlayer.setPlaybackParams(getCurrentPlaybackSpeed(), skipSilence); - } - - public float getCurrentPlaybackSpeed() { - if (mediaPlayer == null) { - return 1.0f; - } - return mediaPlayer.getPlaybackSpeed(); - } - - public boolean getCurrentSkipSilence() { - if (mediaPlayer == null) { - return false; - } - return mediaPlayer.getSkipSilence(); - } - - public boolean isStartWhenPrepared() { - return mediaPlayer.isStartWhenPrepared(); - } - - public void setStartWhenPrepared(boolean s) { - mediaPlayer.setStartWhenPrepared(s); - } - - public void seekTo(final int t) { - mediaPlayer.seekTo(t); - EventBus.getDefault().post(new PlaybackPositionEvent(t, getDuration())); - } - - private void seekDelta(final int d) { - mediaPlayer.seekDelta(d); - } - - /** - * call getDuration() on mediaplayer or return INVALID_TIME if player is in - * an invalid state. - */ - public int getDuration() { - if (mediaPlayer == null) { - return Playable.INVALID_TIME; - } - return mediaPlayer.getDuration(); - } - - /** - * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player - * is in an invalid state. - */ - public int getCurrentPosition() { - if (mediaPlayer == null) { - return Playable.INVALID_TIME; - } - return mediaPlayer.getPosition(); - } - - public List<String> getAudioTracks() { - if (mediaPlayer == null) { - return Collections.emptyList(); - } - return mediaPlayer.getAudioTracks(); - } - - public int getSelectedAudioTrack() { - if (mediaPlayer == null) { - return -1; - } - return mediaPlayer.getSelectedAudioTrack(); - } - - public void setAudioTrack(int track) { - if (mediaPlayer != null) { - mediaPlayer.setAudioTrack(track); - } - } - - public boolean isStreaming() { - return mediaPlayer.isStreaming(); - } - - public Pair<Integer, Integer> getVideoSize() { - return mediaPlayer.getVideoSize(); - } - - private void setupPositionObserver() { - if (positionEventTimer != null) { - positionEventTimer.dispose(); - } - - Log.d(TAG, "Setting up position observer"); - positionEventTimer = Observable.interval(1, TimeUnit.SECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(number -> { - EventBus.getDefault().post(new PlaybackPositionEvent(getCurrentPosition(), getDuration())); - if (Build.VERSION.SDK_INT < 29) { - notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed()); - NotificationManager notificationManager = (NotificationManager) - getSystemService(NOTIFICATION_SERVICE); - if (ContextCompat.checkSelfPermission(getApplicationContext(), - Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { - notificationManager.notify(R.id.notification_playing, notificationBuilder.build()); - } - } - skipEndingIfNecessary(); - }); - } - - private void cancelPositionObserver() { - if (positionEventTimer != null) { - positionEventTimer.dispose(); - } - } - - private void addPlayableToQueue(Playable playable) { - if (playable instanceof FeedMedia) { - long itemId = ((FeedMedia) playable).getItem().getId(); - DBWriter.addQueueItem(this, false, true, itemId); - notifyChildrenChanged(getString(R.string.queue_label)); - } - } - - private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() { - - private static final String TAG = "MediaSessionCompat"; - - @Override - public void onPlay() { - Log.d(TAG, "onPlay()"); - PlayerStatus status = getStatus(); - if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - resume(); - } else if (status == PlayerStatus.INITIALIZED) { - setStartWhenPrepared(true); - prepare(); - } - } - - @Override - public void onPlayFromMediaId(String mediaId, Bundle extras) { - Log.d(TAG, "onPlayFromMediaId: mediaId: " + mediaId + " extras: " + extras.toString()); - FeedMedia p = DBReader.getFeedMedia(Long.parseLong(mediaId)); - if (p != null) { - startPlaying(p, false); - } - } - - @Override - public void onPlayFromSearch(String query, Bundle extras) { - Log.d(TAG, "onPlayFromSearch query=" + query + " extras=" + extras.toString()); - - if (query.equals("")) { - Log.d(TAG, "onPlayFromSearch called with empty query, resuming from the last position"); - startPlayingFromPreferences(); - return; - } - - List<FeedItem> results = DBReader.searchFeedItems(0, query); - if (results.size() > 0 && results.get(0).getMedia() != null) { - FeedMedia media = results.get(0).getMedia(); - startPlaying(media, false); - return; - } - onPlay(); - } - - @Override - public void onPause() { - Log.d(TAG, "onPause()"); - if (getStatus() == PlayerStatus.PLAYING) { - pause(!UserPreferences.isPersistNotify(), false); - } - } - - @Override - public void onStop() { - Log.d(TAG, "onStop()"); - mediaPlayer.stopPlayback(true); - } - - @Override - public void onSkipToPrevious() { - Log.d(TAG, "onSkipToPrevious()"); - seekDelta(-UserPreferences.getRewindSecs() * 1000); - } - - @Override - public void onRewind() { - Log.d(TAG, "onRewind()"); - seekDelta(-UserPreferences.getRewindSecs() * 1000); - } - - public void onNextChapter() { - List<Chapter> chapters = mediaPlayer.getPlayable().getChapters(); - if (chapters == null) { - // No chapters, just fallback to next episode - mediaPlayer.skip(); - return; - } - - int nextChapter = ChapterUtils.getCurrentChapterIndex( - mediaPlayer.getPlayable(), mediaPlayer.getPosition()) + 1; - - if (chapters.size() < nextChapter + 1) { - // We are on the last chapter, just fallback to the next episode - mediaPlayer.skip(); - return; - } - - mediaPlayer.seekTo((int) chapters.get(nextChapter).getStart()); - } - - @Override - public void onFastForward() { - Log.d(TAG, "onFastForward()"); - seekDelta(UserPreferences.getFastForwardSecs() * 1000); - } - - @Override - public void onSkipToNext() { - Log.d(TAG, "onSkipToNext()"); - UiModeManager uiModeManager = (UiModeManager) getApplicationContext() - .getSystemService(Context.UI_MODE_SERVICE); - if (UserPreferences.getHardwareForwardButton() == KeyEvent.KEYCODE_MEDIA_NEXT - || uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { - mediaPlayer.skip(); - } else { - seekDelta(UserPreferences.getFastForwardSecs() * 1000); - } - } - - - @Override - public void onSeekTo(long pos) { - Log.d(TAG, "onSeekTo()"); - seekTo((int) pos); - } - - @Override - public void onSetPlaybackSpeed(float speed) { - Log.d(TAG, "onSetPlaybackSpeed()"); - setSpeed(speed); - } - - @Override - public boolean onMediaButtonEvent(final Intent mediaButton) { - Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")"); - if (mediaButton != null) { - KeyEvent keyEvent = mediaButton.getParcelableExtra(Intent.EXTRA_KEY_EVENT); - if (keyEvent != null && - keyEvent.getAction() == KeyEvent.ACTION_DOWN && - keyEvent.getRepeatCount() == 0) { - int keyCode = keyEvent.getKeyCode(); - if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { - clickCount++; - clickHandler.removeCallbacksAndMessages(null); - clickHandler.postDelayed(() -> { - if (clickCount == 1) { - handleKeycode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false); - } else if (clickCount == 2) { - onFastForward(); - } else if (clickCount == 3) { - onRewind(); - } - clickCount = 0; - }, ViewConfiguration.getDoubleTapTimeout()); - return true; - } else { - return handleKeycode(keyCode, false); - } - } - } - return false; - } - - @Override - public void onCustomAction(String action, Bundle extra) { - Log.d(TAG, "onCustomAction(" + action + ")"); - if (CUSTOM_ACTION_FAST_FORWARD.equals(action)) { - onFastForward(); - } else if (CUSTOM_ACTION_REWIND.equals(action)) { - onRewind(); - } else if (CUSTOM_ACTION_SKIP_TO_NEXT.equals(action)) { - mediaPlayer.skip(); - } else if (CUSTOM_ACTION_NEXT_CHAPTER.equals(action)) { - onNextChapter(); - } else if (CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED.equals(action)) { - List<Float> selectedSpeeds = UserPreferences.getPlaybackSpeedArray(); - - // If the list has zero or one element, there's nothing we can do to change the playback speed. - if (selectedSpeeds.size() > 1) { - int speedPosition = selectedSpeeds.indexOf(mediaPlayer.getPlaybackSpeed()); - float newSpeed; - - if (speedPosition == selectedSpeeds.size() - 1) { - // This is the last element. Wrap instead of going over the size of the list. - newSpeed = selectedSpeeds.get(0); - } else { - // If speedPosition is still -1 (the user isn't using a preset), use the first preset in the - // list. - newSpeed = selectedSpeeds.get(speedPosition + 1); - } - onSetPlaybackSpeed(newSpeed); - } - } else if (CUSTOM_ACTION_TOGGLE_SLEEP_TIMER.equals(action)) { - if (sleepTimerActive()) { - disableSleepTimer(); - } else { - setSleepTimer(SleepTimerPreferences.timerMillis()); - } - } - } - }; -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceInterface.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceInterface.java deleted file mode 100644 index 18ead601d..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceInterface.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -public abstract class PlaybackServiceInterface { - public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - public static final String EXTRA_ALLOW_STREAM_THIS_TIME = "extra.de.danoeh.antennapod.core.service.allowStream"; - public static final String EXTRA_ALLOW_STREAM_ALWAYS = "extra.de.danoeh.antennapod.core.service.allowStreamAlways"; - - public static final String ACTION_PLAYER_NOTIFICATION - = "action.de.danoeh.antennapod.core.service.playerNotification"; - public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.core.service.notificationCode"; - public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.core.service.notificationType"; - public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; - public static final int NOTIFICATION_TYPE_RELOAD = 3; - public static final int EXTRA_CODE_AUDIO = 1; // Used in NOTIFICATION_TYPE_RELOAD - public static final int EXTRA_CODE_VIDEO = 2; - public static final int EXTRA_CODE_CAST = 3; - - public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE - = "action.de.danoeh.antennapod.core.service.actionShutdownPlaybackService"; -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java deleted file mode 100644 index 140f35c90..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java +++ /dev/null @@ -1,273 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.app.Notification; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.VectorDrawable; -import android.os.Build; -import android.support.v4.media.session.MediaSessionCompat; -import android.util.Log; -import android.view.KeyEvent; -import androidx.annotation.NonNull; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.app.NotificationCompat; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.ui.common.Converter; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.ui.episodes.ImageResourceUtils; -import de.danoeh.antennapod.ui.episodes.TimeSpeedConverter; -import de.danoeh.antennapod.ui.notifications.NotificationUtils; -import java.util.ArrayList; -import java.util.concurrent.ExecutionException; - -import de.danoeh.antennapod.playback.base.PlayerStatus; -import org.apache.commons.lang3.ArrayUtils; - -public class PlaybackServiceNotificationBuilder { - private static final String TAG = "PlaybackSrvNotification"; - private static Bitmap defaultIcon = null; - - private final Context context; - private Playable playable; - private MediaSessionCompat.Token mediaSessionToken; - private PlayerStatus playerStatus; - private Bitmap icon; - private String position; - - public PlaybackServiceNotificationBuilder(@NonNull Context context) { - this.context = context; - } - - public void setPlayable(Playable playable) { - if (playable != this.playable) { - clearCache(); - } - this.playable = playable; - } - - private void clearCache() { - this.icon = null; - this.position = null; - } - - public void updatePosition(int position, float speed) { - TimeSpeedConverter converter = new TimeSpeedConverter(speed); - this.position = Converter.getDurationStringLong(converter.convert(position)); - } - - public boolean isIconCached() { - return icon != null; - } - - public void loadIcon() { - int iconSize = (int) (128 * context.getResources().getDisplayMetrics().density); - final RequestOptions options = new RequestOptions().centerCrop(); - try { - icon = Glide.with(context) - .asBitmap() - .load(playable.getImageLocation()) - .apply(options) - .submit(iconSize, iconSize) - .get(); - } catch (ExecutionException e) { - try { - icon = Glide.with(context) - .asBitmap() - .load(ImageResourceUtils.getFallbackImageLocation(playable)) - .apply(options) - .submit(iconSize, iconSize) - .get(); - } catch (InterruptedException ignore) { - Log.e(TAG, "Media icon loader was interrupted"); - } catch (Throwable tr) { - Log.e(TAG, "Error loading the media icon for the notification", tr); - } - } catch (InterruptedException ignore) { - Log.e(TAG, "Media icon loader was interrupted"); - } catch (Throwable tr) { - Log.e(TAG, "Error loading the media icon for the notification", tr); - } - } - - public Bitmap getCachedIcon() { - return icon; - } - - private Bitmap getDefaultIcon() { - if (defaultIcon == null) { - defaultIcon = getBitmap(context, R.mipmap.ic_launcher); - } - return defaultIcon; - } - - private static Bitmap getBitmap(VectorDrawable vectorDrawable) { - Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), - vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - vectorDrawable.draw(canvas); - return bitmap; - } - - private static Bitmap getBitmap(Context context, int drawableId) { - Drawable drawable = AppCompatResources.getDrawable(context, drawableId); - if (drawable instanceof BitmapDrawable) { - return ((BitmapDrawable) drawable).getBitmap(); - } else if (drawable instanceof VectorDrawable) { - return getBitmap((VectorDrawable) drawable); - } else { - return null; - } - } - - public Notification build() { - NotificationCompat.Builder notification = new NotificationCompat.Builder(context, - NotificationUtils.CHANNEL_ID_PLAYING); - - if (playable != null) { - notification.setContentTitle(playable.getFeedTitle()); - notification.setContentText(playable.getEpisodeTitle()); - addActions(notification, mediaSessionToken, playerStatus); - - if (icon != null) { - notification.setLargeIcon(icon); - } else { - notification.setLargeIcon(getDefaultIcon()); - } - - if (Build.VERSION.SDK_INT < 29) { - notification.setSubText(position); - } - } else { - notification.setContentTitle(context.getString(R.string.app_name)); - notification.setContentText("Loading. If this does not go away, play any episode and contact us."); - } - - notification.setContentIntent(getPlayerActivityPendingIntent()); - notification.setWhen(0); - notification.setSmallIcon(R.drawable.ic_notification); - notification.setOngoing(false); - notification.setOnlyAlertOnce(true); - notification.setShowWhen(false); - notification.setPriority(UserPreferences.getNotifyPriority()); - notification.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); - notification.setColor(NotificationCompat.COLOR_DEFAULT); - return notification.build(); - } - - private PendingIntent getPlayerActivityPendingIntent() { - return PendingIntent.getActivity(context, R.id.pending_intent_player_activity, - PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - - private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken, - PlayerStatus playerStatus) { - ArrayList<Integer> compactActionList = new ArrayList<>(); - - int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction - - PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_REWIND, numActions); - notification.addAction(R.drawable.ic_notification_fast_rewind, context.getString(R.string.rewind_label), - rewindButtonPendingIntent); - compactActionList.add(numActions); - numActions++; - - if (playerStatus == PlayerStatus.PLAYING) { - PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_PAUSE, numActions); - notification.addAction(R.drawable.ic_notification_pause, //pause action - context.getString(R.string.pause_label), - pauseButtonPendingIntent); - } else { - PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_PLAY, numActions); - notification.addAction(R.drawable.ic_notification_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); - notification.addAction(R.drawable.ic_notification_fast_forward, context.getString(R.string.fast_forward_label), - ffButtonPendingIntent); - compactActionList.add(numActions); - numActions++; - - if (UserPreferences.showNextChapterOnFullNotification() && playable.getChapters() != null) { - PendingIntent nextChapterPendingIntent = getPendingIntentForCustomMediaAction( - PlaybackService.CUSTOM_ACTION_NEXT_CHAPTER, numActions); - notification.addAction(R.drawable.ic_notification_next_chapter, context.getString(R.string.next_chapter), - nextChapterPendingIntent); - numActions++; - } - - if (UserPreferences.showSkipOnFullNotification()) { - PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_NEXT, numActions); - notification.addAction(R.drawable.ic_notification_skip, context.getString(R.string.skip_episode_label), - skipButtonPendingIntent); - numActions++; - } - - PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_STOP, numActions); - notification.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() - .setMediaSession(mediaSessionToken) - .setShowActionsInCompactView(ArrayUtils.toPrimitive(compactActionList.toArray(new Integer[0]))) - .setShowCancelButton(true) - .setCancelButtonIntent(stopButtonPendingIntent)); - } - - private PendingIntent getPendingIntentForMediaAction(int keycodeValue, int requestCode) { - Intent intent = new Intent(context, PlaybackService.class); - intent.setAction("MediaCode" + keycodeValue); - intent.putExtra(MediaButtonReceiver.EXTRA_KEYCODE, keycodeValue); - - if (Build.VERSION.SDK_INT >= 26) { - return PendingIntent.getForegroundService(context, requestCode, intent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } else { - return PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - } - - private PendingIntent getPendingIntentForCustomMediaAction(String action, int requestCode) { - Intent intent = new Intent(context, PlaybackService.class); - intent.setAction("MediaAction" + action); - intent.putExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION, action); - - if (Build.VERSION.SDK_INT >= 26) { - return PendingIntent.getForegroundService(context, requestCode, intent, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } else { - return PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - } - } - - public void setMediaSessionToken(MediaSessionCompat.Token mediaSessionToken) { - this.mediaSessionToken = mediaSessionToken; - } - - public void setPlayerStatus(PlayerStatus playerStatus) { - this.playerStatus = playerStatus; - } - - public PlayerStatus getPlayerStatus() { - return playerStatus; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java deleted file mode 100644 index 9926e01da..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.app.Notification; -import android.util.Log; - -import androidx.core.app.ServiceCompat; - -class PlaybackServiceStateManager { - private static final String TAG = "PlaybackSrvState"; - private final PlaybackService playbackService; - - private volatile boolean isInForeground = false; - private volatile boolean hasReceivedValidStartCommand = false; - - PlaybackServiceStateManager(PlaybackService playbackService) { - this.playbackService = playbackService; - } - - void startForeground(int notificationId, Notification notification) { - Log.d(TAG, "startForeground"); - playbackService.startForeground(notificationId, notification); - isInForeground = true; - } - - void stopService() { - Log.d(TAG, "stopService"); - stopForeground(true); - playbackService.stopSelf(); - hasReceivedValidStartCommand = false; - } - - void stopForeground(boolean removeNotification) { - Log.d(TAG, "stopForeground"); - if (isInForeground) { - if (removeNotification) { - ServiceCompat.stopForeground(playbackService, ServiceCompat.STOP_FOREGROUND_REMOVE); - } else { - ServiceCompat.stopForeground(playbackService, ServiceCompat.STOP_FOREGROUND_DETACH); - } - } - isInForeground = false; - } - - boolean hasReceivedValidStartCommand() { - return hasReceivedValidStartCommand; - } - - void validStartCommandWasReceived() { - this.hasReceivedValidStartCommand = true; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java deleted file mode 100644 index bee4619ef..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ /dev/null @@ -1,365 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.os.Vibrator; -import androidx.annotation.NonNull; -import android.util.Log; - -import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; -import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.ui.widget.WidgetUpdater; -import io.reactivex.disposables.Disposable; -import org.greenrobot.eventbus.EventBus; - -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.model.playback.Playable; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; - - -/** - * Manages the background tasks of PlaybackSerivce, i.e. - * the sleep timer, the position saver, the widget updater and - * the queue loader. - * <p/> - * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback) - * to notify the PlaybackService about updates from the running tasks. - */ -public class PlaybackServiceTaskManager { - private static final String TAG = "PlaybackServiceTaskMgr"; - - /** - * Update interval of position saver in milliseconds. - */ - public static final int POSITION_SAVER_WAITING_INTERVAL = 5000; - /** - * Notification interval of widget updater in milliseconds. - */ - public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1000; - - private static final int SCHED_EX_POOL_SIZE = 2; - private final ScheduledThreadPoolExecutor schedExecutor; - - private ScheduledFuture<?> positionSaverFuture; - private ScheduledFuture<?> widgetUpdaterFuture; - private ScheduledFuture<?> sleepTimerFuture; - private volatile Disposable chapterLoaderFuture; - - private SleepTimer sleepTimer; - - private final Context context; - private final PSTMCallback callback; - - /** - * Sets up a new PSTM. This method will also start the queue loader task. - * - * @param context - * @param callback A PSTMCallback object for notifying the user about updates. Must not be null. - */ - public PlaybackServiceTaskManager(@NonNull Context context, - @NonNull PSTMCallback callback) { - this.context = context; - this.callback = callback; - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, r -> { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }); - } - - /** - * Starts the position saver task. If the position saver is already active, nothing will happen. - */ - public synchronized void startPositionSaver() { - if (!isPositionSaverActive()) { - Runnable positionSaver = callback::positionSaverTick; - positionSaver = useMainThreadIfNecessary(positionSaver); - positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, - POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); - - Log.d(TAG, "Started PositionSaver"); - } else { - Log.d(TAG, "Call to startPositionSaver was ignored."); - } - } - - /** - * Returns true if the position saver is currently running. - */ - public synchronized boolean isPositionSaverActive() { - return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone(); - } - - /** - * Cancels the position saver. If the position saver is not running, nothing will happen. - */ - public synchronized void cancelPositionSaver() { - if (isPositionSaverActive()) { - positionSaverFuture.cancel(false); - Log.d(TAG, "Cancelled PositionSaver"); - } - } - - /** - * Starts the widget updater task. If the widget updater is already active, nothing will happen. - */ - public synchronized void startWidgetUpdater() { - if (!isWidgetUpdaterActive() && !schedExecutor.isShutdown()) { - Runnable widgetUpdater = this::requestWidgetUpdate; - widgetUpdater = useMainThreadIfNecessary(widgetUpdater); - widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, - WIDGET_UPDATER_NOTIFICATION_INTERVAL, WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); - Log.d(TAG, "Started WidgetUpdater"); - } else { - Log.d(TAG, "Call to startWidgetUpdater was ignored."); - } - } - - /** - * Retrieves information about the widget state in the calling thread and then displays it in a background thread. - */ - public synchronized void requestWidgetUpdate() { - WidgetUpdater.WidgetState state = callback.requestWidgetState(); - if (!schedExecutor.isShutdown()) { - schedExecutor.execute(() -> WidgetUpdater.updateWidget(context, state)); - } else { - Log.d(TAG, "Call to requestWidgetUpdate was ignored."); - } - } - - /** - * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be - * cancelled first. - * After waitingTime has elapsed, onSleepTimerExpired() will be called. - * - * @throws java.lang.IllegalArgumentException if waitingTime <= 0 - */ - public synchronized void setSleepTimer(long waitingTime) { - if (waitingTime <= 0) { - throw new IllegalArgumentException("Waiting time <= 0"); - } - - Log.d(TAG, "Setting sleep timer to " + waitingTime + " milliseconds"); - if (isSleepTimerActive()) { - sleepTimerFuture.cancel(true); - } - sleepTimer = new SleepTimer(waitingTime); - sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); - EventBus.getDefault().post(SleepTimerUpdatedEvent.justEnabled(waitingTime)); - } - - /** - * Returns true if the sleep timer is currently active. - */ - public synchronized boolean isSleepTimerActive() { - return sleepTimer != null - && sleepTimerFuture != null - && !sleepTimerFuture.isCancelled() - && !sleepTimerFuture.isDone() - && sleepTimer.getWaitingTime() > 0; - } - - /** - * Disables the sleep timer. If the sleep timer is not active, nothing will happen. - */ - public synchronized void disableSleepTimer() { - if (isSleepTimerActive()) { - Log.d(TAG, "Disabling sleep timer"); - sleepTimer.cancel(); - } - } - - /** - * Restarts the sleep timer. If the sleep timer is not active, nothing will happen. - */ - public synchronized void restartSleepTimer() { - if (isSleepTimerActive()) { - Log.d(TAG, "Restarting sleep timer"); - sleepTimer.restart(); - } - } - - /** - * Returns the current sleep timer time or 0 if the sleep timer is not active. - */ - public synchronized long getSleepTimerTimeLeft() { - if (isSleepTimerActive()) { - return sleepTimer.getWaitingTime(); - } else { - return 0; - } - } - - /** - * Returns true if the widget updater is currently running. - */ - public synchronized boolean isWidgetUpdaterActive() { - return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone(); - } - - /** - * Cancels the widget updater. If the widget updater is not running, nothing will happen. - */ - public synchronized void cancelWidgetUpdater() { - if (isWidgetUpdaterActive()) { - widgetUpdaterFuture.cancel(false); - Log.d(TAG, "Cancelled WidgetUpdater"); - } - } - - /** - * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, - * it will be cancelled first. - * On completion, the callback's onChapterLoaded method will be called. - */ - public synchronized void startChapterLoader(@NonNull final Playable media) { - if (chapterLoaderFuture != null) { - chapterLoaderFuture.dispose(); - chapterLoaderFuture = null; - } - - if (media.getChapters() == null) { - chapterLoaderFuture = Completable.create(emitter -> { - ChapterUtils.loadChapters(media, context, false); - emitter.onComplete(); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(() -> callback.onChapterLoaded(media), - throwable -> Log.d(TAG, "Error loading chapters: " + Log.getStackTraceString(throwable))); - } - } - - - /** - * Cancels all tasks. The PSTM will be in the initial state after execution of this method. - */ - public synchronized void cancelAllTasks() { - cancelPositionSaver(); - cancelWidgetUpdater(); - disableSleepTimer(); - - if (chapterLoaderFuture != null) { - chapterLoaderFuture.dispose(); - chapterLoaderFuture = null; - } - } - - /** - * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after - * execution of this method. - */ - public void shutdown() { - cancelAllTasks(); - schedExecutor.shutdownNow(); - } - - private Runnable useMainThreadIfNecessary(Runnable runnable) { - if (Looper.myLooper() == Looper.getMainLooper()) { - // Called in main thread => ExoPlayer is used - // Run on ui thread even if called from schedExecutor - Handler handler = new Handler(Looper.getMainLooper()); - return () -> handler.post(runnable); - } else { - return runnable; - } - } - - /** - * Sleeps for a given time and then pauses playback. - */ - class SleepTimer implements Runnable { - private static final String TAG = "SleepTimer"; - private static final long UPDATE_INTERVAL = 1000L; - public static final long NOTIFICATION_THRESHOLD = 10000; - private boolean hasVibrated = false; - private final long waitingTime; - private long timeLeft; - private ShakeListener shakeListener; - - public SleepTimer(long waitingTime) { - super(); - this.waitingTime = waitingTime; - this.timeLeft = waitingTime; - } - - @Override - public void run() { - Log.d(TAG, "Starting"); - long lastTick = System.currentTimeMillis(); - EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(timeLeft)); - while (timeLeft > 0) { - try { - Thread.sleep(UPDATE_INTERVAL); - } catch (InterruptedException e) { - Log.d(TAG, "Thread was interrupted while waiting"); - e.printStackTrace(); - break; - } - - long now = System.currentTimeMillis(); - timeLeft -= now - lastTick; - lastTick = now; - - EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(timeLeft)); - if (timeLeft < NOTIFICATION_THRESHOLD) { - Log.d(TAG, "Sleep timer is about to expire"); - if (SleepTimerPreferences.vibrate() && !hasVibrated) { - Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); - if (v != null) { - v.vibrate(500); - hasVibrated = true; - } - } - if (shakeListener == null && SleepTimerPreferences.shakeToReset()) { - shakeListener = new ShakeListener(context, this); - } - } - if (timeLeft <= 0) { - Log.d(TAG, "Sleep timer expired"); - if (shakeListener != null) { - shakeListener.pause(); - shakeListener = null; - } - hasVibrated = false; - } - } - } - - public long getWaitingTime() { - return timeLeft; - } - - public void restart() { - EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled()); - setSleepTimer(waitingTime); - if (shakeListener != null) { - shakeListener.pause(); - shakeListener = null; - } - } - - public void cancel() { - sleepTimerFuture.cancel(true); - if (shakeListener != null) { - shakeListener.pause(); - } - EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled()); - } - } - - public interface PSTMCallback { - void positionSaverTick(); - - WidgetUpdater.WidgetState requestWidgetState(); - - void onChapterLoaded(Playable media); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java deleted file mode 100644 index 43837a473..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java +++ /dev/null @@ -1,38 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.playback.base.PlayerStatus; - -class PlaybackVolumeUpdater { - - public void updateVolumeIfNecessary(PlaybackServiceMediaPlayer mediaPlayer, long feedId, - VolumeAdaptionSetting volumeAdaptionSetting) { - Playable playable = mediaPlayer.getPlayable(); - - if (playable instanceof FeedMedia) { - updateFeedMediaVolumeIfNecessary(mediaPlayer, feedId, volumeAdaptionSetting, (FeedMedia) playable); - } - } - - private void updateFeedMediaVolumeIfNecessary(PlaybackServiceMediaPlayer mediaPlayer, long feedId, - VolumeAdaptionSetting volumeAdaptionSetting, FeedMedia feedMedia) { - if (feedMedia.getItem().getFeed().getId() == feedId) { - FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences(); - preferences.setVolumeAdaptionSetting(volumeAdaptionSetting); - - if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { - forceUpdateVolume(mediaPlayer); - } - } - } - - private void forceUpdateVolume(PlaybackServiceMediaPlayer mediaPlayer) { - mediaPlayer.pause(false, false); - mediaPlayer.resume(); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java deleted file mode 100644 index b967577af..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java +++ /dev/null @@ -1,63 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.util.Log; - -class ShakeListener implements SensorEventListener -{ - private static final String TAG = ShakeListener.class.getSimpleName(); - - private Sensor mAccelerometer; - private SensorManager mSensorMgr; - private final PlaybackServiceTaskManager.SleepTimer mSleepTimer; - private final Context mContext; - - public ShakeListener(Context context, PlaybackServiceTaskManager.SleepTimer sleepTimer) { - mContext = context; - mSleepTimer = sleepTimer; - resume(); - } - - private void resume() { - // only a precaution, the user should actually not be able to activate shake to reset - // when the accelerometer is not available - mSensorMgr = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); - if (mSensorMgr == null) { - throw new UnsupportedOperationException("Sensors not supported"); - } - mAccelerometer = mSensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - if (!mSensorMgr.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI)) { // if not supported - mSensorMgr.unregisterListener(this); - throw new UnsupportedOperationException("Accelerometer not supported"); - } - } - - public void pause() { - if (mSensorMgr != null) { - mSensorMgr.unregisterListener(this); - mSensorMgr = null; - } - } - - @Override - public void onSensorChanged(SensorEvent event) { - float gX = event.values[0] / SensorManager.GRAVITY_EARTH; - float gY = event.values[1] / SensorManager.GRAVITY_EARTH; - float gZ = event.values[2] / SensorManager.GRAVITY_EARTH; - - double gForce = Math.sqrt(gX*gX + gY*gY + gZ*gZ); - if (gForce > 2.25) { - Log.d(TAG, "Detected shake " + gForce); - mSleepTimer.restart(); - } - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - } - -}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java deleted file mode 100644 index 854562e2c..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.os.Bundle; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; - -public class WearMediaSession { - /** - * Take a custom action builder and make sure the custom action shows on Wear OS because this is the Play version - * of the app. - */ - static void addWearExtrasToAction(PlaybackStateCompat.CustomAction.Builder actionBuilder) { - Bundle actionExtras = new Bundle(); - actionExtras.putBoolean("android.support.wearable.media.extra.CUSTOM_ACTION_SHOW_ON_WEAR", true); - actionBuilder.setExtras(actionExtras); - } - - static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) { - Bundle sessionExtras = new Bundle(); - sessionExtras.putBoolean("android.support.wearable.media.extra.RESERVE_SLOT_SKIP_TO_PREVIOUS", false); - sessionExtras.putBoolean("android.support.wearable.media.extra.RESERVE_SLOT_SKIP_TO_NEXT", false); - mediaSession.setExtras(sessionExtras); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java index 145693f2c..93f7d578a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -9,7 +9,6 @@ import java.util.List; import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.core.util.PlaybackStatus; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; @@ -73,7 +72,6 @@ public class AutomaticDownloadAlgorithm { if (!item.isAutoDownloadEnabled() || item.isDownloaded() || !item.hasMedia() - || PlaybackStatus.isPlaying(item.getMedia()) || item.getFeed().isLocalFeed()) { it.remove(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/PlaybackStatus.java b/core/src/main/java/de/danoeh/antennapod/core/util/PlaybackStatus.java deleted file mode 100644 index 6fb7c487b..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/PlaybackStatus.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.model.feed.FeedMedia; - -public abstract class PlaybackStatus { - /** - * Reads playback preferences to determine whether this FeedMedia object is - * currently being played and the current player status is playing. - */ - public static boolean isCurrentlyPlaying(FeedMedia media) { - return isPlaying(media) && PlaybackService.isRunning - && ((PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PLAYING)); - } - - public static boolean isPlaying(FeedMedia media) { - return PlaybackPreferences.getCurrentlyPlayingMediaType() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA - && media != null - && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == media.getId(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java deleted file mode 100644 index 9a622d440..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.Playable; - -/** - * Provides utility methods for Playable objects. - */ -public abstract class PlayableUtils { - /** - * Saves the current position of this object. - * - * @param newPosition new playback position in ms - * @param timestamp current time in ms - */ - public static void saveCurrentPosition(Playable playable, int newPosition, long timestamp) { - playable.setPosition(newPosition); - playable.setLastPlayedTime(timestamp); - - if (playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - if (item != null && item.isNew()) { - DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); - } - if (media.getStartPosition() >= 0 && playable.getPosition() > media.getStartPosition()) { - media.setPlayedDuration(media.getPlayedDurationWhenStarted() - + playable.getPosition() - media.getStartPosition()); - } - DBWriter.setFeedMediaPlaybackInformation(media); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java deleted file mode 100644 index 41342cb1f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ /dev/null @@ -1,480 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.os.IBinder; -import android.util.Log; -import android.util.Pair; -import android.view.SurfaceHolder; -import androidx.annotation.NonNull; -import de.danoeh.antennapod.core.service.playback.PlaybackServiceInterface; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.DBWriter; -import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; -import de.danoeh.antennapod.event.playback.SpeedChangedEvent; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.playback.base.PlayerStatus; -import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.Collections; -import java.util.List; - -/** - * Communicates with the playback service. GUI classes should use this class to - * control playback instead of communicating with the PlaybackService directly. - */ -public abstract class PlaybackController { - - private static final String TAG = "PlaybackController"; - - private final Activity activity; - private PlaybackService playbackService; - private Playable media; - private PlayerStatus status = PlayerStatus.STOPPED; - - private boolean mediaInfoLoaded = false; - private boolean released = false; - private boolean initialized = false; - private boolean eventsRegistered = false; - private long loadedFeedMedia = -1; - - public PlaybackController(@NonNull Activity activity) { - this.activity = activity; - } - - /** - * Creates a new connection to the playbackService. - */ - public synchronized void init() { - if (!eventsRegistered) { - EventBus.getDefault().register(this); - eventsRegistered = true; - } - if (PlaybackService.isRunning) { - initServiceRunning(); - } else { - updatePlayButtonShowsPlay(true); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onEventMainThread(PlaybackServiceEvent event) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_STARTED) { - init(); - } - } - - private synchronized void initServiceRunning() { - if (initialized) { - return; - } - initialized = true; - - activity.registerReceiver(statusUpdate, new IntentFilter( - PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); - activity.registerReceiver(notificationReceiver, new IntentFilter( - PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION)); - - if (!released) { - bindToService(); - } else { - throw new IllegalStateException("Can't call init() after release() has been called"); - } - checkMediaInfoLoaded(); - } - - /** - * Should be called if the PlaybackController is no longer needed, for - * example in the activity's onStop() method. - */ - public void release() { - Log.d(TAG, "Releasing PlaybackController"); - - try { - activity.unregisterReceiver(statusUpdate); - } catch (IllegalArgumentException e) { - // ignore - } - - try { - activity.unregisterReceiver(notificationReceiver); - } catch (IllegalArgumentException e) { - // ignore - } - unbind(); - media = null; - released = true; - - if (eventsRegistered) { - EventBus.getDefault().unregister(this); - eventsRegistered = false; - } - } - - private void unbind() { - try { - activity.unbindService(mConnection); - } catch (IllegalArgumentException e) { - // ignore - } - initialized = false; - } - - /** - * Should be called in the activity's onPause() method. - */ - public void pause() { - mediaInfoLoaded = false; - } - - /** - * Tries to establish a connection to the PlaybackService. If it isn't - * running, the PlaybackService will be started with the last played media - * as the arguments of the launch intent. - */ - private void bindToService() { - Log.d(TAG, "Trying to connect to service"); - if (!PlaybackService.isRunning) { - throw new IllegalStateException("Trying to bind but service is not running"); - } - boolean bound = activity.bindService(new Intent(activity, PlaybackService.class), mConnection, 0); - Log.d(TAG, "Result for service binding: " + bound); - } - - private final ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - if(service instanceof PlaybackService.LocalBinder) { - playbackService = ((PlaybackService.LocalBinder) service).getService(); - if (!released) { - queryService(); - Log.d(TAG, "Connection to Service established"); - } else { - Log.i(TAG, "Connection to playback service has been established, " + - "but controller has already been released"); - } - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - playbackService = null; - initialized = false; - Log.d(TAG, "Disconnected from Service"); - } - }; - - private final BroadcastReceiver statusUpdate = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.d(TAG, "Received statusUpdate Intent."); - if (playbackService != null) { - PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); - status = info.playerStatus; - media = info.playable; - handleStatus(); - } else { - Log.w(TAG, "Couldn't receive status update: playbackService was null"); - if (PlaybackService.isRunning) { - bindToService(); - } else { - status = PlayerStatus.STOPPED; - handleStatus(); - } - } - } - }; - - private final BroadcastReceiver notificationReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - int type = intent.getIntExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_TYPE, -1); - int code = intent.getIntExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_CODE, -1); - if (code == -1 || type == -1) { - Log.d(TAG, "Bad arguments. Won't handle intent"); - return; - } - switch (type) { - case PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD: - if (playbackService == null && PlaybackService.isRunning) { - bindToService(); - return; - } - mediaInfoLoaded = false; - queryService(); - break; - case PlaybackServiceInterface.NOTIFICATION_TYPE_PLAYBACK_END: - onPlaybackEnd(); - break; - } - } - - }; - - public void onPlaybackEnd() {} - - /** - * Is called whenever the PlaybackService changes its status. This method - * should be used to update the GUI or start/cancel background threads. - */ - private void handleStatus() { - Log.d(TAG, "status: " + status.toString()); - checkMediaInfoLoaded(); - switch (status) { - case PLAYING: - updatePlayButtonShowsPlay(false); - break; - case PREPARING: - if (playbackService != null) { - updatePlayButtonShowsPlay(!playbackService.isStartWhenPrepared()); - } - break; - case PAUSED: - case PREPARED: // Fall-through - case STOPPED: // Fall-through - case INITIALIZED: // Fall-through - updatePlayButtonShowsPlay(true); - break; - default: - break; - } - } - - private void checkMediaInfoLoaded() { - if (!mediaInfoLoaded || loadedFeedMedia != PlaybackPreferences.getCurrentlyPlayingFeedMediaId()) { - loadedFeedMedia = PlaybackPreferences.getCurrentlyPlayingFeedMediaId(); - loadMediaInfo(); - } - mediaInfoLoaded = true; - } - - protected void updatePlayButtonShowsPlay(boolean showPlay) { - - } - - public abstract void loadMediaInfo(); - - /** - * Called when connection to playback service has been established or - * information has to be refreshed - */ - private void queryService() { - Log.d(TAG, "Querying service info"); - if (playbackService != null) { - PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); - status = info.playerStatus; - media = info.playable; - - // make sure that new media is loaded if it's available - mediaInfoLoaded = false; - handleStatus(); - - } else { - Log.e(TAG, - "queryService() was called without an existing connection to playbackservice"); - } - } - - public void playPause() { - if (playbackService == null) { - new PlaybackServiceStarter(activity, media).start(); - Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!"); - return; - } - switch (status) { - case PLAYING: - playbackService.pause(true, false); - break; - case PAUSED: - case PREPARED: - playbackService.resume(); - break; - case PREPARING: - playbackService.setStartWhenPrepared(!playbackService.isStartWhenPrepared()); - break; - case INITIALIZED: - playbackService.setStartWhenPrepared(true); - playbackService.prepare(); - break; - default: - new PlaybackServiceStarter(activity, media) - .callEvenIfRunning(true) - .start(); - Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown"); - break; - } - } - - public int getPosition() { - if (playbackService != null) { - return playbackService.getCurrentPosition(); - } else if (getMedia() != null) { - return getMedia().getPosition(); - } else { - return Playable.INVALID_TIME; - } - } - - public int getDuration() { - if (playbackService != null) { - return playbackService.getDuration(); - } else if (getMedia() != null) { - return getMedia().getDuration(); - } else { - return Playable.INVALID_TIME; - } - } - - public Playable getMedia() { - if (media == null) { - media = DBReader.getFeedMedia(PlaybackPreferences.getCurrentlyPlayingFeedMediaId()); - } - return media; - } - - public boolean sleepTimerActive() { - return playbackService != null && playbackService.sleepTimerActive(); - } - - public void disableSleepTimer() { - if (playbackService != null) { - playbackService.disableSleepTimer(); - } - } - - public long getSleepTimerTimeLeft() { - if (playbackService != null) { - return playbackService.getSleepTimerTimeLeft(); - } else { - return Playable.INVALID_TIME; - } - } - - public void extendSleepTimer(long extendTime) { - long timeLeft = getSleepTimerTimeLeft(); - if (playbackService != null && timeLeft != Playable.INVALID_TIME) { - setSleepTimer(timeLeft + extendTime); - } - } - - public void setSleepTimer(long time) { - if (playbackService != null) { - playbackService.setSleepTimer(time); - } - } - - public void seekTo(int time) { - if (playbackService != null) { - playbackService.seekTo(time); - } else if (getMedia() instanceof FeedMedia) { - FeedMedia media = (FeedMedia) getMedia(); - media.setPosition(time); - DBWriter.setFeedItem(media.getItem()); - EventBus.getDefault().post(new PlaybackPositionEvent(time, getMedia().getDuration())); - } - } - - public void setVideoSurface(SurfaceHolder holder) { - if (playbackService != null) { - playbackService.setVideoSurface(holder); - } - } - - public PlayerStatus getStatus() { - return status; - } - - public void setPlaybackSpeed(float speed) { - if (playbackService != null) { - playbackService.setSpeed(speed); - } else { - EventBus.getDefault().post(new SpeedChangedEvent(speed)); - } - } - - public void setSkipSilence(boolean skipSilence) { - if (playbackService != null) { - playbackService.setSkipSilence(skipSilence); - } - } - - public float getCurrentPlaybackSpeedMultiplier() { - if (playbackService != null) { - return playbackService.getCurrentPlaybackSpeed(); - } else { - return PlaybackSpeedUtils.getCurrentPlaybackSpeed(getMedia()); - } - } - - public boolean getCurrentPlaybackSkipSilence() { - if (playbackService != null) { - return playbackService.getCurrentSkipSilence(); - } else { - return PlaybackSpeedUtils.getCurrentSkipSilencePreference(getMedia()) - == FeedPreferences.SkipSilence.AGGRESSIVE; - } - } - - public List<String> getAudioTracks() { - if (playbackService == null) { - return Collections.emptyList(); - } - return playbackService.getAudioTracks(); - } - - public int getSelectedAudioTrack() { - if (playbackService == null) { - return -1; - } - return playbackService.getSelectedAudioTrack(); - } - - public void setAudioTrack(int track) { - if (playbackService != null) { - playbackService.setAudioTrack(track); - } - } - - public boolean isPlayingVideoLocally() { - if (PlaybackService.isCasting()) { - return false; - } else if (playbackService != null) { - return PlaybackService.getCurrentMediaType() == MediaType.VIDEO; - } else { - return getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO; - } - } - - public Pair<Integer, Integer> getVideoSize() { - if (playbackService != null) { - return playbackService.getVideoSize(); - } else { - return null; - } - } - - public void notifyVideoSurfaceAbandoned() { - if (playbackService != null) { - playbackService.notifyVideoSurfaceAbandoned(); - } - } - - public boolean isStreaming() { - return playbackService != null && playbackService.isStreaming(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java deleted file mode 100644 index 3b20e3d25..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.content.Context; -import android.content.Intent; -import android.os.Parcelable; -import androidx.core.content.ContextCompat; - -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.service.playback.PlaybackServiceInterface; -import de.danoeh.antennapod.model.playback.Playable; - -public class PlaybackServiceStarter { - private final Context context; - private final Playable media; - private boolean shouldStreamThisTime = false; - private boolean callEvenIfRunning = false; - - public PlaybackServiceStarter(Context context, Playable media) { - this.context = context; - this.media = media; - } - - /** - * Default value: false - */ - public PlaybackServiceStarter callEvenIfRunning(boolean callEvenIfRunning) { - this.callEvenIfRunning = callEvenIfRunning; - return this; - } - - public PlaybackServiceStarter shouldStreamThisTime(boolean shouldStreamThisTime) { - this.shouldStreamThisTime = shouldStreamThisTime; - return this; - } - - public Intent getIntent() { - Intent launchIntent = new Intent(context, PlaybackService.class); - launchIntent.putExtra(PlaybackServiceInterface.EXTRA_PLAYABLE, (Parcelable) media); - launchIntent.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, shouldStreamThisTime); - return launchIntent; - } - - public void start() { - if (PlaybackService.isRunning && !callEvenIfRunning) { - return; - } - ContextCompat.startForegroundService(context, getIntent()); - } -} diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml index 8678b4413..8c4091390 100644 --- a/core/src/main/res/values/ids.xml +++ b/core/src/main/res/values/ids.xml @@ -14,8 +14,4 @@ <!-- View types --> <item name="view_type_episode_item" type="id"/> - - <!-- Notifications need unique IDs to update/cancel them --> - <item name="notification_playing" type="id"/> - <item name="notification_streaming_confirmation" type="id"/> -</resources>
\ No newline at end of file +</resources> diff --git a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java deleted file mode 100644 index 92c0e8e3d..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java +++ /dev/null @@ -1,227 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.playback.base.PlayerStatus; -import org.junit.Before; -import org.junit.Test; - -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class PlaybackVolumeUpdaterTest { - - private static final long FEED_ID = 42; - - private PlaybackServiceMediaPlayer mediaPlayer; - - @Before - public void setUp() { - mediaPlayer = mock(PlaybackServiceMediaPlayer.class); - } - - @Test - public void noChangeIfNoFeedMediaPlaying() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PAUSED); - - Playable noFeedMedia = mock(Playable.class); - when(mediaPlayer.getPlayable()).thenReturn(noFeedMedia); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.OFF); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void noChangeIfPlayerStatusIsError() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.ERROR); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.OFF); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void noChangeIfPlayerStatusIsIndeterminate() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.INDETERMINATE); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.OFF); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void noChangeIfPlayerStatusIsStopped() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.STOPPED); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.OFF); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void noChangeIfPlayableIsNoItemOfAffectedFeed() { - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PLAYING); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - when(feedMedia.getItem().getFeed().getId()).thenReturn(FEED_ID + 1); - - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.OFF); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void updatesPreferencesForLoadedFeedMediaIfPlayerStatusIsPaused() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PAUSED); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - FeedPreferences feedPreferences = feedMedia.getItem().getFeed().getPreferences(); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(feedPreferences, times(1)).setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void updatesPreferencesForLoadedFeedMediaIfPlayerStatusIsPrepared() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PREPARED); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - FeedPreferences feedPreferences = feedMedia.getItem().getFeed().getPreferences(); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(feedPreferences, times(1)).setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void updatesPreferencesForLoadedFeedMediaIfPlayerStatusIsInitializing() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.INITIALIZING); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - FeedPreferences feedPreferences = feedMedia.getItem().getFeed().getPreferences(); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(feedPreferences, times(1)).setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void updatesPreferencesForLoadedFeedMediaIfPlayerStatusIsPreparing() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PREPARING); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - FeedPreferences feedPreferences = feedMedia.getItem().getFeed().getPreferences(); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(feedPreferences, times(1)).setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void updatesPreferencesForLoadedFeedMediaIfPlayerStatusIsSeeking() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.SEEKING); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - FeedPreferences feedPreferences = feedMedia.getItem().getFeed().getPreferences(); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(feedPreferences, times(1)).setVolumeAdaptionSetting(VolumeAdaptionSetting.LIGHT_REDUCTION); - - verify(mediaPlayer, never()).pause(anyBoolean(), anyBoolean()); - verify(mediaPlayer, never()).resume(); - } - - @Test - public void updatesPreferencesAndForcesVolumeChangeForLoadedFeedMediaIfPlayerStatusIsPlaying() { - PlaybackVolumeUpdater playbackVolumeUpdater = new PlaybackVolumeUpdater(); - - when(mediaPlayer.getPlayerStatus()).thenReturn(PlayerStatus.PLAYING); - - FeedMedia feedMedia = mockFeedMedia(); - when(mediaPlayer.getPlayable()).thenReturn(feedMedia); - FeedPreferences feedPreferences = feedMedia.getItem().getFeed().getPreferences(); - - playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer, FEED_ID, VolumeAdaptionSetting.HEAVY_REDUCTION); - - verify(feedPreferences, times(1)).setVolumeAdaptionSetting(VolumeAdaptionSetting.HEAVY_REDUCTION); - - verify(mediaPlayer, times(1)).pause(false, false); - verify(mediaPlayer, times(1)).resume(); - } - - private FeedMedia mockFeedMedia() { - FeedMedia feedMedia = mock(FeedMedia.class); - FeedItem feedItem = mock(FeedItem.class); - Feed feed = mock(Feed.class); - FeedPreferences feedPreferences = mock(FeedPreferences.class); - - when(feedMedia.getItem()).thenReturn(feedItem); - when(feedItem.getFeed()).thenReturn(feed); - when(feed.getId()).thenReturn(FEED_ID); - when(feed.getPreferences()).thenReturn(feedPreferences); - return feedMedia; - } -} |