summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/build.gradle14
-rw-r--r--core/src/main/AndroidManifest.xml56
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java23
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java44
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/QuickSettingsTileService.java61
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java405
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java773
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java2001
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceInterface.java20
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java273
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceStateManager.java51
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java365
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java38
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java63
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java24
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/PlaybackStatus.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java35
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java480
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java49
-rw-r--r--core/src/main/res/values/ids.xml6
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java227
22 files changed, 2 insertions, 5030 deletions
diff --git a/core/build.gradle b/core/build.gradle
index f7d736968..75a585b08 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -23,32 +23,20 @@ android {
}
dependencies {
- implementation project(':event')
implementation project(':model')
implementation project(':net:common')
implementation project(':net:download:service-interface')
- implementation project(':net:sync:service-interface')
implementation project(':parser:feed')
implementation project(':parser:media')
- implementation project(':playback:base')
- implementation project(':playback:cast')
- implementation project(':storage:database')
implementation project(':storage:preferences')
- implementation project(':ui:app-start-intent')
+ implementation project(':storage:database')
implementation project(':ui:common')
- implementation project(':ui:episodes')
- implementation project(':ui:notifications')
- implementation project(':ui:widget')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "androidx.core:core:$coreVersion"
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation "androidx.fragment:fragment:$fragmentVersion"
- implementation "androidx.media:media:$mediaVersion"
- implementation "androidx.media3:media3-datasource-okhttp:$media3Version"
- implementation "androidx.media3:media3-exoplayer:$media3Version"
- implementation "androidx.media3:media3-ui:$media3Version"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "androidx.work:work-runtime:$workManagerVersion"
implementation "com.google.android.material:material:$googleMaterialVersion"
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;
- }
-}