summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java2
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java28
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java69
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java4
-rw-r--r--app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java7
-rw-r--r--app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java14
-rw-r--r--app/src/main/AndroidManifest.xml3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java1
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java3
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java1
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java2
-rw-r--r--app/src/main/res/menu/cast_enabled.xml10
-rw-r--r--app/src/main/res/xml/preferences_playback.xml6
-rw-r--r--app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java157
-rw-r--r--app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java21
-rw-r--r--app/src/play/java/de/danoeh/antennapod/dialog/CustomMRControllerDialog.java480
-rw-r--r--app/src/play/java/de/danoeh/antennapod/fragment/CustomMRControllerDialogFragment.java15
-rw-r--r--app/src/play/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java48
-rw-r--r--app/src/play/res/layout/media_router_controller.xml41
-rw-r--r--core/build.gradle5
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java7
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java54
-rw-r--r--core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java15
-rw-r--r--core/src/free/res/values/strings.xml4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java (renamed from core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java)2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java18
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java176
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java25
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java33
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java4
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java2
-rw-r--r--core/src/main/res/values-land/dimens.xml4
-rw-r--r--core/src/main/res/values/dimens.xml7
-rw-r--r--core/src/main/res/values/strings.xml19
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java12
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java64
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java120
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java11
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java1091
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java303
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java10
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java57
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java106
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java314
-rw-r--r--core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java28
-rw-r--r--core/src/play/res/values/strings.xml4
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java2
-rw-r--r--playback/README.md3
-rw-r--r--playback/base/README.md3
-rw-r--r--playback/base/build.gradle10
-rw-r--r--playback/base/src/main/AndroidManifest.xml1
-rw-r--r--playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java (renamed from core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java)34
-rw-r--r--playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java33
-rw-r--r--playback/base/src/main/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtils.java (renamed from core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java)2
-rw-r--r--playback/base/src/test/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtilTest.java (renamed from core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java)2
-rw-r--r--playback/cast/README.md3
-rw-r--r--playback/cast/build.gradle17
-rw-r--r--playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java (renamed from app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java)2
-rw-r--r--playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPsmp.java17
-rw-r--r--playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastStateListener.java15
-rw-r--r--playback/cast/src/main/AndroidManifest.xml1
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java35
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java26
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java (renamed from core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java)359
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastStateListener.java69
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastUtils.java181
-rw-r--r--playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java135
-rw-r--r--playback/cast/src/play/res/menu/cast_button.xml11
-rw-r--r--settings.gradle3
-rw-r--r--ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml5
78 files changed, 901 insertions, 3490 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 87dea29de..3dcf7fe1e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -116,6 +116,8 @@ dependencies {
implementation project(':net:sync:gpoddernet')
implementation project(':net:sync:model')
implementation project(':parser:feed')
+ implementation project(':playback:base')
+ implementation project(':playback:cast')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
index 9f7af3a16..2ab2361d7 100644
--- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java
@@ -11,6 +11,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.awaitility.Awaitility;
import org.hamcrest.Matcher;
import org.junit.After;
@@ -32,7 +33,6 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.IntentUtils;
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java b/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
index 2c164f131..4d57b9b43 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/CancelablePSMPCallback.java
@@ -1,9 +1,10 @@
package de.test.antennapod.service.playback;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
@@ -43,14 +44,6 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
}
@Override
- public boolean onMediaPlayerInfo(int code, int resourceId) {
- if (isCancelled) {
- return true;
- }
- return originalCallback.onMediaPlayerInfo(code, resourceId);
- }
-
- @Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
if (isCancelled) {
return;
@@ -82,6 +75,15 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
return originalCallback.getNextInQueue(currentMedia);
}
+ @Nullable
+ @Override
+ public Playable findMedia(@NonNull String url) {
+ if (isCancelled) {
+ return null;
+ }
+ return originalCallback.findMedia(url);
+ }
+
@Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
if (isCancelled) {
@@ -89,4 +91,12 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
}
originalCallback.onPlaybackEnded(mediaType, stopPlaying);
}
+
+ @Override
+ public void ensureMediaInfoLoaded(@NonNull Playable media) {
+ if (isCancelled) {
+ return;
+ }
+ originalCallback.ensureMediaInfoLoaded(media);
+ }
} \ No newline at end of file
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java b/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
index 090a94d6e..fb55c7ad0 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/DefaultPSMPCallback.java
@@ -1,54 +1,59 @@
package de.test.antennapod.service.playback;
import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
- @Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ @Override
+ public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
- }
+ }
- @Override
- public void shouldStop() {
+ @Override
+ public void shouldStop() {
- }
+ }
- @Override
- public void onMediaChanged(boolean reloadUI) {
+ @Override
+ public void onMediaChanged(boolean reloadUI) {
- }
+ }
- @Override
- public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
- return false;
- }
+ @Override
+ public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
- @Override
- public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
+ }
- }
+ @Override
+ public void onPlaybackStart(@NonNull Playable playable, int position) {
- @Override
- public void onPlaybackStart(@NonNull Playable playable, int position) {
+ }
- }
+ @Override
+ public void onPlaybackPause(Playable playable, int position) {
- @Override
- public void onPlaybackPause(Playable playable, int position) {
+ }
- }
+ @Override
+ public Playable getNextInQueue(Playable currentMedia) {
+ return null;
+ }
- @Override
- public Playable getNextInQueue(Playable currentMedia) {
- return null;
- }
+ @Nullable
+ @Override
+ public Playable findMedia(@NonNull String url) {
+ return null;
+ }
- @Override
- public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
+ @Override
+ public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
- }
- } \ No newline at end of file
+ }
+
+ @Override
+ public void ensureMediaInfoLoaded(@NonNull Playable media) {
+ }
+} \ No newline at end of file
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
index 87a5fa65c..32298200e 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
@@ -5,6 +5,8 @@ import android.content.Context;
import androidx.test.filters.MediumTest;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.test.antennapod.EspressoTestUtils;
import junit.framework.AssertionFailedError;
@@ -24,8 +26,6 @@ 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.core.service.playback.LocalPSMP;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.model.playback.Playable;
import de.test.antennapod.util.service.download.HTTPBin;
diff --git a/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java b/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java
deleted file mode 100644
index fb23dfa1a..000000000
--- a/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.danoeh.antennapod.config;
-
-import de.danoeh.antennapod.core.CastCallbacks;
-
-class CastCallbackImpl implements CastCallbacks {
-
-}
diff --git a/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java b/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
deleted file mode 100644
index e096f883f..000000000
--- a/app/src/free/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.danoeh.antennapod.preferences;
-
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
-
-/**
- * Implements functions from PreferenceController that are flavor dependent.
- */
-public class PreferenceControllerFlavorHelper {
-
- public static void setupFlavoredUI(PlaybackPreferencesFragment ui) {
- ui.findPreference(UserPreferences.PREF_CAST_ENABLED).setEnabled(false);
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 45c21ce6b..0f8242e63 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,6 +54,9 @@
<meta-data
android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/>
+ <meta-data
+ android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
+ android:value="de.danoeh.antennapod.playback.cast.CastOptionsProvider" />
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
index 94270339d..7dc760e76 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -38,6 +38,7 @@ import com.bumptech.glide.Glide;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
+import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
import org.greenrobot.eventbus.EventBus;
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
index f895f76bb..4ff2a5775 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
@@ -43,7 +43,6 @@ import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.Converter;
@@ -62,6 +61,8 @@ import de.danoeh.antennapod.dialog.SleepTimerDialog;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java
index a45eb5199..1f4f657b1 100644
--- a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java
+++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java
@@ -15,6 +15,5 @@ class ClientConfigurator {
ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME;
ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl();
ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl();
- ClientConfig.castCallbacks = new CastCallbackImpl();
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
index 77d450f70..95e2eb1aa 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/AudioPlayerFragment.java
@@ -32,6 +32,7 @@ import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
+import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -41,7 +42,6 @@ import java.text.NumberFormat;
import java.util.List;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.CastEnabledActivity;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.event.FavoritesEvent;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
index 0d7aadbd0..04ad6e2bd 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
@@ -17,6 +17,7 @@ import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -24,7 +25,6 @@ import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChaptersListAdapter;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.model.feed.Chapter;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
index d1ab44572..1e24d62f7 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
@@ -23,9 +23,9 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.view.PlayButton;
import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java
index 9a86a4b3c..7fa2ed4d1 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/PlaybackPreferencesFragment.java
@@ -16,7 +16,6 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
-import de.danoeh.antennapod.preferences.PreferenceControllerFlavorHelper;
import java.util.Map;
import org.greenrobot.eventbus.EventBus;
@@ -31,7 +30,6 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
addPreferencesFromResource(R.xml.preferences_playback);
setupPlaybackScreen();
- PreferenceControllerFlavorHelper.setupFlavoredUI(this);
buildSmartMarkAsPlayedPreference();
}
diff --git a/app/src/main/res/menu/cast_enabled.xml b/app/src/main/res/menu/cast_enabled.xml
deleted file mode 100644
index d6e85c311..000000000
--- a/app/src/main/res/menu/cast_enabled.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:custom="http://schemas.android.com/apk/res-auto">
-
- <item
- android:id="@+id/media_route_menu_item"
- android:title="@string/cast_media_route_menu_title"
- custom:actionProviderClass="de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider"
- custom:showAsAction="ifRoom"/>
-</menu> \ No newline at end of file
diff --git a/app/src/main/res/xml/preferences_playback.xml b/app/src/main/res/xml/preferences_playback.xml
index 59bdaedcb..add9e8d4c 100644
--- a/app/src/main/res/xml/preferences_playback.xml
+++ b/app/src/main/res/xml/preferences_playback.xml
@@ -127,11 +127,5 @@
android:title="@string/media_player"
android:summary="@string/pref_media_player_message"
android:entryValues="@array/media_player_values"/>
- <SwitchPreferenceCompat
- android:defaultValue="false"
- android:enabled="true"
- android:key="prefCast"
- android:summary="@string/pref_cast_message"
- android:title="@string/pref_cast_title"/>
</PreferenceCategory>
</PreferenceScreen>
diff --git a/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
deleted file mode 100644
index 753feb3e7..000000000
--- a/app/src/play/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package de.danoeh.antennapod.activity;
-
-import android.content.SharedPreferences;
-import android.media.AudioManager;
-import android.os.Bundle;
-import androidx.preference.PreferenceManager;
-import androidx.appcompat.app.AppCompatActivity;
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import com.google.android.gms.cast.ApplicationMetadata;
-
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.cast.CastButtonVisibilityManager;
-import de.danoeh.antennapod.core.cast.CastConsumer;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
-import de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.playback.PlaybackService;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Activity that allows for showing the MediaRouter button whenever there's a cast device in the
- * network.
- */
-public abstract class CastEnabledActivity extends AppCompatActivity
- implements SharedPreferences.OnSharedPreferenceChangeListener {
- public static final String TAG = "CastEnabledActivity";
-
- private CastConsumer castConsumer;
- private CastManager castManager;
- private final List<CastButtonVisibilityManager> castButtons = new ArrayList<>();
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (!CastManager.isInitialized()) {
- return;
- }
-
- PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
- .registerOnSharedPreferenceChangeListener(this);
-
- castConsumer = new DefaultCastConsumer() {
- @Override
- public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
- onCastConnectionChanged(true);
- }
-
- @Override
- public void onDisconnected() {
- onCastConnectionChanged(false);
- }
- };
- castManager = CastManager.getInstance();
- castManager.addCastConsumer(castConsumer);
- CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager(castManager);
- castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
- onCastConnectionChanged(castManager.isConnected());
- castButtons.add(castButtonVisibilityManager);
- }
-
- @Override
- protected void onDestroy() {
- if (!CastManager.isInitialized()) {
- super.onDestroy();
- return;
- }
- PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
- .unregisterOnSharedPreferenceChangeListener(this);
- castManager.removeCastConsumer(castConsumer);
- super.onDestroy();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- if (!CastManager.isInitialized()) {
- return;
- }
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.setResumed(true);
- }
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- if (!CastManager.isInitialized()) {
- return;
- }
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.setResumed(false);
- }
- }
-
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
- boolean newValue = UserPreferences.isCastEnabled();
- Log.d(TAG, "onSharedPreferenceChanged(), isCastEnabled set to " + newValue);
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.setPrefEnabled(newValue);
- }
- // PlaybackService has its own listener, so if it's active we don't have to take action here.
- if (!newValue && !PlaybackService.isRunning) {
- CastManager.getInstance().disconnect();
- }
- }
- }
-
- private void onCastConnectionChanged(boolean connected) {
- if (connected) {
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.onConnected();
- }
- setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
- } else {
- for (CastButtonVisibilityManager castButton : castButtons) {
- castButton.onDisconnected();
- }
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
- }
- }
-
- /**
- * Should be called by any activity or fragment for which the cast button should be shown.
- */
- public final void requestCastButton(Menu menu) {
- if (!CastManager.isInitialized()) {
- return;
- }
-
- MenuItem mediaRouteButton = menu.findItem(R.id.media_route_menu_item);
- if (mediaRouteButton == null) {
- getMenuInflater().inflate(R.menu.cast_enabled, menu);
- mediaRouteButton = menu.findItem(R.id.media_route_menu_item);
- }
-
- SwitchableMediaRouteActionProvider mediaRouteActionProvider =
- CastManager.getInstance().addMediaRouterButton(mediaRouteButton);
- CastButtonVisibilityManager castButtonVisibilityManager =
- new CastButtonVisibilityManager(CastManager.getInstance());
- castButtonVisibilityManager.setMenu(menu);
- castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
- castButtonVisibilityManager.mediaRouteActionProvider = mediaRouteActionProvider;
- castButtonVisibilityManager.setResumed(true);
- castButtonVisibilityManager.requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS);
- mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable());
- }
-}
diff --git a/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java b/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java
deleted file mode 100644
index 2a879c62d..000000000
--- a/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package de.danoeh.antennapod.config;
-
-import androidx.annotation.NonNull;
-import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
-import androidx.mediarouter.app.MediaRouteDialogFactory;
-
-import de.danoeh.antennapod.core.CastCallbacks;
-import de.danoeh.antennapod.fragment.CustomMRControllerDialogFragment;
-
-public class CastCallbackImpl implements CastCallbacks {
- @Override
- public MediaRouteDialogFactory getMediaRouterDialogFactory() {
- return new MediaRouteDialogFactory() {
- @NonNull
- @Override
- public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
- return new CustomMRControllerDialogFragment();
- }
- };
- }
-}
diff --git a/app/src/play/java/de/danoeh/antennapod/dialog/CustomMRControllerDialog.java b/app/src/play/java/de/danoeh/antennapod/dialog/CustomMRControllerDialog.java
deleted file mode 100644
index 6d8450a18..000000000
--- a/app/src/play/java/de/danoeh/antennapod/dialog/CustomMRControllerDialog.java
+++ /dev/null
@@ -1,480 +0,0 @@
-package de.danoeh.antennapod.dialog;
-
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.RemoteException;
-import androidx.annotation.NonNull;
-import android.support.v4.media.MediaDescriptionCompat;
-import android.support.v4.media.MediaMetadataCompat;
-import android.support.v4.media.session.MediaControllerCompat;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-import androidx.core.util.Pair;
-import androidx.core.view.MarginLayoutParamsCompat;
-import androidx.core.view.accessibility.AccessibilityEventCompat;
-import androidx.mediarouter.app.MediaRouteControllerDialog;
-import androidx.palette.graphics.Palette;
-import androidx.mediarouter.media.MediaRouter;
-import androidx.appcompat.widget.AppCompatImageView;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.FrameLayout;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.request.RequestOptions;
-import com.bumptech.glide.request.target.Target;
-
-import java.util.concurrent.ExecutionException;
-
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.glide.ApGlideSettings;
-import io.reactivex.Observable;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
-
-public class CustomMRControllerDialog extends MediaRouteControllerDialog {
- public static final String TAG = "CustomMRContrDialog";
-
- private MediaRouter mediaRouter;
- private MediaSessionCompat.Token token;
-
- private ImageView artView;
- private TextView titleView;
- private TextView subtitleView;
- private ImageButton playPauseButton;
- private LinearLayout rootView;
-
- private boolean viewsCreated = false;
-
- private Disposable fetchArtSubscription;
-
- private MediaControllerCompat mediaController;
- private MediaControllerCompat.Callback mediaControllerCallback;
-
- public CustomMRControllerDialog(Context context) {
- this(context, 0);
- }
-
- private CustomMRControllerDialog(Context context, int theme) {
- super(context, theme);
- mediaRouter = MediaRouter.getInstance(getContext());
- token = mediaRouter.getMediaSessionToken();
- try {
- if (token != null) {
- mediaController = new MediaControllerCompat(getContext(), token);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "Error creating media controller", e);
- }
-
- if (mediaController != null) {
- mediaControllerCallback = new MediaControllerCompat.Callback() {
- @Override
- public void onSessionDestroyed() {
- if (mediaController != null) {
- mediaController.unregisterCallback(mediaControllerCallback);
- mediaController = null;
- }
- }
-
- @Override
- public void onMetadataChanged(MediaMetadataCompat metadata) {
- updateViews();
- }
-
- @Override
- public void onPlaybackStateChanged(PlaybackStateCompat state) {
- updateState();
- }
- };
- mediaController.registerCallback(mediaControllerCallback);
- }
- }
-
- @Override
- public View onCreateMediaControlView(Bundle savedInstanceState) {
- boolean landscape = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
- if (landscape) {
- /*
- * When a horizontal LinearLayout measures itself, it first measures its children and
- * settles their widths on the first pass, and only then figures out its height, never
- * revisiting the widths measurements.
- * When one has a child view that imposes a certain aspect ratio (such as an ImageView),
- * then its width and height are related to each other, and so if one allows for a large
- * height, then it will request for itself a large width as well. However, on the first
- * child measurement, the LinearLayout imposes a very relaxed height bound, that the
- * child uses to tell the width it wants, a value which the LinearLayout will interpret
- * as final, even though the child will want to change it once a more restrictive height
- * bound is imposed later.
- *
- * Our solution is, given that the heights of the children do not depend on their widths
- * in this case, we first figure out the layout's height and only then perform the
- * usual sequence of measurements.
- *
- * Note: this solution does not take into account any vertical paddings nor children's
- * vertical margins in determining the height, as this View as well as its children are
- * defined in code and no paddings/margins that would influence these computations are
- * introduced.
- *
- * There were no resources online for this type of issue as far as I could gather.
- */
- rootView = new LinearLayout(getContext()) {
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // We'd like to find the overall height before adjusting the widths within the LinearLayout
- int maxHeight = Integer.MIN_VALUE;
- if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
- for (int i = 0; i < getChildCount(); i++) {
- int height = Integer.MIN_VALUE;
- View child = getChildAt(i);
- ViewGroup.LayoutParams lp = child.getLayoutParams();
- // we only measure children whose layout_height is not MATCH_PARENT
- if (lp.height >= 0) {
- height = lp.height;
- } else if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
- child.measure(widthMeasureSpec, heightMeasureSpec);
- height = child.getMeasuredHeight();
- }
- maxHeight = Math.max(maxHeight, height);
- }
- }
- if (maxHeight > 0) {
- super.onMeasure(widthMeasureSpec,
- MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY));
- } else {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
- };
- rootView.setOrientation(LinearLayout.HORIZONTAL);
- } else {
- rootView = new LinearLayout(getContext());
- rootView.setOrientation(LinearLayout.VERTICAL);
- }
- FrameLayout.LayoutParams rootParams = new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- rootParams.setMargins(0, 0, 0,
- getContext().getResources().getDimensionPixelSize(R.dimen.media_router_controller_bottom_margin));
- rootView.setLayoutParams(rootParams);
-
- // Start the session activity when a content item (album art, title or subtitle) is clicked.
- View.OnClickListener onClickListener = v -> {
- if (mediaController != null) {
- PendingIntent pi = mediaController.getSessionActivity();
- if (pi != null) {
- try {
- pi.send();
- dismiss();
- } catch (PendingIntent.CanceledException e) {
- Log.e(TAG, pi + " was not sent, it had been canceled.");
- }
- }
- }
- };
-
- LinearLayout.LayoutParams artParams;
- /*
- * On portrait orientation, we want to limit the artView's height to 9/16 of the available
- * width. Reason is that we need to choose the height wisely otherwise we risk the dialog
- * being much larger than the screen, and there doesn't seem to be a good way to know the
- * available height beforehand.
- *
- * On landscape orientation, we want to limit the artView's width to its available height.
- * Otherwise, horizontal images would take too much space and severely restrict the space
- * for episode title and play/pause button.
- *
- * Internal implementation of ImageView only uses the source image's aspect ratio, but we
- * want to impose our own and fallback to the source image's when it is more favorable.
- * Solutions were inspired, among other similar sources, on
- * http://stackoverflow.com/questions/18077325/scale-image-to-fill-imageview-width-and-keep-aspect-ratio
- */
- if (landscape) {
- artView = new AppCompatImageView(getContext()) {
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int desiredWidth = widthMeasureSpec;
- int desiredMeasureMode = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY ?
- MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
- if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
- Drawable drawable = getDrawable();
- if (drawable != null) {
- int intrHeight = drawable.getIntrinsicHeight();
- int intrWidth = drawable.getIntrinsicWidth();
- int originalHeight = MeasureSpec.getSize(heightMeasureSpec);
- if (intrHeight < intrWidth) {
- desiredWidth = MeasureSpec.makeMeasureSpec(
- originalHeight, desiredMeasureMode);
- } else {
- desiredWidth = MeasureSpec.makeMeasureSpec(
- Math.round((float) originalHeight * intrWidth / intrHeight),
- desiredMeasureMode);
- }
- }
- }
- super.onMeasure(desiredWidth, heightMeasureSpec);
- }
- };
- artParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.MATCH_PARENT);
- MarginLayoutParamsCompat.setMarginStart(artParams,
- getContext().getResources().getDimensionPixelSize(R.dimen.media_router_controller_playback_control_horizontal_spacing));
- } else {
- artView = new AppCompatImageView(getContext()) {
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int desiredHeight = heightMeasureSpec;
- if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
- Drawable drawable = getDrawable();
- if (drawable != null) {
- int originalWidth = MeasureSpec.getSize(widthMeasureSpec);
- int intrHeight = drawable.getIntrinsicHeight();
- int intrWidth = drawable.getIntrinsicWidth();
- float scale;
- if (intrHeight*16 > intrWidth*9) {
- // image is taller than 16:9
- scale = (float) originalWidth * 9 / 16 / intrHeight;
- } else {
- // image is more horizontal than 16:9
- scale = (float) originalWidth / intrWidth;
- }
- desiredHeight = MeasureSpec.makeMeasureSpec(
- Math.round(intrHeight * scale),
- MeasureSpec.EXACTLY);
- }
- }
- super.onMeasure(widthMeasureSpec, desiredHeight);
- }
- };
- artParams = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
- }
- // When we fetch the bitmap, we want to know if we should set a background color or not.
- artView.setTag(landscape);
-
- artView.setScaleType(ImageView.ScaleType.FIT_CENTER);
- artView.setOnClickListener(onClickListener);
-
- artView.setLayoutParams(artParams);
- rootView.addView(artView);
-
- ViewGroup wrapper = rootView;
-
- if (landscape) {
- // Here we wrap with a frame layout because we want to set different layout parameters
- // for landscape orientation.
- wrapper = new FrameLayout(getContext());
- wrapper.setLayoutParams(new LinearLayout.LayoutParams(
- 0,
- ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
- rootView.addView(wrapper);
- rootView.setWeightSum(1f);
- }
-
- View playbackControlLayout = View.inflate(getContext(), R.layout.media_router_controller, wrapper);
-
- titleView = playbackControlLayout.findViewById(R.id.mrc_control_title);
- subtitleView = playbackControlLayout.findViewById(R.id.mrc_control_subtitle);
- playbackControlLayout.findViewById(R.id.mrc_control_title_container).setOnClickListener(onClickListener);
- playPauseButton = playbackControlLayout.findViewById(R.id.mrc_control_play_pause);
- playPauseButton.setOnClickListener(v -> {
- PlaybackStateCompat state;
- if (mediaController != null && (state = mediaController.getPlaybackState()) != null) {
- boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_PLAYING;
- if (isPlaying) {
- mediaController.getTransportControls().pause();
- } else {
- mediaController.getTransportControls().play();
- }
- // Announce the action for accessibility.
- AccessibilityManager accessibilityManager = (AccessibilityManager)
- getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
- if (accessibilityManager != null && accessibilityManager.isEnabled()) {
- AccessibilityEvent event = AccessibilityEvent.obtain(
- AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
- event.setPackageName(getContext().getPackageName());
- event.setClassName(getClass().getName());
- int resId = isPlaying ? R.string.mr_controller_pause : R.string.mr_controller_play;
- event.getText().add(getContext().getString(resId));
- accessibilityManager.sendAccessibilityEvent(event);
- }
- }
- });
-
- viewsCreated = true;
- updateViews();
- return rootView;
- }
-
- @Override
- public void onDetachedFromWindow() {
- if (fetchArtSubscription != null) {
- fetchArtSubscription.dispose();
- fetchArtSubscription = null;
- }
- super.onDetachedFromWindow();
- }
-
- private void updateViews() {
- if (!viewsCreated || token == null || mediaController == null) {
- rootView.setVisibility(View.GONE);
- return;
- }
- MediaMetadataCompat metadata = mediaController.getMetadata();
- MediaDescriptionCompat description = metadata == null ? null : metadata.getDescription();
- if (description == null) {
- rootView.setVisibility(View.GONE);
- return;
- }
-
- PlaybackStateCompat state = mediaController.getPlaybackState();
- MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute();
-
- CharSequence title = description.getTitle();
- boolean hasTitle = !TextUtils.isEmpty(title);
- CharSequence subtitle = description.getSubtitle();
- boolean hasSubtitle = !TextUtils.isEmpty(subtitle);
-
- boolean showTitle = false;
- boolean showSubtitle = false;
- if (route.getPresentationDisplay() != null &&
- route.getPresentationDisplay().getDisplayId() != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) {
- // The user is currently casting screen.
- titleView.setText(R.string.mr_controller_casting_screen);
- showTitle = true;
- } else if (state == null || state.getState() == PlaybackStateCompat.STATE_NONE) {
- // Show "No media selected" as we don't yet know the playback state.
- // (Only exception is bluetooth where we don't show anything.)
- if (!route.isBluetooth()) {
- titleView.setText(R.string.mr_controller_no_media_selected);
- showTitle = true;
- }
- } else if (!hasTitle && !hasSubtitle) {
- titleView.setText(R.string.mr_controller_no_info_available);
- showTitle = true;
- } else {
- if (hasTitle) {
- titleView.setText(title);
- showTitle = true;
- }
- if (hasSubtitle) {
- subtitleView.setText(subtitle);
- showSubtitle = true;
- }
- }
- if (showSubtitle) {
- titleView.setSingleLine();
- } else {
- titleView.setMaxLines(2);
- }
- titleView.setVisibility(showTitle ? View.VISIBLE : View.GONE);
- subtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
-
- updateState();
-
- if(rootView.getVisibility() != View.VISIBLE) {
- artView.setVisibility(View.GONE);
- rootView.setVisibility(View.VISIBLE);
- }
-
- if (fetchArtSubscription != null) {
- fetchArtSubscription.dispose();
- }
-
- fetchArtSubscription = Observable.fromCallable(() -> fetchArt(description))
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> {
- fetchArtSubscription = null;
- if (artView == null) {
- return;
- }
- if (result.first != null) {
- if (!((Boolean) artView.getTag())) {
- artView.setBackgroundColor(result.second);
- }
- artView.setImageBitmap(result.first);
- artView.setVisibility(View.VISIBLE);
- } else {
- artView.setVisibility(View.GONE);
- }
- }, error -> Log.e(TAG, Log.getStackTraceString(error)));
-
- }
-
- private void updateState() {
- PlaybackStateCompat state;
- if (!viewsCreated || mediaController == null ||
- (state = mediaController.getPlaybackState()) == null) {
- return;
- }
- boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_BUFFERING
- || state.getState() == PlaybackStateCompat.STATE_PLAYING;
- boolean supportsPlay = (state.getActions() & (PlaybackStateCompat.ACTION_PLAY
- | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0;
- boolean supportsPause = (state.getActions() & (PlaybackStateCompat.ACTION_PAUSE
- | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0;
- if (isPlaying && supportsPause) {
- playPauseButton.setVisibility(View.VISIBLE);
- playPauseButton.setImageResource(getThemeResource(getContext(), R.attr.mediaRoutePauseDrawable));
- playPauseButton.setContentDescription(getContext().getResources().getText(R.string.mr_controller_pause));
- } else if (!isPlaying && supportsPlay) {
- playPauseButton.setVisibility(View.VISIBLE);
- playPauseButton.setImageResource(getThemeResource(getContext(), R.attr.mediaRoutePlayDrawable));
- playPauseButton.setContentDescription(getContext().getResources().getText(R.string.mr_controller_play));
- } else {
- playPauseButton.setVisibility(View.GONE);
- }
- }
-
- private static int getThemeResource(Context context, int attr) {
- TypedValue value = new TypedValue();
- return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0;
- }
-
- @NonNull
- private Pair<Bitmap, Integer> fetchArt(@NonNull MediaDescriptionCompat description) {
- Bitmap iconBitmap = description.getIconBitmap();
- Uri iconUri = description.getIconUri();
- Bitmap art = null;
- if (iconBitmap != null) {
- art = iconBitmap;
- } else if (iconUri != null) {
- try {
- art = Glide.with(getContext().getApplicationContext())
- .asBitmap()
- .load(iconUri.toString())
- .apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
- .submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
- .get();
- } catch (InterruptedException | ExecutionException e) {
- Log.e(TAG, "Image art load failed", e);
- }
- }
- int backgroundColor = 0;
- if (art != null && art.getWidth()*9 < art.getHeight()*16) {
- // Portrait art requires dominant color as background color.
- Palette palette = new Palette.Builder(art).maximumColorCount(1).generate();
- backgroundColor = palette.getSwatches().isEmpty()
- ? 0 : palette.getSwatches().get(0).getRgb();
- }
- return new Pair<>(art, backgroundColor);
- }
-}
diff --git a/app/src/play/java/de/danoeh/antennapod/fragment/CustomMRControllerDialogFragment.java b/app/src/play/java/de/danoeh/antennapod/fragment/CustomMRControllerDialogFragment.java
deleted file mode 100644
index dad7b0bfd..000000000
--- a/app/src/play/java/de/danoeh/antennapod/fragment/CustomMRControllerDialogFragment.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.danoeh.antennapod.fragment;
-
-import android.content.Context;
-import android.os.Bundle;
-import androidx.mediarouter.app.MediaRouteControllerDialog;
-import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
-
-import de.danoeh.antennapod.dialog.CustomMRControllerDialog;
-
-public class CustomMRControllerDialogFragment extends MediaRouteControllerDialogFragment {
- @Override
- public MediaRouteControllerDialog onCreateControllerDialog(Context context, Bundle savedInstanceState) {
- return new CustomMRControllerDialog(context);
- }
-}
diff --git a/app/src/play/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java b/app/src/play/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
deleted file mode 100644
index b51fb40b0..000000000
--- a/app/src/play/java/de/danoeh/antennapod/preferences/PreferenceControllerFlavorHelper.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package de.danoeh.antennapod.preferences;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GoogleApiAvailability;
-
-import de.danoeh.antennapod.PodcastApp;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
-
-/**
- * Implements functions from PreferenceController that are flavor dependent.
- */
-public class PreferenceControllerFlavorHelper {
-
- public static void setupFlavoredUI(PlaybackPreferencesFragment ui) {
- //checks whether Google Play Services is installed on the device (condition necessary for Cast support)
- ui.findPreference(UserPreferences.PREF_CAST_ENABLED).setOnPreferenceChangeListener((preference, o) -> {
- if (o instanceof Boolean && ((Boolean) o)) {
- final int googlePlayServicesCheck = GoogleApiAvailability.getInstance()
- .isGooglePlayServicesAvailable(ui.getActivity());
- if (googlePlayServicesCheck == ConnectionResult.SUCCESS) {
- displayRestartRequiredDialog(ui.requireContext());
- return true;
- } else {
- GoogleApiAvailability.getInstance()
- .getErrorDialog(ui.getActivity(), googlePlayServicesCheck, 0)
- .show();
- return false;
- }
- }
- return true;
- });
- }
-
- private static void displayRestartRequiredDialog(@NonNull Context context) {
- AlertDialog.Builder dialog = new AlertDialog.Builder(context);
- dialog.setTitle(android.R.string.dialog_alert_title);
- dialog.setMessage(R.string.pref_restart_required);
- dialog.setPositiveButton(android.R.string.ok, (dialog1, which) -> PodcastApp.forceRestart());
- dialog.setCancelable(false);
- dialog.show();
- }
-}
diff --git a/app/src/play/res/layout/media_router_controller.xml b/app/src/play/res/layout/media_router_controller.xml
deleted file mode 100644
index bdb1b1cc2..000000000
--- a/app/src/play/res/layout/media_router_controller.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/mrc_playback_control"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="@dimen/media_router_controller_playback_control_vertical_padding"
- android:paddingBottom="@dimen/media_router_controller_playback_control_vertical_padding"
- android:paddingLeft="@dimen/media_router_controller_playback_control_start_padding"
- android:paddingStart="@dimen/media_router_controller_playback_control_start_padding"
- android:paddingRight="@dimen/media_router_controller_playback_control_horizontal_spacing"
- android:paddingEnd="@dimen/media_router_controller_playback_control_horizontal_spacing">
- <ImageButton android:id="@+id/mrc_control_play_pause"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="@dimen/media_router_controller_playback_control_horizontal_spacing"
- android:layout_marginStart="@dimen/media_router_controller_playback_control_horizontal_spacing"
- android:layout_alignParentRight="true"
- android:layout_alignParentEnd="true"
- android:contentDescription="@string/mr_controller_play"
- android:background="?android:attr/selectableItemBackground"/>
-
- <LinearLayout android:id="@+id/mrc_control_title_container"
- android:orientation="vertical"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_alignParentStart="true"
- android:layout_toLeftOf="@id/mrc_control_play_pause"
- android:layout_toStartOf="@id/mrc_control_play_pause"
- android:layout_centerVertical="true">
- <TextView android:id="@+id/mrc_control_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.MediaRouter.PrimaryText"/>
- <TextView android:id="@+id/mrc_control_subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText"
- android:singleLine="true" />
- </LinearLayout>
-</RelativeLayout>
diff --git a/core/build.gradle b/core/build.gradle
index ce76fec70..b3954c879 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -27,6 +27,8 @@ dependencies {
implementation project(':net:sync:model')
implementation project(':parser:feed')
implementation project(':parser:media')
+ implementation project(':playback:base')
+ implementation project(':playback:cast')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
implementation project(':ui:png-icons')
@@ -61,9 +63,6 @@ dependencies {
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
// Non-free dependencies:
- playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1'
- playApi 'androidx.mediarouter:mediarouter:1.0.0'
- playApi "com.google.android.gms:play-services-cast:$playServicesVersion"
playApi "com.google.android.support:wearable:$wearableSupportVersion"
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"
diff --git a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java
deleted file mode 100644
index 2e266c736..000000000
--- a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.danoeh.antennapod.core;
-
-/**
- * Callbacks for Chromecast support on the core module
- */
-public interface CastCallbacks {
-}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
deleted file mode 100644
index 837cb1bd0..000000000
--- a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-import android.content.Context;
-import androidx.annotation.StringRes;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-
-/**
- * Class intended to work along PlaybackService and provide support for different flavors.
- */
-class PlaybackServiceFlavorHelper {
-
- private final PlaybackService.FlavorHelperCallback callback;
-
- PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) {
- this.callback = callback;
- }
-
- void initializeMediaPlayer(Context context) {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- }
-
- void removeCastConsumer() {
- // no-op
- }
-
- boolean castDisconnect(boolean castDisconnect) {
- return false;
- }
-
- boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) {
- return false;
- }
-
- void registerWifiBroadcastReceiver() {
- // no-op
- }
-
- void unregisterWifiBroadcastReceiver() {
- // no-op
- }
-
- boolean onSharedPreference(String key) {
- return false;
- }
-
- void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) {
- // no-op
- }
-
- void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
- // no-op
- }
-}
diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
new file mode 100644
index 000000000..373b24bc8
--- /dev/null
+++ b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
@@ -0,0 +1,15 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+
+class WearMediaSession {
+ static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName,
+ CharSequence name, int icon) {
+ // no-op
+ }
+
+ static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
+ // no-op
+ }
+}
diff --git a/core/src/free/res/values/strings.xml b/core/src/free/res/values/strings.xml
deleted file mode 100644
index fb49bbbe7..000000000
--- a/core/src/free/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="pref_cast_message" translatable="false">@string/pref_cast_message_free_flavor</string>
-</resources>
diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
index 755bec14e..ac67fb042 100644
--- a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -30,8 +30,6 @@ public class ClientConfig {
public static DownloadServiceCallbacks downloadServiceCallbacks;
- public static CastCallbacks castCallbacks;
-
private static boolean initialized = false;
public static synchronized void initialize(Context context) {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
index 8d80ef32b..f0c61403f 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java
@@ -8,8 +8,8 @@ import android.util.Log;
import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus;
import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
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
index 79363e872..7ce06a9fb 100644
--- 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
@@ -40,6 +40,7 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.service.download.HttpDownloader;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.playback.IPlayer;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
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
index 5648024de..34fc7d699 100644
--- 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
@@ -17,8 +17,9 @@ 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.core.storage.DBReader;
import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.antennapod.audio.MediaPlayer;
import java.io.File;
@@ -39,7 +40,7 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
+import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils;
import de.danoeh.antennapod.core.util.playback.AudioPlayer;
import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.model.playback.Playable;
@@ -148,7 +149,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
}
public LocalPSMP(@NonNull Context context,
- @NonNull PSMPCallback callback) {
+ @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) {
super(context, callback);
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.playerLock = new PlayerLock();
@@ -265,9 +266,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
LocalPSMP.this.startWhenPrepared.set(startWhenPrepared);
setPlayerStatus(PlayerStatus.INITIALIZING, media);
try {
- if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
- ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
- }
+ callback.ensureMediaInfoLoaded(media);
callback.onMediaChanged(false);
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence());
if (stream) {
@@ -1098,7 +1097,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
EventBus.getDefault().post(BufferUpdateEvent.ended());
return true;
default:
- return callback.onMediaPlayerInfo(what, 0);
+ return true;
}
}
@@ -1148,4 +1147,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
executor.submit(r);
}
}
+
+ @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
index 60ccb5c9e..949c0ff9d 100644
--- 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
@@ -37,6 +37,7 @@ 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;
@@ -47,6 +48,10 @@ import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
+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 org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@@ -103,24 +108,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
*/
private static final String TAG = "PlaybackService";
- /**
- * Parcelable of type Playable.
- */
public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
- /**
- * True if cast session should disconnect.
- */
- public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect";
- /**
- * True if media should be streamed.
- */
public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream";
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";
- /**
- * True if playback should be started immediately after media has been
- * prepared.
- */
public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared";
public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately";
@@ -200,10 +191,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
private PlaybackServiceMediaPlayer mediaPlayer;
private PlaybackServiceTaskManager taskManager;
- private PlaybackServiceFlavorHelper flavorHelper;
private PlaybackServiceStateManager stateManager;
private Disposable positionEventTimer;
private PlaybackServiceNotificationBuilder notificationBuilder;
+ private CastStateListener castStateListener;
private String autoSkippedFeedMediaId = null;
@@ -280,7 +271,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
EventBus.getDefault().register(this);
taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
- flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback);
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(prefListener);
@@ -305,12 +295,36 @@ public class PlaybackService extends MediaBrowserServiceCompat {
npe.printStackTrace();
}
- flavorHelper.initializeMediaPlayer(PlaybackService.this);
+ recreateMediaPlayer();
mediaSession.setActive(true);
-
+ castStateListener = new CastStateListener(this) {
+ @Override
+ public void onSessionStartedOrEnded() {
+ recreateMediaPlayer();
+ }
+ };
EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED));
}
+ 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();
@@ -324,6 +338,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopForeground(!UserPreferences.isPersistNotify());
isRunning = false;
currentMediaType = MediaType.UNKNOWN;
+ castStateListener.destroy();
cancelPositionObserver();
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener);
@@ -337,8 +352,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
unregisterReceiver(audioBecomingNoisy);
unregisterReceiver(skipCurrentEpisodeReceiver);
unregisterReceiver(pausePlayCurrentEpisodeReceiver);
- flavorHelper.removeCastConsumer();
- flavorHelper.unregisterWifiBroadcastReceiver();
mediaPlayer.shutdown();
taskManager.shutdown();
}
@@ -483,9 +496,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false);
- final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
- if (keycode == -1 && playable == null && !castDisconnect) {
+ if (keycode == -1 && playable == null) {
Log.e(TAG, "PlaybackService was started with no arguments");
stateManager.stopService();
return Service.START_NOT_STICKY;
@@ -509,7 +521,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopService();
return Service.START_NOT_STICKY;
}
- } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) {
+ } else {
stateManager.validStartCommandWasReceived();
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true);
boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false);
@@ -553,9 +565,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopService();
});
return Service.START_NOT_STICKY;
- } else {
- Log.d(TAG, "Did not handle intent to PlaybackService: " + intent);
- Log.d(TAG, "Extras: " + intent.getExtras());
}
}
@@ -781,8 +790,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
}
-
-
@Override
public WidgetUpdater.WidgetState requestWidgetState() {
return new WidgetUpdater.WidgetState(getPlayable(), getStatus(),
@@ -873,11 +880,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
}
@Override
- public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
- return flavorHelper.onMediaPlayerInfo(PlaybackService.this, code, resourceId);
- }
-
- @Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped,
boolean playingNext) {
PlaybackService.this.onPostPlayback(media, ended, skipped, playingNext);
@@ -916,10 +918,24 @@ public class PlaybackService extends MediaBrowserServiceCompat {
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)
@@ -1248,15 +1264,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
// This would give the PIP of videos a play button
capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY;
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) {
- flavorHelper.sessionStateAddActionForWear(sessionState,
+ WearMediaSession.sessionStateAddActionForWear(sessionState,
CUSTOM_ACTION_REWIND,
getString(R.string.rewind_label),
android.R.drawable.ic_media_rew);
- flavorHelper.sessionStateAddActionForWear(sessionState,
+ WearMediaSession.sessionStateAddActionForWear(sessionState,
CUSTOM_ACTION_FAST_FORWARD,
getString(R.string.fast_forward_label),
android.R.drawable.ic_media_ff);
- flavorHelper.mediaSessionSetExtraForWear(mediaSession);
+ WearMediaSession.mediaSessionSetExtraForWear(mediaSession);
}
}
@@ -1338,7 +1354,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
notificationBuilder.setPlayable(playable);
notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken());
notificationBuilder.setPlayerStatus(playerStatus);
- notificationBuilder.setCasting(isCasting);
notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed());
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
@@ -1901,93 +1916,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
(sharedPreferences, key) -> {
if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) {
updateNotificationAndMediaSession(getPlayable());
- } else {
- flavorHelper.onSharedPreference(key);
}
};
-
- interface FlavorHelperCallback {
- PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback();
-
- void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer);
-
- PlaybackServiceMediaPlayer getMediaPlayer();
-
- void setIsCasting(boolean isCasting);
-
- void sendNotificationBroadcast(int type, int code);
-
- void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position);
-
- void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info);
-
- MediaSessionCompat getMediaSession();
-
- Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter);
-
- void unregisterReceiver(BroadcastReceiver receiver);
- }
-
- private final FlavorHelperCallback flavorHelperCallback = new FlavorHelperCallback() {
- @Override
- public PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback() {
- return PlaybackService.this.mediaPlayerCallback;
- }
-
- @Override
- public void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer) {
- PlaybackService.this.mediaPlayer = mediaPlayer;
- }
-
- @Override
- public PlaybackServiceMediaPlayer getMediaPlayer() {
- return PlaybackService.this.mediaPlayer;
- }
-
- @Override
- public void setIsCasting(boolean isCasting) {
- PlaybackService.isCasting = isCasting;
- stateManager.validStartCommandWasReceived();
- }
-
- @Override
- public void sendNotificationBroadcast(int type, int code) {
- PlaybackService.this.sendNotificationBroadcast(type, code);
- }
-
- @Override
- public void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) {
- PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position);
- }
-
- @Override
- public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) {
- if (connected) {
- PlaybackService.this.updateNotificationAndMediaSession(info.playable);
- } else {
- PlayerStatus status = info.playerStatus;
- if (status == PlayerStatus.PLAYING || status == PlayerStatus.SEEKING
- || status == PlayerStatus.PREPARING || UserPreferences.isPersistNotify()) {
- PlaybackService.this.updateNotificationAndMediaSession(info.playable);
- } else if (!UserPreferences.isPersistNotify()) {
- stateManager.stopForeground(true);
- }
- }
- }
-
- @Override
- public MediaSessionCompat getMediaSession() {
- return PlaybackService.this.mediaSession;
- }
-
- @Override
- public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
- return PlaybackService.this.registerReceiver(receiver, filter);
- }
-
- @Override
- public void unregisterReceiver(BroadcastReceiver receiver) {
- PlaybackService.this.unregisterReceiver(receiver);
- }
- };
}
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
index 5aee8c24c..c348f5773 100644
--- 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
@@ -31,17 +31,17 @@ import de.danoeh.antennapod.model.playback.Playable;
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 Context context;
+ private final Context context;
private Playable playable;
private MediaSessionCompat.Token mediaSessionToken;
private PlayerStatus playerStatus;
- private boolean isCasting;
private Bitmap icon;
private String position;
@@ -140,7 +140,7 @@ public class PlaybackServiceNotificationBuilder {
if (playable != null) {
notification.setContentTitle(playable.getFeedTitle());
notification.setContentText(playable.getEpisodeTitle());
- addActions(notification, mediaSessionToken, playerStatus, isCasting);
+ addActions(notification, mediaSessionToken, playerStatus);
if (icon != null) {
notification.setLargeIcon(icon);
@@ -175,23 +175,10 @@ public class PlaybackServiceNotificationBuilder {
}
private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken,
- PlayerStatus playerStatus, boolean isCasting) {
+ PlayerStatus playerStatus) {
ArrayList<Integer> compactActionList = new ArrayList<>();
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
-
- if (isCasting) {
- Intent stopCastingIntent = new Intent(context, PlaybackService.class);
- stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true);
- PendingIntent stopCastingPendingIntent = PendingIntent.getService(context,
- numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT
- | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
- notification.addAction(R.drawable.ic_notification_cast_off,
- context.getString(R.string.cast_disconnect_label),
- stopCastingPendingIntent);
- numActions++;
- }
-
// always let them rewind
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
@@ -270,10 +257,6 @@ public class PlaybackServiceNotificationBuilder {
this.playerStatus = playerStatus;
}
- public void setCasting(boolean casting) {
- isCasting = casting;
- }
-
public PlayerStatus getPlayerStatus() {
return playerStatus;
}
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
index edb8bc3a9..43837a473 100644
--- 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
@@ -4,6 +4,8 @@ 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 {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
deleted file mode 100644
index 4f2ae34f8..000000000
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-public enum PlayerStatus {
- INDETERMINATE(0), // player is currently changing its state, listeners should wait until the player has left this state.
- ERROR(-1),
- PREPARING(19),
- PAUSED(30),
- PLAYING(40),
- STOPPED(5),
- PREPARED(20),
- SEEKING(29),
- INITIALIZING(9), // playback service is loading the Playable's metadata
- INITIALIZED(10); // playback service was started, data source of media player was set.
-
- private final int statusValue;
- private static final PlayerStatus[] fromOrdinalLookup;
-
- static {
- fromOrdinalLookup = PlayerStatus.values();
- }
-
- PlayerStatus(int val) {
- statusValue = val;
- }
-
- public static PlayerStatus fromOrdinal(int o) {
- return fromOrdinalLookup[o];
- }
-
- public boolean isAtLeast(PlayerStatus other) {
- return other == null || this.statusValue>=other.statusValue;
- }
-}
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
index b436d80b2..549171c76 100644
--- 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
@@ -22,9 +22,9 @@ import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
-import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
index 5275e7080..2762fb9fe 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java
@@ -25,11 +25,11 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.receiver.PlayerWidget;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
index b14fb3b0b..325c508c5 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java
@@ -6,9 +6,9 @@ import androidx.annotation.NonNull;
import androidx.core.app.SafeJobIntentService;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlayableUtils;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
public class WidgetUpdaterJobService extends SafeJobIntentService {
private static final int JOB_ID = -17001;
diff --git a/core/src/main/res/values-land/dimens.xml b/core/src/main/res/values-land/dimens.xml
deleted file mode 100644
index 73b2b2e98..000000000
--- a/core/src/main/res/values-land/dimens.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <dimen name="media_router_controller_playback_control_start_padding">@dimen/media_router_controller_playback_control_horizontal_spacing</dimen>
-</resources>
diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml
index d1e200d1d..4b2247492 100644
--- a/core/src/main/res/values/dimens.xml
+++ b/core/src/main/res/values/dimens.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
-
<dimen name="widget_margin">0dp</dimen>
<dimen name="external_player_height">64dp</dimen>
<dimen name="text_size_micro">12sp</dimen>
@@ -28,11 +27,5 @@
<dimen name="audioplayer_playercontrols_length_big">64dp</dimen>
<dimen name="audioplayer_playercontrols_margin">12dp</dimen>
- <dimen name="media_router_controller_playback_control_vertical_padding">16dp</dimen>
- <dimen name="media_router_controller_playback_control_horizontal_spacing">12dp</dimen>
- <dimen name="media_router_controller_playback_control_start_padding">24dp</dimen>
- <dimen name="media_router_controller_bottom_margin">8dp</dimen>
-
<dimen name="nav_drawer_max_screen_size">480dp</dimen>
-
</resources>
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 1ab5b2184..59b335bc8 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -502,9 +502,6 @@
<string name="pref_proxy_title">Proxy</string>
<string name="pref_proxy_sum">Set a network proxy</string>
<string name="pref_no_browser_found">No web browser found.</string>
- <string name="pref_cast_title">Chromecast support</string>
- <string name="pref_cast_message_play_flavor">Enable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV)</string>
- <string name="pref_cast_message_free_flavor" tools:ignore="UnusedResources">Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod</string>
<string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string>
<string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string>
<string name="media_player_builtin">Built-in Android player (deprecated) </string>
@@ -664,7 +661,6 @@
<string name="pref_pausePlaybackForFocusLoss_title">Pause for Interruptions</string>
<string name="pref_resumeAfterCall_sum">Resume playback after a phone call completes</string>
<string name="pref_resumeAfterCall_title">Resume after Call</string>
- <string name="pref_restart_required">AntennaPod has to be restarted for this change to take effect.</string>
<!-- Online feed view -->
<string name="subscribe_label">Subscribe</string>
@@ -808,21 +804,6 @@
<!-- Subscriptions fragment -->
<string name="subscription_num_columns">Number of columns</string>
- <!-- Casting -->
- <string name="cast_media_route_menu_title">Play on&#8230;</string>
- <string name="cast_disconnect_label">Disconnect the cast session</string>
- <string name="cast_not_castable">Media selected is not compatible with cast device</string>
- <string name="cast_failed_to_play">Failed to start the playback of media</string>
- <string name="cast_failed_to_stop">Failed to stop the playback of media</string>
- <string name="cast_failed_to_pause">Failed to pause the playback of media</string>
- <string name="cast_failed_setting_volume">Failed to set the volume</string>
- <string name="cast_failed_no_connection">No connection to the cast device is present</string>
- <string name="cast_failed_no_connection_trans">Connection to the cast device has been lost. Application is trying to re-establish the connection, if possible. Please wait for a few seconds and try again.</string>
- <string name="cast_failed_status_request">Failed to sync up with the cast device</string>
- <string name="cast_failed_seek">Failed to seek to the new position on the cast device</string>
- <string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string>
- <string name="cast_failed_media_error_skipping">Error playing media. Skipping&#8230;</string>
-
<!-- Notification channels -->
<string name="notification_group_errors">Errors</string>
<string name="notification_group_news">News</string>
diff --git a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java
deleted file mode 100644
index 27f985a4c..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package de.danoeh.antennapod.core;
-
-import androidx.annotation.Nullable;
-import androidx.mediarouter.app.MediaRouteDialogFactory;
-
-/**
- * Callbacks for Chromecast support on the core module
- */
-public interface CastCallbacks {
-
- @Nullable MediaRouteDialogFactory getMediaRouterDialogFactory();
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
deleted file mode 100644
index 48de7c6e1..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package de.danoeh.antennapod.core;
-
-import android.content.Context;
-import android.util.Log;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
-import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
-import de.danoeh.antennapod.core.preferences.UsageStatistics;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
-import de.danoeh.antennapod.core.storage.PodDBAdapter;
-import de.danoeh.antennapod.core.util.NetworkUtils;
-import de.danoeh.antennapod.core.util.gui.NotificationUtils;
-import de.danoeh.antennapod.net.ssl.SslProviderInstaller;
-
-import java.io.File;
-
-/**
- * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables.
- * Apps using the core module of AntennaPod should register implementations of all interfaces here.
- */
-public class ClientConfig {
- private static final String TAG = "ClientConfig";
-
- private ClientConfig(){}
-
- /**
- * Should be used when setting User-Agent header for HTTP-requests.
- */
- public static String USER_AGENT;
-
- public static ApplicationCallbacks applicationCallbacks;
-
- public static DownloadServiceCallbacks downloadServiceCallbacks;
-
- public static CastCallbacks castCallbacks;
-
- private static boolean initialized = false;
-
- public static synchronized void initialize(Context context) {
- if (initialized) {
- return;
- }
- PodDBAdapter.init(context);
- UserPreferences.init(context);
- UsageStatistics.init(context);
- PlaybackPreferences.init(context);
- SslProviderInstaller.install(context);
- NetworkUtils.init(context);
- // Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary
- // Google Play Service usage.
- // Down side: when the user decides to enable casting, AntennaPod needs to be restarted
- // for it to take effect.
- if (UserPreferences.isCastEnabled()) {
- CastManager.init(context);
- } else {
- Log.v(TAG, "Cast is disabled. All Cast-related initialization will be skipped.");
- }
- AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp"));
- SleepTimerPreferences.init(context);
- NotificationUtils.createChannels(context);
- initialized = true;
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java
deleted file mode 100644
index 8d0e40116..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.util.Log;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import de.danoeh.antennapod.core.R;
-
-public class CastButtonVisibilityManager {
- private static final String TAG = "CastBtnVisibilityMgr";
- private final CastManager castManager;
- private volatile boolean prefEnabled = false;
- private volatile boolean viewRequested = false;
- private volatile boolean resumed = false;
- private volatile boolean connected = false;
- private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
- private Menu menu;
- public SwitchableMediaRouteActionProvider mediaRouteActionProvider;
-
- public CastButtonVisibilityManager(CastManager castManager) {
- this.castManager = castManager;
- }
-
- public synchronized void setPrefEnabled(boolean newValue) {
- if (prefEnabled != newValue && resumed && (viewRequested || connected)) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- prefEnabled = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized void setResumed(boolean newValue) {
- if (resumed == newValue) {
- Log.e(TAG, "resumed should never change to the same value");
- return;
- }
- resumed = newValue;
- if (prefEnabled && (viewRequested || connected)) {
- if (resumed) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- }
-
- public synchronized void setViewRequested(boolean newValue) {
- if (viewRequested != newValue && resumed && prefEnabled && !connected) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- viewRequested = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized void setConnected(boolean newValue) {
- if (connected != newValue && resumed && prefEnabled && !prefEnabled) {
- if (newValue) {
- castManager.incrementUiCounter();
- } else {
- castManager.decrementUiCounter();
- }
- }
- connected = newValue;
- if (mediaRouteActionProvider != null) {
- mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected));
- }
- }
-
- public synchronized boolean shouldEnable() {
- return prefEnabled && viewRequested;
- }
-
- public void setMenu(Menu menu) {
- setViewRequested(false);
- showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM;
- this.menu = menu;
- setShowAsAction();
- }
-
- public void requestCastButton(int showAsAction) {
- setViewRequested(true);
- this.showAsAction = showAsAction;
- setShowAsAction();
- }
-
- public void onConnected() {
- setConnected(true);
- setShowAsAction();
- }
-
- public void onDisconnected() {
- setConnected(false);
- setShowAsAction();
- }
-
- private void setShowAsAction() {
- if (menu == null) {
- Log.d(TAG, "setShowAsAction() without a menu");
- return;
- }
- MenuItem item = menu.findItem(R.id.media_route_menu_item);
- if (item == null) {
- Log.e(TAG, "setShowAsAction(), but cast button not inflated");
- return;
- }
- item.setShowAsAction(connected ? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction);
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java
deleted file mode 100644
index 213dd1875..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumer;
-
-public interface CastConsumer extends VideoCastConsumer{
-
- /**
- * Called when the stream's volume is changed.
- */
- void onStreamVolumeChanged(double value, boolean isMute);
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java
deleted file mode 100644
index dd07b9cd8..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java
+++ /dev/null
@@ -1,1091 +0,0 @@
-/*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- * ------------------------------------------------------------------------
- *
- * Changes made by Domingos Lopes <domingos86lopes@gmail.com>
- *
- * original can be found at http://www.github.com/googlecast/CastCompanionLibrary-android
- */
-
-package de.danoeh.antennapod.core.cast;
-
-import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.core.view.ActionProvider;
-import androidx.core.view.MenuItemCompat;
-import androidx.mediarouter.media.MediaRouter;
-import android.util.Log;
-import android.view.MenuItem;
-
-import com.google.android.gms.cast.ApplicationMetadata;
-import com.google.android.gms.cast.Cast;
-import com.google.android.gms.cast.CastDevice;
-import com.google.android.gms.cast.CastMediaControlIntent;
-import com.google.android.gms.cast.CastStatusCodes;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.cast.MediaQueueItem;
-import com.google.android.gms.cast.MediaStatus;
-import com.google.android.gms.cast.RemoteMediaPlayer;
-import com.google.android.gms.common.ConnectionResult;
-import com.google.android.gms.common.GoogleApiAvailability;
-import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager;
-import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.OnFailedListener;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
-
-import org.json.JSONObject;
-
-import java.io.IOException;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-
-import de.danoeh.antennapod.core.ClientConfig;
-import de.danoeh.antennapod.core.R;
-
-import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_PLAY;
-import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_UNCHANGED;
-
-/**
- * A subclass of {@link BaseCastManager} that is suitable for casting video contents (it
- * also provides a single custom data channel/namespace if an out-of-band communication is
- * needed).
- * <p>
- * Clients need to initialize this class by calling
- * {@link #init(android.content.Context)} in the Application's
- * {@code onCreate()} method. To access the (singleton) instance of this class, clients
- * need to call {@link #getInstance()}.
- * <p>This
- * class manages various states of the remote cast device. Client applications, however, can
- * complement the default behavior of this class by hooking into various callbacks that it provides
- * (see {@link CastConsumer}).
- * Since the number of these callbacks is usually much larger than what a single application might
- * be interested in, there is a no-op implementation of this interface (see
- * {@link DefaultCastConsumer}) that applications can subclass to override only those methods that
- * they are interested in. Since this library depends on the cast functionalities provided by the
- * Google Play services, the library checks to ensure that the right version of that service is
- * installed. It also provides a simple static method {@code checkGooglePlayServices()} that clients
- * can call at an early stage of their applications to provide a dialog for users if they need to
- * update/activate their Google Play Services library.
- *
- * @see CastConfiguration
- */
-public class CastManager extends BaseCastManager implements OnFailedListener {
- public static final String TAG = "CastManager";
-
- public static final String CAST_APP_ID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
-
- private MediaStatus mediaStatus;
- private static CastManager INSTANCE;
- private RemoteMediaPlayer remoteMediaPlayer;
- private int state = MediaStatus.PLAYER_STATE_IDLE;
- private final Set<CastConsumer> castConsumers = new CopyOnWriteArraySet<>();
-
- public static final int QUEUE_OPERATION_LOAD = 1;
- public static final int QUEUE_OPERATION_APPEND = 9;
-
- private CastManager(Context context, CastConfiguration castConfiguration) {
- super(context, castConfiguration);
- Log.d(TAG, "CastManager is instantiated");
- }
-
- public static synchronized CastManager init(Context context) {
- if (INSTANCE == null) {
- CastConfiguration castConfiguration = new CastConfiguration.Builder(CAST_APP_ID)
- .enableDebug()
- .enableAutoReconnect()
- .enableWifiReconnection()
- .setLaunchOptions(true, Locale.getDefault())
- .setMediaRouteDialogFactory(ClientConfig.castCallbacks.getMediaRouterDialogFactory())
- .build();
- Log.d(TAG, "New instance of CastManager is created");
- if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance()
- .isGooglePlayServicesAvailable(context)) {
- Log.e(TAG, "Couldn't find the appropriate version of Google Play Services");
- }
- INSTANCE = new CastManager(context, castConfiguration);
- }
- return INSTANCE;
- }
-
- /**
- * Returns a (singleton) instance of this class. Clients should call this method in order to
- * get a hold of this singleton instance, only after it is initialized. If it is not initialized
- * yet, an {@link IllegalStateException} will be thrown.
- *
- */
- public static CastManager getInstance() {
- if (INSTANCE == null) {
- String msg = "No CastManager instance was found, did you forget to initialize it?";
- Log.e(TAG, msg);
- throw new IllegalStateException(msg);
- }
- return INSTANCE;
- }
-
- public static boolean isInitialized() {
- return INSTANCE != null;
- }
-
- /**
- * Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media
- * control APIs that this library do not provide a wrapper for, client applications can call
- * those methods directly after obtaining an instance of the active {@link RemoteMediaPlayer}.
- */
- public final RemoteMediaPlayer getRemoteMediaPlayer() {
- return remoteMediaPlayer;
- }
-
- /*
- * A simple check to make sure remoteMediaPlayer is not null
- */
- private void checkRemoteMediaPlayerAvailable() throws NoConnectionException {
- if (remoteMediaPlayer == null) {
- throw new NoConnectionException();
- }
- }
-
- /**
- * Indicates if the remote media is currently playing (or buffering).
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaPlaying() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return state == MediaStatus.PLAYER_STATE_BUFFERING
- || state == MediaStatus.PLAYER_STATE_PLAYING;
- }
-
- /**
- * Returns <code>true</code> if the remote connected device is playing a movie.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaPaused() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return state == MediaStatus.PLAYER_STATE_PAUSED;
- }
-
- /**
- * Returns <code>true</code> only if there is a media on the remote being played, paused or
- * buffered.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isRemoteMediaLoaded() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- return isRemoteMediaPaused() || isRemoteMediaPlaying();
- }
-
- /**
- * Gets the remote's system volume. It internally detects what type of volume is used.
- *
- * @throws NoConnectionException If no connectivity to the device exists
- * @throws TransientNetworkDisconnectionException If framework is still trying to recover from
- * a possibly transient loss of network
- */
- public double getStreamVolume() throws TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getMediaStatus().getStreamVolume();
- }
-
- /**
- * Sets the stream volume.
- *
- * @param volume Should be a value between 0 and 1, inclusive.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- * @throws CastException If setting system volume fails
- */
- public void setStreamVolume(double volume) throws CastException,
- TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- if (volume > 1.0) {
- volume = 1.0;
- } else if (volume < 0) {
- volume = 0.0;
- }
-
- RemoteMediaPlayer mediaPlayer = getRemoteMediaPlayer();
- if (mediaPlayer == null) {
- throw new NoConnectionException();
- }
- mediaPlayer.setStreamVolume(mApiClient, volume).setResultCallback(
- (result) -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_setting_volume,
- result.getStatus().getStatusCode());
- } else {
- CastManager.this.onStreamVolumeChanged();
- }
- });
- }
-
- /**
- * Returns <code>true</code> if remote Stream is muted.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public boolean isStreamMute() throws TransientNetworkDisconnectionException, NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getMediaStatus().isMute();
- }
-
- /**
- * Returns the duration of the media that is loaded, in milliseconds.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public long getMediaDuration() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getStreamDuration();
- }
-
- /**
- * Returns the current (approximate) position of the current media, in milliseconds.
- *
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public long getCurrentMediaPosition() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- checkRemoteMediaPlayerAvailable();
- return remoteMediaPlayer.getApproximateStreamPosition();
- }
-
- public int getApplicationStandbyState() throws IllegalStateException {
- Log.d(TAG, "getApplicationStandbyState()");
- return Cast.CastApi.getStandbyState(mApiClient);
- }
-
- private void onApplicationDisconnected(int errorCode) {
- Log.d(TAG, "onApplicationDisconnected() reached with error code: " + errorCode);
- mApplicationErrorCode = errorCode;
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationDisconnected(errorCode);
- }
- if (mMediaRouter != null) {
- Log.d(TAG, "onApplicationDisconnected(): Cached RouteInfo: " + getRouteInfo());
- Log.d(TAG, "onApplicationDisconnected(): Selected RouteInfo: "
- + mMediaRouter.getSelectedRoute());
- if (getRouteInfo() == null || mMediaRouter.getSelectedRoute().equals(getRouteInfo())) {
- Log.d(TAG, "onApplicationDisconnected(): Setting route to default");
- mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
- }
- }
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- }
-
- private void onApplicationStatusChanged() {
- if (!isConnected()) {
- return;
- }
- try {
- String appStatus = Cast.CastApi.getApplicationStatus(mApiClient);
- Log.d(TAG, "onApplicationStatusChanged() reached: " + appStatus);
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationStatusChanged(appStatus);
- }
- } catch (IllegalStateException e) {
- Log.e(TAG, "onApplicationStatusChanged()", e);
- }
- }
-
- private void onDeviceVolumeChanged() {
- Log.d(TAG, "onDeviceVolumeChanged() reached");
- double volume;
- try {
- volume = getDeviceVolume();
- boolean isMute = isDeviceMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onVolumeChanged(volume, isMute);
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Failed to get volume", e);
- }
-
- }
-
- private void onStreamVolumeChanged() {
- Log.d(TAG, "onStreamVolumeChanged() reached");
- double volume;
- try {
- volume = getStreamVolume();
- boolean isMute = isStreamMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onStreamVolumeChanged(volume, isMute);
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Failed to get volume", e);
- }
- }
-
- @Override
- protected void onApplicationConnected(ApplicationMetadata appMetadata,
- String applicationStatus, String sessionId, boolean wasLaunched) {
- Log.d(TAG, "onApplicationConnected() reached with sessionId: " + sessionId
- + ", and mReconnectionStatus=" + mReconnectionStatus);
- mApplicationErrorCode = NO_APPLICATION_ERROR;
- if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) {
- // we have tried to reconnect and successfully launched the app, so
- // it is time to select the route and make the cast icon happy :-)
- List<MediaRouter.RouteInfo> routes = mMediaRouter.getRoutes();
- if (routes != null) {
- String routeId = mPreferenceAccessor.getStringFromPreference(PREFS_KEY_ROUTE_ID);
- for (MediaRouter.RouteInfo routeInfo : routes) {
- if (routeId.equals(routeInfo.getId())) {
- // found the right route
- Log.d(TAG, "Found the correct route during reconnection attempt");
- mReconnectionStatus = RECONNECTION_STATUS_FINALIZED;
- mMediaRouter.selectRoute(routeInfo);
- break;
- }
- }
- }
- }
- try {
- //attachDataChannel();
- attachMediaChannel();
- mSessionId = sessionId;
- // saving device for future retrieval; we only save the last session info
- mPreferenceAccessor.saveStringToPreference(PREFS_KEY_SESSION_ID, mSessionId);
- remoteMediaPlayer.requestStatus(mApiClient).setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_status_request,
- result.getStatus().getStatusCode());
- }
- });
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationConnected(appMetadata, mSessionId, wasLaunched);
- }
- } catch (TransientNetworkDisconnectionException e) {
- Log.e(TAG, "Failed to attach media/data channel due to network issues", e);
- onFailed(R.string.cast_failed_no_connection_trans, NO_STATUS_CODE);
- } catch (NoConnectionException e) {
- Log.e(TAG, "Failed to attach media/data channel due to network issues", e);
- onFailed(R.string.cast_failed_no_connection, NO_STATUS_CODE);
- }
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager
- * #onConnectivityRecovered()
- */
- @Override
- public void onConnectivityRecovered() {
- reattachMediaChannel();
- //reattachDataChannel();
- super.onConnectivityRecovered();
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.CastClient.Listener#onApplicationStopFailed (int)
- */
- @Override
- public void onApplicationStopFailed(int errorCode) {
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationStopFailed(errorCode);
- }
- }
-
- @Override
- public void onApplicationConnectionFailed(int errorCode) {
- Log.d(TAG, "onApplicationConnectionFailed() reached with errorCode: " + errorCode);
- mApplicationErrorCode = errorCode;
- if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) {
- if (errorCode == CastStatusCodes.APPLICATION_NOT_RUNNING) {
- // while trying to re-establish session, we found out that the app is not running
- // so we need to disconnect
- mReconnectionStatus = RECONNECTION_STATUS_INACTIVE;
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- }
- } else {
- for (CastConsumer consumer : castConsumers) {
- consumer.onApplicationConnectionFailed(errorCode);
- }
- onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
- if (mMediaRouter != null) {
- Log.d(TAG, "onApplicationConnectionFailed(): Setting route to default");
- mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
- }
- }
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>.
- * Units is milliseconds.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, boolean autoPlay, int position)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- loadMedia(media, autoPlay, position, null);
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>).
- * Units is milliseconds.
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, boolean autoPlay, int position, JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- loadMedia(media, null, autoPlay, position, customData);
- }
-
- /**
- * Loads a media. For this to succeed, you need to have successfully launched the application.
- *
- * @param media The media to be loaded
- * @param activeTracks An array containing the list of track IDs to be set active for this
- * media upon a successful load
- * @param autoPlay If <code>true</code>, playback starts after load
- * @param position Where to start the playback (only used if autoPlay is <code>true</code>).
- * Units is milliseconds.
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void loadMedia(MediaInfo media, final long[] activeTracks, boolean autoPlay,
- int position, JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "loadMedia");
- checkConnectivity();
- if (media == null) {
- return;
- }
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to load a video with no active media session");
- throw new NoConnectionException();
- }
-
- Log.d(TAG, "remoteMediaPlayer.load() with media=" + media.getMetadata().getString(MediaMetadata.KEY_TITLE)
- + ", position=" + position + ", autoplay=" + autoPlay);
- remoteMediaPlayer.load(mApiClient, media, autoPlay, position, activeTracks, customData)
- .setResultCallback(result -> {
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaLoadResult(result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Loads and optionally starts playback of a new queue of media items.
- *
- * @param items Array of items to load, in the order that they should be played. Must not be
- * {@code null} or empty.
- * @param startIndex The array index of the item in the {@code items} array that should be
- * played first (i.e., it will become the currentItem).If {@code repeatMode}
- * is {@link MediaStatus#REPEAT_MODE_REPEAT_OFF} playback will end when the
- * last item in the array is played.
- * <p>
- * This may be useful for continuation scenarios where the user was already
- * using the sender application and in the middle decides to cast. This lets
- * the sender application avoid mapping between the local and remote queue
- * positions and/or avoid issuing an extra request to update the queue.
- * <p>
- * This value must be less than the length of {@code items}.
- * @param repeatMode The repeat playback mode for the queue. One of
- * {@link MediaStatus#REPEAT_MODE_REPEAT_OFF},
- * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL},
- * {@link MediaStatus#REPEAT_MODE_REPEAT_SINGLE} and
- * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE}.
- * @param customData Custom application-specific data to pass along with the request, may be
- * {@code null}.
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void queueLoad(final MediaQueueItem[] items, final int startIndex, final int repeatMode,
- final JSONObject customData)
- throws TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "queueLoad");
- checkConnectivity();
- if (items == null || items.length == 0) {
- return;
- }
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to queue one or more videos with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.queueLoad() with " + items.length + "items, starting at "
- + startIndex);
- remoteMediaPlayer
- .queueLoad(mApiClient, items, startIndex, repeatMode, customData)
- .setResultCallback(result -> {
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaQueueOperationResult(QUEUE_OPERATION_LOAD,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Plays the loaded media.
- *
- * @param position Where to start the playback. Units is milliseconds.
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- checkConnectivity();
- Log.d(TAG, "attempting to play media at position " + position + " seconds");
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to play a video with no active media session");
- throw new NoConnectionException();
- }
- seekAndPlay(position);
- }
-
- /**
- * Resumes the playback from where it was left (can be the beginning).
- *
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "play(customData)");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to play a video with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.play(mApiClient, customData)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_play,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Resumes the playback from where it was left (can be the beginning).
- *
- * @throws CastException
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void play() throws CastException, TransientNetworkDisconnectionException,
- NoConnectionException {
- play(null);
- }
-
- /**
- * Stops the playback of media/stream
- *
- * @param customData Optional {@link JSONObject}
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void stop(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "stop()");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to stop a stream with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.stop(mApiClient, customData).setResultCallback(
- result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_stop,
- result.getStatus().getStatusCode());
- }
- }
- );
- }
-
- /**
- * Stops the playback of media/stream
- *
- * @throws CastException
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void stop() throws CastException,
- TransientNetworkDisconnectionException, NoConnectionException {
- stop(null);
- }
-
- /**
- * Pauses the playback.
- *
- * @throws CastException
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void pause() throws CastException, TransientNetworkDisconnectionException,
- NoConnectionException {
- pause(null);
- }
-
- /**
- * Pauses the playback.
- *
- * @param customData Optional {@link JSONObject} data to be passed to the cast device
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void pause(JSONObject customData) throws
- TransientNetworkDisconnectionException, NoConnectionException {
- Log.d(TAG, "attempting to pause media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to pause a video with no active media session");
- throw new NoConnectionException();
- }
- remoteMediaPlayer.pause(mApiClient, customData)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_to_pause,
- result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Seeks to the given point without changing the state of the player, i.e. after seek is
- * completed, it resumes what it was doing before the start of seek.
- *
- * @param position in milliseconds
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void seek(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attempting to seek media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seek a video with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.seek() to position " + position);
- remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_UNCHANGED).setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode());
- }
- });
- }
-
- /**
- * Fast forwards the media by the given amount. If {@code lengthInMillis} is negative, it
- * rewinds the media.
- *
- * @param lengthInMillis The amount to fast forward the media, given in milliseconds
- * @throws TransientNetworkDisconnectionException
- * @throws NoConnectionException
- */
- public void forward(int lengthInMillis) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "forward(): attempting to forward media by " + lengthInMillis);
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seek a video with no active media session");
- throw new NoConnectionException();
- }
- long position = remoteMediaPlayer.getApproximateStreamPosition() + lengthInMillis;
- seek((int) position);
- }
-
- /**
- * Seeks to the given point and starts playback regardless of the starting state.
- *
- * @param position in milliseconds
- * @throws NoConnectionException
- * @throws TransientNetworkDisconnectionException
- */
- public void seekAndPlay(int position) throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attempting to seek media");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- Log.e(TAG, "Trying to seekAndPlay a video with no active media session");
- throw new NoConnectionException();
- }
- Log.d(TAG, "remoteMediaPlayer.seek() to position " + position + "and play");
- remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_PLAY)
- .setResultCallback(result -> {
- if (!result.getStatus().isSuccess()) {
- onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode());
- }
- });
- }
-
- private void attachMediaChannel() throws TransientNetworkDisconnectionException,
- NoConnectionException {
- Log.d(TAG, "attachMediaChannel()");
- checkConnectivity();
- if (remoteMediaPlayer == null) {
- remoteMediaPlayer = new RemoteMediaPlayer();
-
- remoteMediaPlayer.setOnStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onStatusUpdated() is reached");
- CastManager.this.onRemoteMediaPlayerStatusUpdated();
- }
- );
-
- remoteMediaPlayer.setOnPreloadStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onPreloadStatusUpdated() is reached");
- CastManager.this.onRemoteMediaPreloadStatusUpdated();
- });
-
-
- remoteMediaPlayer.setOnMetadataUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onMetadataUpdated() is reached");
- CastManager.this.onRemoteMediaPlayerMetadataUpdated();
- }
- );
-
- remoteMediaPlayer.setOnQueueStatusUpdatedListener(
- () -> {
- Log.d(TAG, "RemoteMediaPlayer::onQueueStatusUpdated() is reached");
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus != null
- && mediaStatus.getQueueItems() != null) {
- List<MediaQueueItem> queueItems = mediaStatus
- .getQueueItems();
- int itemId = mediaStatus.getCurrentItemId();
- MediaQueueItem item = mediaStatus
- .getQueueItemById(itemId);
- int repeatMode = mediaStatus.getQueueRepeatMode();
- onQueueUpdated(queueItems, item, repeatMode, false);
- } else {
- onQueueUpdated(null, null,
- MediaStatus.REPEAT_MODE_REPEAT_OFF, false);
- }
- });
-
- }
- try {
- Log.d(TAG, "Registering MediaChannel namespace");
- Cast.CastApi.setMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace(),
- remoteMediaPlayer);
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "attachMediaChannel()", e);
- }
- }
-
- private void reattachMediaChannel() {
- if (remoteMediaPlayer != null && mApiClient != null) {
- try {
- Log.d(TAG, "Registering MediaChannel namespace");
- Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
- remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "reattachMediaChannel()", e);
- }
- }
- }
-
- private void detachMediaChannel() {
- Log.d(TAG, "trying to detach media channel");
- if (remoteMediaPlayer != null) {
- try {
- Cast.CastApi.removeMessageReceivedCallbacks(mApiClient,
- remoteMediaPlayer.getNamespace());
- } catch (IOException | IllegalStateException e) {
- Log.e(TAG, "detachMediaChannel()", e);
- }
- remoteMediaPlayer = null;
- }
- }
-
- /**
- * Returns the latest retrieved value for the {@link MediaStatus}. This value is updated
- * whenever the onStatusUpdated callback is called.
- */
- public final MediaStatus getMediaStatus() {
- return mediaStatus;
- }
-
- /*
- * This is called by onStatusUpdated() of the RemoteMediaPlayer
- */
- private void onRemoteMediaPlayerStatusUpdated() {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated() reached");
- if (mApiClient == null || remoteMediaPlayer == null) {
- Log.d(TAG, "mApiClient or remoteMediaPlayer is null, so will not proceed");
- return;
- }
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus == null) {
- Log.d(TAG, "MediaStatus is null, so will not proceed");
- return;
- } else {
- List<MediaQueueItem> queueItems = mediaStatus.getQueueItems();
- if (queueItems != null) {
- int itemId = mediaStatus.getCurrentItemId();
- MediaQueueItem item = mediaStatus.getQueueItemById(itemId);
- int repeatMode = mediaStatus.getQueueRepeatMode();
- onQueueUpdated(queueItems, item, repeatMode, false);
- } else {
- onQueueUpdated(null, null, MediaStatus.REPEAT_MODE_REPEAT_OFF, false);
- }
- state = mediaStatus.getPlayerState();
- int idleReason = mediaStatus.getIdleReason();
-
- if (state == MediaStatus.PLAYER_STATE_PLAYING) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = playing");
- } else if (state == MediaStatus.PLAYER_STATE_PAUSED) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = paused");
- } else if (state == MediaStatus.PLAYER_STATE_IDLE) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = IDLE with reason: "
- + idleReason);
- if (idleReason == MediaStatus.IDLE_REASON_ERROR) {
- // something bad happened on the cast device
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): IDLE reason = ERROR");
- onFailed(R.string.cast_failed_receiver_player_error, NO_STATUS_CODE);
- }
- } else if (state == MediaStatus.PLAYER_STATE_BUFFERING) {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = buffering");
- } else {
- Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = unknown");
- }
- }
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPlayerStatusUpdated();
- }
- if (mediaStatus != null) {
- double volume = mediaStatus.getStreamVolume();
- boolean isMute = mediaStatus.isMute();
- for (CastConsumer consumer : castConsumers) {
- consumer.onStreamVolumeChanged(volume, isMute);
- }
- }
- }
-
- private void onRemoteMediaPreloadStatusUpdated() {
- MediaQueueItem item = null;
- mediaStatus = remoteMediaPlayer.getMediaStatus();
- if (mediaStatus != null) {
- item = mediaStatus.getQueueItemById(mediaStatus.getPreloadedItemId());
- }
- Log.d(TAG, "onRemoteMediaPreloadStatusUpdated() " + item);
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPreloadStatusUpdated(item);
- }
- }
-
- /*
- * This is called by onQueueStatusUpdated() of RemoteMediaPlayer
- */
- private void onQueueUpdated(List<MediaQueueItem> queueItems, MediaQueueItem item,
- int repeatMode, boolean shuffle) {
- Log.d(TAG, "onQueueUpdated() reached");
- Log.d(TAG, String.format(Locale.US, "Queue Items size: %d, Item: %s, Repeat Mode: %d, Shuffle: %s",
- queueItems == null ? 0 : queueItems.size(), item, repeatMode, shuffle));
- for (CastConsumer consumer : castConsumers) {
- consumer.onMediaQueueUpdated(queueItems, item, repeatMode, shuffle);
- }
- }
-
- /*
- * This is called by onMetadataUpdated() of RemoteMediaPlayer
- */
- public void onRemoteMediaPlayerMetadataUpdated() {
- Log.d(TAG, "onRemoteMediaPlayerMetadataUpdated() reached");
- for (CastConsumer consumer : castConsumers) {
- consumer.onRemoteMediaPlayerMetadataUpdated();
- }
- }
-
- /**
- * Registers a {@link CastConsumer} interface with this class.
- * Registered listeners will be notified of changes to a variety of
- * lifecycle and media status changes through the callbacks that the interface provides.
- *
- * @see DefaultCastConsumer
- */
- public synchronized void addCastConsumer(CastConsumer listener) {
- if (listener != null) {
- addBaseCastConsumer(listener);
- castConsumers.add(listener);
- Log.d(TAG, "Successfully added the new CastConsumer listener " + listener);
- }
- }
-
- /**
- * Unregisters a {@link CastConsumer}.
- */
- public synchronized void removeCastConsumer(CastConsumer listener) {
- if (listener != null) {
- removeBaseCastConsumer(listener);
- castConsumers.remove(listener);
- }
- }
-
- @Override
- protected void onDeviceUnselected() {
- detachMediaChannel();
- //removeDataChannel();
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- @Override
- protected Cast.CastOptions.Builder getCastOptionBuilder(CastDevice device) {
- Cast.CastOptions.Builder builder = new Cast.CastOptions.Builder(mSelectedCastDevice, new CastListener());
- if (isFeatureEnabled(CastConfiguration.FEATURE_DEBUGGING)) {
- builder.setVerboseLoggingEnabled(true);
- }
- return builder;
- }
-
- @Override
- public void onConnectionFailed(ConnectionResult result) {
- super.onConnectionFailed(result);
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- @Override
- public void onDisconnected(boolean stopAppOnExit, boolean clearPersistedConnectionData,
- boolean setDefaultRoute) {
- super.onDisconnected(stopAppOnExit, clearPersistedConnectionData, setDefaultRoute);
- state = MediaStatus.PLAYER_STATE_IDLE;
- mediaStatus = null;
- }
-
- class CastListener extends Cast.Listener {
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.Cast.Listener#onApplicationDisconnected (int)
- */
- @Override
- public void onApplicationDisconnected(int statusCode) {
- CastManager.this.onApplicationDisconnected(statusCode);
- }
-
- /*
- * (non-Javadoc)
- * @see com.google.android.gms.cast.Cast.Listener#onApplicationStatusChanged ()
- */
- @Override
- public void onApplicationStatusChanged() {
- CastManager.this.onApplicationStatusChanged();
- }
-
- @Override
- public void onVolumeChanged() {
- CastManager.this.onDeviceVolumeChanged();
- }
- }
-
- @Override
- public void onFailed(int resourceId, int statusCode) {
- Log.d(TAG, "onFailed: " + mContext.getString(resourceId) + ", code: " + statusCode);
- super.onFailed(resourceId, statusCode);
- }
-
- /**
- * Checks whether the selected Cast Device has the specified audio or video capabilities.
- *
- * @param capability capability from:
- * <ul>
- * <li>{@link CastDevice#CAPABILITY_AUDIO_IN}</li>
- * <li>{@link CastDevice#CAPABILITY_AUDIO_OUT}</li>
- * <li>{@link CastDevice#CAPABILITY_VIDEO_IN}</li>
- * <li>{@link CastDevice#CAPABILITY_VIDEO_OUT}</li>
- * </ul>
- * @param defaultVal value to return whenever there's no device selected.
- * @return {@code true} if the selected device has the specified capability,
- * {@code false} otherwise.
- */
- public boolean hasCapability(final int capability, final boolean defaultVal) {
- if (mSelectedCastDevice != null) {
- return mSelectedCastDevice.hasCapability(capability);
- } else {
- return defaultVal;
- }
- }
-
- /**
- * Adds and wires up the Switchable Media Router cast button. It returns a reference to the
- * {@link SwitchableMediaRouteActionProvider} associated with the button if the caller needs
- * such reference. It is assumed that the enclosing
- * {@link android.app.Activity} inherits (directly or indirectly) from
- * {@link androidx.appcompat.app.AppCompatActivity}.
- *
- * @param menuItem MenuItem of the Media Router cast button.
- */
- public final SwitchableMediaRouteActionProvider addMediaRouterButton(@NonNull MenuItem menuItem) {
- ActionProvider actionProvider = MenuItemCompat.getActionProvider(menuItem);
- if (!(actionProvider instanceof SwitchableMediaRouteActionProvider)) {
- Log.wtf(TAG, "MenuItem provided to addMediaRouterButton() is not compatible with " +
- "SwitchableMediaRouteActionProvider." +
- ((actionProvider == null) ? " Its action provider is null!" : ""),
- new ClassCastException());
- return null;
- }
- SwitchableMediaRouteActionProvider mediaRouteActionProvider =
- (SwitchableMediaRouteActionProvider) actionProvider;
- mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
- if (mCastConfiguration.getMediaRouteDialogFactory() != null) {
- mediaRouteActionProvider.setDialogFactory(mCastConfiguration.getMediaRouteDialogFactory());
- }
- return mediaRouteActionProvider;
- }
-
- /* (non-Javadoc)
- * These methods startReconnectionService and stopReconnectionService simply override the ones
- * from BaseCastManager with empty implementations because we handle the service ourselves, but
- * need to allow BaseCastManager to save current network information.
- */
- @Override
- protected void startReconnectionService(long mediaDurationLeft) {
- // Do nothing
- }
-
- @Override
- protected void stopReconnectionService() {
- // Do nothing
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
deleted file mode 100644
index e1f52aa9f..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java
+++ /dev/null
@@ -1,303 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.content.ContentResolver;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.google.android.gms.cast.CastDevice;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.common.images.WebImage;
-
-import java.util.Calendar;
-import java.util.List;
-
-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.playback.Playable;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
-import de.danoeh.antennapod.core.storage.DBReader;
-
-/**
- * Helper functions for Cast support.
- */
-public class CastUtils {
- private CastUtils(){}
-
- private static final String TAG = "CastUtils";
-
- public static final String KEY_MEDIA_ID = "de.danoeh.antennapod.core.cast.MediaId";
-
- public static final String KEY_EPISODE_IDENTIFIER = "de.danoeh.antennapod.core.cast.EpisodeId";
- public static final String KEY_EPISODE_LINK = "de.danoeh.antennapod.core.cast.EpisodeLink";
- public static final String KEY_FEED_URL = "de.danoeh.antennapod.core.cast.FeedUrl";
- public static final String KEY_FEED_WEBSITE = "de.danoeh.antennapod.core.cast.FeedWebsite";
- public static final String KEY_EPISODE_NOTES = "de.danoeh.antennapod.core.cast.EpisodeNotes";
-
- /**
- * The field <code>AntennaPod.FormatVersion</code> specifies which version of MediaMetaData
- * fields we're using. Future implementations should try to be backwards compatible with earlier
- * versions, and earlier versions should be forward compatible until the version indicated by
- * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code>. If an update makes the format unreadable for
- * an earlier version, then its version number should be greater than the
- * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code> value set on the earlier one, so that it
- * doesn't try to parse the object.
- */
- public static final String KEY_FORMAT_VERSION = "de.danoeh.antennapod.core.cast.FormatVersion";
- public static final int FORMAT_VERSION_VALUE = 1;
- public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999;
-
- public static boolean isCastable(Playable media) {
- if (media == null) {
- return false;
- }
- if (media instanceof FeedMedia || media instanceof RemoteMedia) {
- String url = media.getStreamUrl();
- if (url == null || url.isEmpty()) {
- return false;
- }
- if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
- return false; // Local feed
- }
- switch (media.getMediaType()) {
- case UNKNOWN:
- return false;
- case AUDIO:
- return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT, true);
- case VIDEO:
- return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT, true);
- }
- }
- return false;
- }
-
- /**
- * Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device.
- * Before using this method, one should make sure {@link #isCastable(Playable)} returns
- * {@code true}. This method should not run on the main thread.
- *
- * @param media The {@link FeedMedia} object to be converted.
- * @return {@link MediaInfo} object in a format proper for casting.
- */
- public static MediaInfo convertFromFeedMedia(FeedMedia media){
- if (media == null) {
- return null;
- }
- MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
- if (media.getItem() == null) {
- media.setItem(DBReader.getFeedItem(media.getItemId()));
- }
- FeedItem feedItem = media.getItem();
- if (feedItem != null) {
- metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
- String subtitle = media.getFeedTitle();
- if (subtitle != null) {
- metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
- }
-
- if (!TextUtils.isEmpty(feedItem.getImageLocation())) {
- metadata.addImage(new WebImage(Uri.parse(feedItem.getImageLocation())));
- }
- Calendar calendar = Calendar.getInstance();
- calendar.setTime(media.getItem().getPubDate());
- metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
- Feed feed = feedItem.getFeed();
- if (feed != null) {
- if (!TextUtils.isEmpty(feed.getAuthor())) {
- metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor());
- }
- if (!TextUtils.isEmpty(feed.getDownload_url())) {
- metadata.putString(KEY_FEED_URL, feed.getDownload_url());
- }
- if (!TextUtils.isEmpty(feed.getLink())) {
- metadata.putString(KEY_FEED_WEBSITE, feed.getLink());
- }
- }
- if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) {
- metadata.putString(KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier());
- } else {
- metadata.putString(KEY_EPISODE_IDENTIFIER, media.getStreamUrl());
- }
- if (!TextUtils.isEmpty(feedItem.getLink())) {
- metadata.putString(KEY_EPISODE_LINK, feedItem.getLink());
- }
- try {
- DBReader.loadDescriptionOfFeedItem(feedItem);
- metadata.putString(KEY_EPISODE_NOTES, feedItem.getDescription());
- } catch (Exception e) {
- Log.e(TAG, "Unable to load FeedMedia notes", e);
- }
- }
- // This field only identifies the id on the device that has the original version.
- // Idea is to perhaps, on a first approach, check if the version on the local DB with the
- // same id matches the remote object, and if not then search for episode and feed identifiers.
- // This at least should make media recognition for a single device much quicker.
- metadata.putInt(KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue());
- // A way to identify different casting media formats in case we change it in the future and
- // senders with different versions share a casting device.
- metadata.putInt(KEY_FORMAT_VERSION, FORMAT_VERSION_VALUE);
-
- MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl())
- .setContentType(media.getMime_type())
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setMetadata(metadata);
- if (media.getDuration() > 0) {
- builder.setStreamDuration(media.getDuration());
- }
- return builder.build();
- }
-
- //TODO make unit tests for all the conversion methods
- /**
- * Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}.
- *
- * Unless <code>searchFeedMedia</code> is set to <code>false</code>, this method should not run
- * on the GUI thread.
- *
- * @param media The {@link MediaInfo} object to be converted.
- * @param searchFeedMedia If set to <code>true</code>, the database will be queried to find a
- * {@link FeedMedia} instance that matches {@param media}.
- * @return {@link Playable} object in a format proper for casting.
- */
- public static Playable getPlayable(MediaInfo media, boolean searchFeedMedia) {
- Log.d(TAG, "getPlayable called with searchFeedMedia=" + searchFeedMedia);
- if (media == null) {
- Log.d(TAG, "MediaInfo object provided is null, not converting to any Playable instance");
- return null;
- }
- MediaMetadata metadata = media.getMetadata();
- int version = metadata.getInt(KEY_FORMAT_VERSION);
- if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) {
- Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this" +
- "version of AntennaPod CastUtils, curVer=" + FORMAT_VERSION_VALUE +
- ", object version=" + version);
- return null;
- }
- Playable result = null;
- if (searchFeedMedia) {
- long mediaId = metadata.getInt(KEY_MEDIA_ID);
- if (mediaId > 0) {
- FeedMedia fMedia = DBReader.getFeedMedia(mediaId);
- if (fMedia != null) {
- if (matches(media, fMedia)) {
- result = fMedia;
- Log.d(TAG, "FeedMedia object obtained matches the MediaInfo provided. id=" + mediaId);
- } else {
- Log.d(TAG, "FeedMedia object obtained does NOT match the MediaInfo provided. id=" + mediaId);
- }
- } else {
- Log.d(TAG, "Unable to find in database a FeedMedia with id=" + mediaId);
- }
- }
- if (result == null) {
- FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(null,
- metadata.getString(KEY_EPISODE_IDENTIFIER));
- if (feedItem != null) {
- result = feedItem.getMedia();
- Log.d(TAG, "Found episode that matches the MediaInfo provided. Using its media, if existing.");
- }
- }
- }
- if (result == null) {
- List<WebImage> imageList = metadata.getImages();
- String imageUrl = null;
- if (!imageList.isEmpty()) {
- imageUrl = imageList.get(0).getUrl().toString();
- }
- String notes = metadata.getString(KEY_EPISODE_NOTES);
- result = new RemoteMedia(media.getContentId(),
- metadata.getString(KEY_EPISODE_IDENTIFIER),
- metadata.getString(KEY_FEED_URL),
- metadata.getString(MediaMetadata.KEY_SUBTITLE),
- metadata.getString(MediaMetadata.KEY_TITLE),
- metadata.getString(KEY_EPISODE_LINK),
- metadata.getString(MediaMetadata.KEY_ARTIST),
- imageUrl,
- metadata.getString(KEY_FEED_WEBSITE),
- media.getContentType(),
- metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime(),
- notes);
- Log.d(TAG, "Converted MediaInfo into RemoteMedia");
- }
- if (result.getDuration() == 0 && media.getStreamDuration() > 0) {
- result.setDuration((int) media.getStreamDuration());
- }
- return result;
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they
- * represent the same podcast episode.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link FeedMedia} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, FeedMedia media) {
- if (info == null || media == null) {
- return false;
- }
- if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
- return false;
- }
- MediaMetadata metadata = info.getMetadata();
- FeedItem fi = media.getItem();
- if (fi == null || metadata == null ||
- !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) {
- return false;
- }
- Feed feed = fi.getFeed();
- return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url());
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they
- * represent the same podcast episode.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link RemoteMedia} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, RemoteMedia media) {
- if (info == null || media == null) {
- return false;
- }
- if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
- return false;
- }
- MediaMetadata metadata = info.getMetadata();
- return metadata != null &&
- TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier()) &&
- TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl());
- }
-
- /**
- * Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they
- * represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
- * and want to avoid unnecessary conversions.
- *
- * @param info the {@link MediaInfo} object to be compared.
- * @param media the {@link Playable} object to be compared.
- * @return <true>true</true> if there's a match, <code>false</code> otherwise.
- *
- * @see RemoteMedia#equals(Object)
- */
- public static boolean matches(MediaInfo info, Playable media) {
- if (info == null || media == null) {
- return false;
- }
- if (media instanceof RemoteMedia) {
- return matches(info, (RemoteMedia) media);
- }
- return media instanceof FeedMedia && matches(info, (FeedMedia) media);
- }
-
-
- //TODO Queue handling perhaps
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
deleted file mode 100644
index fe4183d54..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumerImpl;
-
-public class DefaultCastConsumer extends VideoCastConsumerImpl implements CastConsumer {
- @Override
- public void onStreamVolumeChanged(double value, boolean isMute) {
- // no-op
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java b/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java
deleted file mode 100644
index 00011ef05..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.net.Uri;
-import android.text.TextUtils;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaMetadata;
-import com.google.android.gms.common.images.WebImage;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
-import java.util.Calendar;
-
-public class MediaInfoCreator {
- public static MediaInfo from(RemoteMedia media) {
- MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
-
- metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
- metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle());
- if (!TextUtils.isEmpty(media.getImageLocation())) {
- metadata.addImage(new WebImage(Uri.parse(media.getImageLocation())));
- }
- Calendar calendar = Calendar.getInstance();
- calendar.setTime(media.getPubDate());
- metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
- if (!TextUtils.isEmpty(media.getFeedAuthor())) {
- metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor());
- }
- if (!TextUtils.isEmpty(media.getFeedUrl())) {
- metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl());
- }
- if (!TextUtils.isEmpty(media.getFeedLink())) {
- metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink());
- }
- if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
- metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier());
- } else {
- metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl());
- }
- if (!TextUtils.isEmpty(media.getEpisodeLink())) {
- metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink());
- }
- String notes = media.getNotes();
- if (notes != null) {
- metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
- }
- // Default id value
- metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
- metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
-
- MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl())
- .setContentType(media.getMimeType())
- .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
- .setMetadata(metadata);
- if (media.getDuration() > 0) {
- builder.setStreamDuration(media.getDuration());
- }
- return builder.build();
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java b/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
deleted file mode 100644
index 5a6a0aa2b..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package de.danoeh.antennapod.core.cast;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.ContextWrapper;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-import androidx.mediarouter.app.MediaRouteActionProvider;
-import androidx.mediarouter.app.MediaRouteChooserDialogFragment;
-import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
-import androidx.mediarouter.media.MediaRouter;
-import android.util.Log;
-
-/**
- * <p>Action Provider that extends {@link MediaRouteActionProvider} and allows the client to
- * disable completely the button by calling {@link #setEnabled(boolean)}.</p>
- *
- * <p>It is disabled by default, so if a client wants to initially have it enabled it must call
- * <code>setEnabled(true)</code>.</p>
- */
-public class SwitchableMediaRouteActionProvider extends MediaRouteActionProvider {
- public static final String TAG = "SwitchblMediaRtActProv";
-
- private static final String CHOOSER_FRAGMENT_TAG =
- "android.support.v7.mediarouter:MediaRouteChooserDialogFragment";
- private static final String CONTROLLER_FRAGMENT_TAG =
- "android.support.v7.mediarouter:MediaRouteControllerDialogFragment";
- private boolean enabled;
-
- public SwitchableMediaRouteActionProvider(Context context) {
- super(context);
- enabled = false;
- }
-
- /**
- * <p>Sets whether the Media Router button should be allowed to become visible or not.</p>
- *
- * <p>It's invisible by default.</p>
- */
- public void setEnabled(boolean newVal) {
- enabled = newVal;
- refreshVisibility();
- }
-
- @Override
- public boolean isVisible() {
- return enabled && super.isVisible();
- }
-
- @Override
- public boolean onPerformDefaultAction() {
- if (!super.onPerformDefaultAction()) {
- // there is no button, but we should still show the dialog if it's the case.
- if (!isVisible()) {
- return false;
- }
- FragmentManager fm = getFragmentManager();
- if (fm == null) {
- return false;
- }
- MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute();
- if (route.isDefault() || !route.matchesSelector(getRouteSelector())) {
- if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
- Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
- return false;
- }
- MediaRouteChooserDialogFragment f =
- getDialogFactory().onCreateChooserDialogFragment();
- f.setRouteSelector(getRouteSelector());
- f.show(fm, CHOOSER_FRAGMENT_TAG);
- } else {
- if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
- Log.w(TAG, "showDialog(): Route controller dialog already showing!");
- return false;
- }
- MediaRouteControllerDialogFragment f =
- getDialogFactory().onCreateControllerDialogFragment();
- f.show(fm, CONTROLLER_FRAGMENT_TAG);
- }
- return true;
-
- } else {
- return true;
- }
- }
-
- private FragmentManager getFragmentManager() {
- Activity activity = getActivity();
- if (activity instanceof FragmentActivity) {
- return ((FragmentActivity)activity).getSupportFragmentManager();
- }
- return null;
- }
-
- private Activity getActivity() {
- // Gross way of unwrapping the Activity so we can get the FragmentManager
- Context context = getContext();
- while (context instanceof ContextWrapper) {
- if (context instanceof Activity) {
- return (Activity)context;
- }
- context = ((ContextWrapper)context).getBaseContext();
- }
- return null;
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
deleted file mode 100644
index 41fd01441..000000000
--- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java
+++ /dev/null
@@ -1,314 +0,0 @@
-package de.danoeh.antennapod.core.service.playback;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.NetworkInfo;
-import android.net.wifi.WifiManager;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
-import android.support.v4.media.session.MediaSessionCompat;
-import android.support.v4.media.session.PlaybackStateCompat;
-import androidx.mediarouter.media.MediaRouter;
-import android.support.wearable.media.MediaControlConstants;
-import android.util.Log;
-import android.widget.Toast;
-
-import com.google.android.gms.cast.ApplicationMetadata;
-import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import de.danoeh.antennapod.core.cast.CastConsumer;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
-import de.danoeh.antennapod.event.MessageEvent;
-import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
-import de.danoeh.antennapod.core.util.NetworkUtils;
-import org.greenrobot.eventbus.EventBus;
-
-/**
- * Class intended to work along PlaybackService and provide support for different flavors.
- */
-public class PlaybackServiceFlavorHelper {
- public static final String TAG = "PlaybackSrvFlavorHelper";
-
- /**
- * Time in seconds during which the CastManager will try to reconnect to the Cast Device after
- * the Wifi Connection is regained.
- */
- private static final int RECONNECTION_ATTEMPT_PERIOD_S = 15;
- /**
- * Stores the state of the cast playback just before it disconnects.
- */
- private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection;
-
- private boolean wifiConnectivity = true;
- private BroadcastReceiver wifiBroadcastReceiver;
-
- private CastManager castManager;
- private MediaRouter mediaRouter;
- private PlaybackService.FlavorHelperCallback callback;
- private CastConsumer castConsumer;
-
- PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) {
- this.callback = callback;
- if (!CastManager.isInitialized()) {
- return;
- }
- mediaRouter = MediaRouter.getInstance(context.getApplicationContext());
- setCastConsumer(context);
- }
-
- void initializeMediaPlayer(Context context) {
- if (!CastManager.isInitialized()) {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- return;
- }
- castManager = CastManager.getInstance();
- castManager.addCastConsumer(castConsumer);
- boolean isCasting = castManager.isConnected();
- callback.setIsCasting(isCasting);
- if (isCasting) {
- if (UserPreferences.isCastEnabled()) {
- onCastAppConnected(context, false);
- } else {
- castManager.disconnect();
- }
- } else {
- callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()));
- }
- }
-
- void removeCastConsumer() {
- if (!CastManager.isInitialized()) {
- return;
- }
- castManager.removeCastConsumer(castConsumer);
- }
-
- boolean castDisconnect(boolean castDisconnect) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- if (castDisconnect) {
- castManager.disconnect();
- }
- return castDisconnect;
- }
-
- boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- switch (code) {
- case RemotePSMP.CAST_ERROR:
- EventBus.getDefault().post(new MessageEvent(context.getString(resourceId)));
- return true;
- case RemotePSMP.CAST_ERROR_PRIORITY_HIGH:
- Toast.makeText(context, resourceId, Toast.LENGTH_SHORT).show();
- return true;
- default:
- return false;
- }
- }
-
- private void setCastConsumer(Context context) {
- castConsumer = new DefaultCastConsumer() {
- @Override
- public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
- onCastAppConnected(context, wasLaunched);
- }
-
- @Override
- public void onDisconnectionReason(int reason) {
- Log.d(TAG, "onDisconnectionReason() with code " + reason);
- // This is our final chance to update the underlying stream position
- // In onDisconnected(), the underlying CastPlayback#mVideoCastConsumer
- // is disconnected and hence we update our local value of stream position
- // to the latest position.
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
- infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo();
- if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT &&
- infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) {
- // If it's NOT based on user action, we shouldn't automatically resume local playback
- infoBeforeCastDisconnection.playerStatus = PlayerStatus.PAUSED;
- }
- }
- }
-
- @Override
- public void onDisconnected() {
- Log.d(TAG, "onDisconnected()");
- callback.setIsCasting(false);
- PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection;
- infoBeforeCastDisconnection = null;
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (info == null && mediaPlayer != null) {
- info = mediaPlayer.getPSMPInfo();
- }
- if (info == null) {
- info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.INDETERMINATE,
- PlayerStatus.STOPPED, null);
- }
- switchMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()),
- info, true);
- if (info.playable != null) {
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_RELOAD,
- info.playable.getMediaType() == MediaType.AUDIO ?
- PlaybackService.EXTRA_CODE_AUDIO : PlaybackService.EXTRA_CODE_VIDEO);
- } else {
- Log.d(TAG, "Cast session disconnected, but no current media");
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END, 0);
- }
- // hardware volume buttons control the local device volume
- mediaRouter.setMediaSessionCompat(null);
- unregisterWifiBroadcastReceiver();
- callback.setupNotification(false, info);
- }
- };
- }
-
- private void onCastAppConnected(Context context, boolean wasLaunched) {
- Log.d(TAG, "A cast device application was " + (wasLaunched ? "launched" : "joined"));
- callback.setIsCasting(true);
- PlaybackServiceMediaPlayer.PSMPInfo info = null;
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- info = mediaPlayer.getPSMPInfo();
- if (info.playerStatus == PlayerStatus.PLAYING) {
- // could be pause, but this way we make sure the new player will get the correct position,
- // since pause runs asynchronously and we could be directing the new player to play even before
- // the old player gives us back the position.
- callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
- }
- }
- if (info == null) {
- info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.INDETERMINATE, PlayerStatus.STOPPED, null);
- }
- callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_RELOAD,
- PlaybackService.EXTRA_CODE_CAST);
- RemotePSMP remotePSMP = new RemotePSMP(context, callback.getMediaPlayerCallback());
- switchMediaPlayer(remotePSMP, info, wasLaunched);
- remotePSMP.init();
- // hardware volume buttons control the remote device volume
- mediaRouter.setMediaSessionCompat(callback.getMediaSession());
- registerWifiBroadcastReceiver();
- callback.setupNotification(true, info);
- }
-
- private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer,
- @NonNull PlaybackServiceMediaPlayer.PSMPInfo info,
- boolean wasLaunched) {
- PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer();
- if (mediaPlayer != null) {
- try {
- mediaPlayer.stopPlayback(false).get(2, TimeUnit.SECONDS);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- Log.e(TAG, "There was a problem stopping playback while switching media players", e);
- }
- mediaPlayer.shutdownQuietly();
- }
- mediaPlayer = newPlayer;
- callback.setMediaPlayer(mediaPlayer);
- Log.d(TAG, "switched to " + mediaPlayer.getClass().getSimpleName());
- if (!wasLaunched) {
- PlaybackServiceMediaPlayer.PSMPInfo candidate = mediaPlayer.getPSMPInfo();
- if (candidate.playable != null &&
- candidate.playerStatus.isAtLeast(PlayerStatus.PREPARING)) {
- // do not automatically send new media to cast device
- info.playable = null;
- }
- }
- if (info.playable != null) {
- mediaPlayer.playMediaObject(info.playable,
- !info.playable.localFileAvailable(),
- info.playerStatus == PlayerStatus.PLAYING,
- info.playerStatus.isAtLeast(PlayerStatus.PREPARING));
- }
- }
-
- void registerWifiBroadcastReceiver() {
- if (!CastManager.isInitialized()) {
- return;
- }
- if (wifiBroadcastReceiver != null) {
- return;
- }
- wifiBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
- NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
- boolean isConnected = info.isConnected();
- //apparently this method gets called twice when a change happens, but one run is enough.
- if (isConnected && !wifiConnectivity) {
- wifiConnectivity = true;
- castManager.startCastDiscovery();
- castManager.reconnectSessionIfPossible(RECONNECTION_ATTEMPT_PERIOD_S, NetworkUtils.getWifiSsid());
- } else {
- wifiConnectivity = isConnected;
- }
- }
- }
- };
- callback.registerReceiver(wifiBroadcastReceiver,
- new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION));
- }
-
- void unregisterWifiBroadcastReceiver() {
- if (!CastManager.isInitialized()) {
- return;
- }
- if (wifiBroadcastReceiver != null) {
- callback.unregisterReceiver(wifiBroadcastReceiver);
- wifiBroadcastReceiver = null;
- }
- }
-
- boolean onSharedPreference(String key) {
- if (!CastManager.isInitialized()) {
- return false;
- }
- if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
- if (!UserPreferences.isCastEnabled()) {
- if (castManager.isConnecting() || castManager.isConnected()) {
- Log.d(TAG, "Disconnecting cast device due to a change in user preferences");
- castManager.disconnect();
- }
- }
- return true;
- }
- return false;
- }
-
- void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) {
- if (!CastManager.isInitialized()) {
- return;
- }
- PlaybackStateCompat.CustomAction.Builder actionBuilder =
- new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon);
- Bundle actionExtras = new Bundle();
- actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true);
- actionBuilder.setExtras(actionExtras);
-
- sessionState.addCustomAction(actionBuilder.build());
- }
-
- void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
- if (!CastManager.isInitialized()) {
- return;
- }
- Bundle sessionExtras = new Bundle();
- sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true);
- sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true);
- mediaSession.setExtras(sessionExtras);
- }
-}
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
new file mode 100644
index 000000000..2167d9f2c
--- /dev/null
+++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java
@@ -0,0 +1,28 @@
+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;
+import android.support.wearable.media.MediaControlConstants;
+
+public class WearMediaSession {
+ public static final String TAG = "WearMediaSession";
+
+ static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName,
+ CharSequence name, int icon) {
+ PlaybackStateCompat.CustomAction.Builder actionBuilder =
+ new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon);
+ Bundle actionExtras = new Bundle();
+ actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true);
+ actionBuilder.setExtras(actionExtras);
+
+ sessionState.addCustomAction(actionBuilder.build());
+ }
+
+ static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
+ Bundle sessionExtras = new Bundle();
+ sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true);
+ sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true);
+ mediaSession.setExtras(sessionExtras);
+ }
+}
diff --git a/core/src/play/res/values/strings.xml b/core/src/play/res/values/strings.xml
deleted file mode 100644
index 7307849d2..000000000
--- a/core/src/play/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="pref_cast_message" translatable="false">@string/pref_cast_message_play_flavor</string>
-</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
index 4890c471a..92c0e8e3d 100644
--- 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
@@ -6,6 +6,8 @@ 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;
diff --git a/playback/README.md b/playback/README.md
new file mode 100644
index 000000000..0709ac2c6
--- /dev/null
+++ b/playback/README.md
@@ -0,0 +1,3 @@
+# :playback
+
+This folder contains modules that deal with media playback.
diff --git a/playback/base/README.md b/playback/base/README.md
new file mode 100644
index 000000000..281a799f1
--- /dev/null
+++ b/playback/base/README.md
@@ -0,0 +1,3 @@
+# :playback:base
+
+This module provides the basic interfaces for a PlaybackServiceMediaPlayer.
diff --git a/playback/base/build.gradle b/playback/base/build.gradle
new file mode 100644
index 000000000..73c320703
--- /dev/null
+++ b/playback/base/build.gradle
@@ -0,0 +1,10 @@
+apply plugin: "com.android.library"
+apply from: "../../common.gradle"
+
+dependencies {
+ implementation project(':model')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+
+ testImplementation 'junit:junit:4.13'
+}
diff --git a/playback/base/src/main/AndroidManifest.xml b/playback/base/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..c6a44a212
--- /dev/null
+++ b/playback/base/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.playback.base" />
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java
index 623ad58bb..d03695896 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
+++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlaybackServiceMediaPlayer.java
@@ -1,10 +1,9 @@
-package de.danoeh.antennapod.core.service.playback;
+package de.danoeh.antennapod.playback.base;
import android.content.Context;
import android.media.AudioManager;
import android.net.wifi.WifiManager;
import androidx.annotation.NonNull;
-import androidx.annotation.StringRes;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
@@ -12,6 +11,7 @@ import android.view.SurfaceHolder;
import java.util.List;
import java.util.concurrent.Future;
+import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.model.playback.Playable;
@@ -31,20 +31,20 @@ public abstract class PlaybackServiceMediaPlayer {
/**
* Return value of some PSMP methods if the method call failed.
*/
- static final int INVALID_TIME = -1;
+ public static final int INVALID_TIME = -1;
private volatile PlayerStatus oldPlayerStatus;
- volatile PlayerStatus playerStatus;
+ protected volatile PlayerStatus playerStatus;
/**
* A wifi-lock that is acquired if the media file is being streamed.
*/
private WifiManager.WifiLock wifiLock;
- final PSMPCallback callback;
- final Context context;
+ protected final PSMPCallback callback;
+ protected final Context context;
- PlaybackServiceMediaPlayer(@NonNull Context context,
+ protected PlaybackServiceMediaPlayer(@NonNull Context context,
@NonNull PSMPCallback callback){
this.context = context;
this.callback = callback;
@@ -281,7 +281,9 @@ public abstract class PlaybackServiceMediaPlayer {
*/
protected abstract boolean shouldLockWifi();
- final synchronized void acquireWifiLockIfNecessary() {
+ public abstract boolean isCasting();
+
+ protected final synchronized void acquireWifiLockIfNecessary() {
if (shouldLockWifi()) {
if (wifiLock == null) {
wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE))
@@ -292,7 +294,7 @@ public abstract class PlaybackServiceMediaPlayer {
}
}
- final synchronized void releaseWifiLockIfNecessary() {
+ protected final synchronized void releaseWifiLockIfNecessary() {
if (wifiLock != null && wifiLock.isHeld()) {
wifiLock.release();
}
@@ -313,7 +315,8 @@ public abstract class PlaybackServiceMediaPlayer {
* @param position The position to be set to the current Playable object in case playback started or paused.
* Will be ignored if given the value of {@link #INVALID_TIME}.
*/
- final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) {
+ protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus,
+ Playable newMedia, int position) {
Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
this.oldPlayerStatus = playerStatus;
@@ -339,7 +342,7 @@ public abstract class PlaybackServiceMediaPlayer {
/**
* @see #setPlayerStatus(PlayerStatus, Playable, int)
*/
- final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
+ protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
setPlayerStatus(newStatus, newMedia, INVALID_TIME);
}
@@ -350,8 +353,6 @@ public abstract class PlaybackServiceMediaPlayer {
void onMediaChanged(boolean reloadUI);
- boolean onMediaPlayerInfo(int code, @StringRes int resourceId);
-
void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext);
void onPlaybackStart(@NonNull Playable playable, int position);
@@ -360,7 +361,12 @@ public abstract class PlaybackServiceMediaPlayer {
Playable getNextInQueue(Playable currentMedia);
+ @Nullable
+ Playable findMedia(@NonNull String url);
+
void onPlaybackEnded(MediaType mediaType, boolean stopPlaying);
+
+ void ensureMediaInfoLoaded(@NonNull Playable media);
}
/**
@@ -371,7 +377,7 @@ public abstract class PlaybackServiceMediaPlayer {
public PlayerStatus playerStatus;
public Playable playable;
- PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
+ public PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
this.oldPlayerStatus = oldPlayerStatus;
this.playerStatus = playerStatus;
this.playable = playable;
diff --git a/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java
new file mode 100644
index 000000000..d995ae21f
--- /dev/null
+++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/PlayerStatus.java
@@ -0,0 +1,33 @@
+package de.danoeh.antennapod.playback.base;
+
+public enum PlayerStatus {
+ INDETERMINATE(0), // player is currently changing its state, listeners should wait until the state is left
+ ERROR(-1),
+ PREPARING(19),
+ PAUSED(30),
+ PLAYING(40),
+ STOPPED(5),
+ PREPARED(20),
+ SEEKING(29),
+ INITIALIZING(9), // playback service is loading the Playable's metadata
+ INITIALIZED(10); // playback service was started, data source of media player was set
+
+ private final int statusValue;
+ private static final PlayerStatus[] fromOrdinalLookup;
+
+ static {
+ fromOrdinalLookup = PlayerStatus.values();
+ }
+
+ PlayerStatus(int val) {
+ statusValue = val;
+ }
+
+ public static PlayerStatus fromOrdinal(int o) {
+ return fromOrdinalLookup[o];
+ }
+
+ public boolean isAtLeast(PlayerStatus other) {
+ return other == null || this.statusValue >= other.statusValue;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtils.java
index 813c6d0f7..7d694f38b 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java
+++ b/playback/base/src/main/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtils.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.util;
+package de.danoeh.antennapod.playback.base;
import java.util.concurrent.TimeUnit;
diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java b/playback/base/src/test/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtilTest.java
index dc64f6ae0..b122971b2 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java
+++ b/playback/base/src/test/java/de/danoeh/antennapod/playback/base/RewindAfterPauseUtilTest.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.core.util;
+package de.danoeh.antennapod.playback.base;
import org.junit.Test;
diff --git a/playback/cast/README.md b/playback/cast/README.md
new file mode 100644
index 000000000..29eb8eacd
--- /dev/null
+++ b/playback/cast/README.md
@@ -0,0 +1,3 @@
+# :playback:cast
+
+This module provides Chromecast support for the Google Play version of the app.
diff --git a/playback/cast/build.gradle b/playback/cast/build.gradle
new file mode 100644
index 000000000..c51354838
--- /dev/null
+++ b/playback/cast/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: "com.android.library"
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+dependencies {
+ implementation project(':event')
+ implementation project(':model')
+ implementation project(':playback:base')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "org.greenrobot:eventbus:$eventbusVersion"
+ annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbusVersion"
+
+ playApi 'androidx.mediarouter:mediarouter:1.2.5'
+ playApi 'com.google.android.gms:play-services-cast-framework:20.0.0'
+}
diff --git a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
index 98d506f65..36524b236 100644
--- a/app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
+++ b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.activity;
+package de.danoeh.antennapod.playback.cast;
import androidx.appcompat.app.AppCompatActivity;
diff --git a/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPsmp.java b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
new file mode 100644
index 000000000..7f5e0f2ab
--- /dev/null
+++ b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
@@ -0,0 +1,17 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+
+/**
+ * Stub implementation of CastPsmp for Free build flavour
+ */
+public class CastPsmp {
+ @Nullable
+ public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
+ @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) {
+ return null;
+ }
+}
diff --git a/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastStateListener.java b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastStateListener.java
new file mode 100644
index 000000000..60cc7dd2c
--- /dev/null
+++ b/playback/cast/src/free/java/de/danoeh/antennapod/playback/cast/CastStateListener.java
@@ -0,0 +1,15 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.Context;
+
+public class CastStateListener {
+
+ public CastStateListener(Context context) {
+ }
+
+ public void destroy() {
+ }
+
+ public void onSessionStartedOrEnded() {
+ }
+}
diff --git a/playback/cast/src/main/AndroidManifest.xml b/playback/cast/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..58c2b9396
--- /dev/null
+++ b/playback/cast/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="de.danoeh.antennapod.playback.cast" />
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
new file mode 100644
index 000000000..2cebde6a3
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastEnabledActivity.java
@@ -0,0 +1,35 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.os.Bundle;
+import android.view.Menu;
+import androidx.appcompat.app.AppCompatActivity;
+import com.google.android.gms.cast.framework.CastButtonFactory;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+
+/**
+ * Activity that allows for showing the MediaRouter button whenever there's a cast device in the
+ * network.
+ */
+public abstract class CastEnabledActivity extends AppCompatActivity {
+ private static final String TAG = "CastEnabledActivity";
+ private boolean canCast = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ canCast = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS;
+ if (canCast) {
+ CastContext.getSharedInstance(this);
+ }
+ }
+
+ public void requestCastButton(Menu menu) {
+ if (!canCast) {
+ return;
+ }
+ getMenuInflater().inflate(R.menu.cast_button, menu);
+ CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, R.id.media_route_menu_item);
+ }
+}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java
new file mode 100644
index 000000000..37885bdd0
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastOptionsProvider.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import com.google.android.gms.cast.CastMediaControlIntent;
+import com.google.android.gms.cast.framework.CastOptions;
+import com.google.android.gms.cast.framework.OptionsProvider;
+import com.google.android.gms.cast.framework.SessionProvider;
+
+import java.util.List;
+
+@SuppressWarnings("unused")
+public class CastOptionsProvider implements OptionsProvider {
+ @Override
+ @NonNull
+ public CastOptions getCastOptions(@NonNull Context context) {
+ return new CastOptions.Builder()
+ .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
+ .build();
+ }
+
+ @Override
+ public List<SessionProvider> getAdditionalSessionProviders(@NonNull Context context) {
+ return null;
+ }
+} \ No newline at end of file
diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
index 38b469e8e..8e74154e8 100644
--- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastPsmp.java
@@ -1,66 +1,77 @@
-package de.danoeh.antennapod.core.service.playback;
+package de.danoeh.antennapod.playback.cast;
import android.content.Context;
-import android.media.MediaPlayer;
import androidx.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
-import com.google.android.gms.cast.Cast;
-import com.google.android.gms.cast.CastStatusCodes;
-import com.google.android.gms.cast.MediaInfo;
-import com.google.android.gms.cast.MediaStatus;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
-import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
-
-import de.danoeh.antennapod.core.cast.MediaInfoCreator;
-
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean;
-import de.danoeh.antennapod.core.R;
-import de.danoeh.antennapod.core.cast.CastConsumer;
-import de.danoeh.antennapod.core.cast.CastManager;
-import de.danoeh.antennapod.core.cast.CastUtils;
-import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
-import de.danoeh.antennapod.core.storage.DBReader;
-import de.danoeh.antennapod.model.playback.RemoteMedia;
+import androidx.annotation.Nullable;
+import com.google.android.gms.cast.MediaError;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaLoadOptions;
+import com.google.android.gms.cast.MediaLoadRequestData;
+import com.google.android.gms.cast.MediaSeekOptions;
+import com.google.android.gms.cast.MediaStatus;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.cast.framework.CastState;
+import com.google.android.gms.cast.framework.media.RemoteMediaClient;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import de.danoeh.antennapod.event.PlayerErrorEvent;
+import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.MediaType;
-import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.model.playback.RemoteMedia;
+import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.playback.base.PlayerStatus;
+import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils;
+import org.greenrobot.eventbus.EventBus;
/**
* Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
*/
-public class RemotePSMP extends PlaybackServiceMediaPlayer {
-
- public static final String TAG = "RemotePSMP";
+public class CastPsmp extends PlaybackServiceMediaPlayer {
- public static final int CAST_ERROR = 3001;
-
- public static final int CAST_ERROR_PRIORITY_HIGH = 3005;
-
- private final CastManager castMgr;
+ public static final String TAG = "CastPSMP";
private volatile Playable media;
private volatile MediaType mediaType;
private volatile MediaInfo remoteMedia;
private volatile int remoteState;
+ private final CastContext castContext;
+ private final RemoteMediaClient remoteMediaClient;
private final AtomicBoolean isBuffering;
private final AtomicBoolean startWhenPrepared;
- public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) {
+ @Nullable
+ public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
+ @NonNull PSMPCallback callback) {
+ if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
+ return null;
+ }
+ if (CastContext.getSharedInstance(context).getCastState() == CastState.CONNECTED) {
+ return new CastPsmp(context, callback);
+ } else {
+ return null;
+ }
+ }
+
+ public CastPsmp(@NonNull Context context, @NonNull PSMPCallback callback) {
super(context, callback);
- castMgr = CastManager.getInstance();
+ castContext = CastContext.getSharedInstance(context);
+ remoteMediaClient = castContext.getSessionManager().getCurrentCastSession().getRemoteMediaClient();
+ remoteMediaClient.registerCallback(remoteMediaClientCallback);
media = null;
mediaType = null;
startWhenPrepared = new AtomicBoolean(false);
@@ -68,94 +79,48 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
remoteState = MediaStatus.PLAYER_STATE_UNKNOWN;
}
- public void init() {
- try {
- if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) {
- onRemoteMediaPlayerStatusUpdated();
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to do initial check for loaded media", e);
- }
-
- castMgr.addCastConsumer(castConsumer);
- }
-
- private CastConsumer castConsumer = new DefaultCastConsumer() {
- @Override
- public void onRemoteMediaPlayerMetadataUpdated() {
- RemotePSMP.this.onRemoteMediaPlayerStatusUpdated();
- }
-
+ private final RemoteMediaClient.Callback remoteMediaClientCallback = new RemoteMediaClient.Callback() {
@Override
- public void onRemoteMediaPlayerStatusUpdated() {
- RemotePSMP.this.onRemoteMediaPlayerStatusUpdated();
+ public void onMetadataUpdated() {
+ super.onMetadataUpdated();
+ onRemoteMediaPlayerStatusUpdated();
}
@Override
- public void onMediaLoadResult(int statusCode) {
- if (playerStatus == PlayerStatus.PREPARING) {
- if (statusCode == CastStatusCodes.SUCCESS) {
- setPlayerStatus(PlayerStatus.PREPARED, media);
- if (media.getDuration() == 0) {
- Log.d(TAG, "Setting duration of media");
- try {
- media.setDuration((int) castMgr.getMediaDuration());
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to get remote media's duration");
- }
- }
- } else if (statusCode != CastStatusCodes.REPLACED){
- Log.d(TAG, "Remote media failed to load");
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
- }
- } else {
- Log.d(TAG, "onMediaLoadResult called, but Player Status wasn't in preparing state, so we ignore the result");
- }
+ public void onPreloadStatusUpdated() {
+ super.onPreloadStatusUpdated();
+ onRemoteMediaPlayerStatusUpdated();
}
@Override
- public void onApplicationStatusChanged(String appStatus) {
- if (playerStatus != PlayerStatus.PLAYING) {
- Log.d(TAG, "onApplicationStatusChanged, but no media was playing");
- return;
- }
- boolean playbackEnded = false;
- try {
- int standbyState = castMgr.getApplicationStandbyState();
- Log.d(TAG, "standbyState: " + standbyState);
- playbackEnded = standbyState == Cast.STANDBY_STATE_YES;
- } catch (IllegalStateException e) {
- Log.d(TAG, "unable to get standbyState on onApplicationStatusChanged()");
- }
- if (playbackEnded) {
- // This is an unconventional thing to occur...
- Log.w(TAG, "Somehow, Chromecast went from playing directly to standby mode");
- endPlayback(false, false, true, true);
- }
+ public void onStatusUpdated() {
+ super.onStatusUpdated();
+ onRemoteMediaPlayerStatusUpdated();
}
@Override
- public void onFailed(int resourceId, int statusCode) {
- callback.onMediaPlayerInfo(CAST_ERROR, resourceId);
+ public void onMediaError(@NonNull MediaError mediaError) {
+ EventBus.getDefault().post(new PlayerErrorEvent(mediaError.getReason()));
}
};
private void setBuffering(boolean buffering) {
if (buffering && isBuffering.compareAndSet(false, true)) {
- callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0);
+ EventBus.getDefault().post(BufferUpdateEvent.started());
} else if (!buffering && isBuffering.compareAndSet(true, false)) {
- callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0);
+ EventBus.getDefault().post(BufferUpdateEvent.ended());
}
}
- private Playable localVersion(MediaInfo info){
- if (info == null) {
+ private Playable localVersion(MediaInfo info) {
+ if (info == null || info.getMetadata() == null) {
return null;
}
if (CastUtils.matches(info, media)) {
return media;
}
- return CastUtils.getPlayable(info, true);
+ String streamUrl = info.getMetadata().getString(CastUtils.KEY_STREAM_URL);
+ return streamUrl == null ? CastUtils.makeRemoteMedia(info) : callback.findMedia(streamUrl);
}
private MediaInfo remoteVersion(Playable playable) {
@@ -166,7 +131,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
return remoteMedia;
}
if (playable instanceof FeedMedia) {
- return CastUtils.convertFromFeedMedia((FeedMedia) playable);
+ return MediaInfoCreator.from((FeedMedia) playable);
}
if (playable instanceof RemoteMedia) {
return MediaInfoCreator.from((RemoteMedia) playable);
@@ -175,7 +140,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
}
private void onRemoteMediaPlayerStatusUpdated() {
- MediaStatus status = castMgr.getMediaStatus();
+ MediaStatus status = remoteMediaClient.getMediaStatus();
if (status == null) {
Log.d(TAG, "Received null MediaStatus");
return;
@@ -206,8 +171,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
remoteState = state;
}
- if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING &&
- state != MediaStatus.PLAYER_STATE_IDLE) {
+ if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING
+ && state != MediaStatus.PLAYER_STATE_IDLE) {
callback.onPlaybackPause(null, INVALID_TIME);
// We don't want setPlayerStatus to handle the onPlaybackPause callback
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
@@ -230,9 +195,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position);
break;
case MediaStatus.PLAYER_STATE_BUFFERING:
- setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING) ?
- PlayerStatus.PREPARING : PlayerStatus.SEEKING,
- currentMedia,
+ setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING)
+ ? PlayerStatus.PREPARING : PlayerStatus.SEEKING, currentMedia,
currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
break;
case MediaStatus.PLAYER_STATE_IDLE:
@@ -271,11 +235,13 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
endPlayback(true, false, true, true);
return;
case MediaStatus.IDLE_REASON_ERROR:
- Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode...");
- callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH,
- R.string.cast_failed_media_error_skipping);
+ Log.w(TAG, "Got an error status from the Chromecast. "
+ + "Skipping, if possible, to the next episode...");
+ EventBus.getDefault().post(new PlayerErrorEvent("Chromecast error code 1"));
endPlayback(false, false, true, true);
return;
+ default:
+ return;
}
break;
case MediaStatus.PLAYER_STATE_UNKNOWN:
@@ -284,7 +250,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
}
break;
default:
- Log.wtf(TAG, "Remote media state undetermined!");
+ Log.w(TAG, "Remote media state undetermined!");
}
if (mediaChanged) {
callback.onMediaChanged(true);
@@ -295,25 +261,29 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
}
@Override
- public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ public void playMediaObject(@NonNull final Playable playable, final boolean stream,
+ final boolean startWhenPrepared, final boolean prepareImmediately) {
Log.d(TAG, "playMediaObject() called");
playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
}
/**
- * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
+ * 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.
*
* @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 (!CastUtils.isCastable(playable)) {
+ private void playMediaObject(@NonNull final Playable playable, final boolean forceReset,
+ final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ if (!CastUtils.isCastable(playable, castContext.getSessionManager().getCurrentCastSession())) {
Log.d(TAG, "media provided is not compatible with cast device");
- callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable);
+ EventBus.getDefault().post(new PlayerErrorEvent("Media not compatible with cast device"));
Playable nextPlayable = playable;
do {
nextPlayable = callback.getNextInQueue(nextPlayable);
- } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable));
+ } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable,
+ castContext.getSessionManager().getCurrentCastSession()));
if (nextPlayable != null) {
playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately);
}
@@ -328,14 +298,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
return;
} else {
// set temporarily to pause in order to update list with current position
- boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
- int position = media.getPosition();
- try {
- isPlaying = castMgr.isRemoteMediaPlaying();
- position = (int) castMgr.getCurrentMediaPosition();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine whether media was playing, falling back to stored player status", e);
- }
+ boolean isPlaying = remoteMediaClient.isPlaying();
+ int position = (int) remoteMediaClient.getApproximateStreamPosition();
if (isPlaying) {
callback.onPlaybackPause(media, position);
}
@@ -343,7 +307,6 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
final Playable oldMedia = media;
callback.onPostPlayback(oldMedia, false, false, true);
}
-
setPlayerStatus(PlayerStatus.INDETERMINATE, null);
}
}
@@ -353,9 +316,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
this.mediaType = media.getMediaType();
this.startWhenPrepared.set(startWhenPrepared);
setPlayerStatus(PlayerStatus.INITIALIZING, media);
- if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
- ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
- }
+ callback.ensureMediaInfoLoaded(media);
callback.onMediaChanged(true);
setPlayerStatus(PlayerStatus.INITIALIZED, media);
if (prepareImmediately) {
@@ -365,29 +326,16 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public void resume() {
- try {
- if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
- int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
+ int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
media.getPosition(),
media.getLastPlayedTime());
- castMgr.play(newPosition);
- } else {
- castMgr.play();
- }
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to resume remote playback", e);
- }
+ seekTo(newPosition);
+ remoteMediaClient.play();
}
@Override
public void pause(boolean abandonFocus, boolean reinit) {
- try {
- if (castMgr.isRemoteMediaPlaying()) {
- castMgr.pause();
- }
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to pause", e);
- }
+ remoteMediaClient.pause();
}
@Override
@@ -395,18 +343,16 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
if (playerStatus == PlayerStatus.INITIALIZED) {
Log.d(TAG, "Preparing media player");
setPlayerStatus(PlayerStatus.PREPARING, media);
- try {
- int position = media.getPosition();
- if (position > 0) {
- position = RewindAfterPauseUtils.calculatePositionWithRewind(
- position,
- media.getLastPlayedTime());
- }
- castMgr.loadMedia(remoteMedia, startWhenPrepared.get(), position);
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Error loading media", e);
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
+ int position = media.getPosition();
+ if (position > 0) {
+ position = RewindAfterPauseUtils.calculatePositionWithRewind(
+ position,
+ media.getLastPlayedTime());
}
+ remoteMediaClient.load(new MediaLoadRequestData.Builder()
+ .setMediaInfo(remoteMedia)
+ .setAutoplay(startWhenPrepared.get())
+ .setCurrentTime(position).build());
}
}
@@ -422,19 +368,9 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public void seekTo(int t) {
- //TODO check other seek implementations and see if there's no issue with sending too many seek commands to the remote media player
- try {
- if (castMgr.isRemoteMediaLoaded()) {
- setPlayerStatus(PlayerStatus.SEEKING, media);
- castMgr.seek(t);
- } else if (media != null && playerStatus == PlayerStatus.INITIALIZED){
- media.setPosition(t);
- startWhenPrepared.set(false);
- prepare();
- }
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to seek", e);
- }
+ new Exception("Seeking to " + t).printStackTrace();
+ remoteMediaClient.seek(new MediaSeekOptions.Builder()
+ .setPosition(t).build());
}
@Override
@@ -449,49 +385,19 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public int getDuration() {
- int retVal = INVALID_TIME;
- boolean prepared;
- try {
- prepared = castMgr.isRemoteMediaLoaded();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to check if remote media is loaded", e);
- prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
- }
- if (prepared) {
- try {
- retVal = (int) castMgr.getMediaDuration();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine remote media's duration", e);
- }
- }
- if(retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
+ int retVal = (int) remoteMediaClient.getStreamDuration();
+ if (retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
retVal = media.getDuration();
}
- Log.d(TAG, "getDuration() -> " + retVal);
return retVal;
}
@Override
public int getPosition() {
- int retVal = INVALID_TIME;
- boolean prepared;
- try {
- prepared = castMgr.isRemoteMediaLoaded();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to check if remote media is loaded", e);
- prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
- }
- if (prepared) {
- try {
- retVal = (int) castMgr.getCurrentMediaPosition();
- } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to determine remote media's position", e);
- }
- }
- if(retVal <= 0 && media != null && media.getPosition() >= 0) {
+ int retVal = (int) remoteMediaClient.getApproximateStreamPosition();
+ if (retVal <= 0 && media != null && media.getPosition() >= 0) {
retVal = media.getPosition();
}
- Log.d(TAG, "getPosition() -> " + retVal);
return retVal;
}
@@ -507,29 +413,21 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public void setPlaybackParams(float speed, boolean skipSilence) {
- //Can be safely ignored as neither set speed not skipSilence is supported
+ double playbackRate = (float) Math.max(MediaLoadOptions.PLAYBACK_RATE_MIN,
+ Math.min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed));
+ remoteMediaClient.setPlaybackRate(playbackRate);
}
@Override
public float getPlaybackSpeed() {
- return 1;
+ MediaStatus status = remoteMediaClient.getMediaStatus();
+ return status != null ? (float) status.getPlaybackRate() : 1.0f;
}
@Override
public void setVolume(float volumeLeft, float volumeRight) {
Log.d(TAG, "Setting the Stream volume on Remote Media Player");
- double volume = (volumeLeft+volumeRight)/2;
- if (volume > 1.0) {
- volume = 1.0;
- }
- if (volume < 0.0) {
- volume = 0.0;
- }
- try {
- castMgr.setStreamVolume(volume);
- } catch (TransientNetworkDisconnectionException | NoConnectionException | CastException e) {
- Log.e(TAG, "Unable to set the volume", e);
- }
+ remoteMediaClient.setStreamVolume(volumeLeft);
}
@Override
@@ -554,7 +452,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override
public void shutdown() {
- castMgr.removeCastConsumer(castConsumer);
+ remoteMediaClient.unregisterCallback(remoteMediaClientCallback);
}
@Override
@@ -626,7 +524,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
boolean playNextEpisode = isPlaying && nextMedia != null;
if (playNextEpisode) {
Log.d(TAG, "Playback of next episode will start immediately.");
- } else if (nextMedia == null){
+ } else if (nextMedia == null) {
Log.d(TAG, "No more episodes available to play");
} else {
Log.d(TAG, "Loading next episode, but not playing automatically.");
@@ -636,45 +534,34 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode);
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing
media = null;
- playMediaObject(nextMedia, false, true /*TODO for now we always stream*/, playNextEpisode, playNextEpisode);
+ playMediaObject(nextMedia, false, true, playNextEpisode, playNextEpisode);
}
}
if (shouldContinue || toStoppedState) {
- boolean shouldPostProcess = true;
if (nextMedia == null) {
- try {
- castMgr.stop();
- shouldPostProcess = false;
- } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
- Log.e(TAG, "Unable to stop playback", e);
- callback.onPlaybackEnded(null, true);
- stop();
- }
- }
- if (shouldPostProcess) {
+ remoteMediaClient.stop();
// Otherwise we rely on the chromecast callback to tell us the playback has stopped.
- callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, nextMedia != null);
+ callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, false);
+ } else {
+ callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, true);
}
} else if (isPlaying) {
callback.onPlaybackPause(currentMedia,
currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
}
- FutureTask<?> future = new FutureTask<>(() -> {}, null);
+ FutureTask<?> future = new FutureTask<>(() -> { }, null);
future.run();
return future;
}
- private void stop() {
- 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 false;
}
+
+ @Override
+ public boolean isCasting() {
+ return true;
+ }
}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastStateListener.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastStateListener.java
new file mode 100644
index 000000000..39f54b11c
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastStateListener.java
@@ -0,0 +1,69 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.cast.framework.CastSession;
+import com.google.android.gms.cast.framework.SessionManagerListener;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+
+public class CastStateListener implements SessionManagerListener<CastSession> {
+ private final CastContext castContext;
+
+ public CastStateListener(Context context) {
+ if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
+ castContext = null;
+ return;
+ }
+ castContext = CastContext.getSharedInstance(context);
+ castContext.getSessionManager().addSessionManagerListener(this, CastSession.class);
+ }
+
+ public void destroy() {
+ if (castContext != null) {
+ castContext.getSessionManager().removeSessionManagerListener(this, CastSession.class);
+ }
+ }
+
+ @Override
+ public void onSessionStarting(@NonNull CastSession castSession) {
+ }
+
+ @Override
+ public void onSessionStarted(@NonNull CastSession session, @NonNull String sessionId) {
+ onSessionStartedOrEnded();
+ }
+
+ @Override
+ public void onSessionStartFailed(@NonNull CastSession castSession, int i) {
+ }
+
+ @Override
+ public void onSessionEnding(@NonNull CastSession castSession) {
+ }
+
+ @Override
+ public void onSessionResumed(@NonNull CastSession session, boolean wasSuspended) {
+ }
+
+ @Override
+ public void onSessionResumeFailed(@NonNull CastSession castSession, int i) {
+ }
+
+ @Override
+ public void onSessionSuspended(@NonNull CastSession castSession, int i) {
+ }
+
+ @Override
+ public void onSessionEnded(@NonNull CastSession session, int error) {
+ onSessionStartedOrEnded();
+ }
+
+ @Override
+ public void onSessionResuming(@NonNull CastSession castSession, @NonNull String s) {
+ }
+
+ public void onSessionStartedOrEnded() {
+ }
+}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastUtils.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastUtils.java
new file mode 100644
index 000000000..312b6b2f9
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/CastUtils.java
@@ -0,0 +1,181 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.content.ContentResolver;
+import android.util.Log;
+import android.text.TextUtils;
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.framework.CastSession;
+import com.google.android.gms.common.images.WebImage;
+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.playback.Playable;
+import de.danoeh.antennapod.model.playback.RemoteMedia;
+
+import java.util.List;
+
+/**
+ * Helper functions for Cast support.
+ */
+public class CastUtils {
+ private CastUtils() {
+ }
+
+ private static final String TAG = "CastUtils";
+
+ public static final String KEY_MEDIA_ID = "de.danoeh.antennapod.core.cast.MediaId";
+
+ public static final String KEY_EPISODE_IDENTIFIER = "de.danoeh.antennapod.core.cast.EpisodeId";
+ public static final String KEY_EPISODE_LINK = "de.danoeh.antennapod.core.cast.EpisodeLink";
+ public static final String KEY_STREAM_URL = "de.danoeh.antennapod.core.cast.StreamUrl";
+ public static final String KEY_FEED_URL = "de.danoeh.antennapod.core.cast.FeedUrl";
+ public static final String KEY_FEED_WEBSITE = "de.danoeh.antennapod.core.cast.FeedWebsite";
+ public static final String KEY_EPISODE_NOTES = "de.danoeh.antennapod.core.cast.EpisodeNotes";
+
+ /**
+ * The field <code>AntennaPod.FormatVersion</code> specifies which version of MediaMetaData
+ * fields we're using. Future implementations should try to be backwards compatible with earlier
+ * versions, and earlier versions should be forward compatible until the version indicated by
+ * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code>. If an update makes the format unreadable for
+ * an earlier version, then its version number should be greater than the
+ * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code> value set on the earlier one, so that it
+ * doesn't try to parse the object.
+ */
+ public static final String KEY_FORMAT_VERSION = "de.danoeh.antennapod.core.cast.FormatVersion";
+ public static final int FORMAT_VERSION_VALUE = 1;
+ public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999;
+
+ public static boolean isCastable(Playable media, CastSession castSession) {
+ if (media == null || castSession == null || castSession.getCastDevice() == null) {
+ return false;
+ }
+ if (media instanceof FeedMedia || media instanceof RemoteMedia) {
+ String url = media.getStreamUrl();
+ if (url == null || url.isEmpty()) {
+ return false;
+ }
+ if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
+ return false; // Local feed
+ }
+ switch (media.getMediaType()) {
+ case AUDIO:
+ return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT);
+ case VIDEO:
+ return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT);
+ default:
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}.
+ * @return {@link Playable} object in a format proper for casting.
+ */
+ public static Playable makeRemoteMedia(MediaInfo media) {
+ MediaMetadata metadata = media.getMetadata();
+ int version = metadata.getInt(KEY_FORMAT_VERSION);
+ if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) {
+ Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this"
+ + "version of AntennaPod CastUtils, curVer=" + FORMAT_VERSION_VALUE
+ + ", object version=" + version);
+ return null;
+ }
+ List<WebImage> imageList = metadata.getImages();
+ String imageUrl = null;
+ if (!imageList.isEmpty()) {
+ imageUrl = imageList.get(0).getUrl().toString();
+ }
+ String notes = metadata.getString(KEY_EPISODE_NOTES);
+ RemoteMedia result = new RemoteMedia(media.getContentId(),
+ metadata.getString(KEY_EPISODE_IDENTIFIER),
+ metadata.getString(KEY_FEED_URL),
+ metadata.getString(MediaMetadata.KEY_SUBTITLE),
+ metadata.getString(MediaMetadata.KEY_TITLE),
+ metadata.getString(KEY_EPISODE_LINK),
+ metadata.getString(MediaMetadata.KEY_ARTIST),
+ imageUrl,
+ metadata.getString(KEY_FEED_WEBSITE),
+ media.getContentType(),
+ metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime(),
+ notes);
+ if (result.getDuration() == 0 && media.getStreamDuration() > 0) {
+ result.setDuration((int) media.getStreamDuration());
+ }
+ return result;
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they
+ * represent the same podcast episode.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link FeedMedia} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, FeedMedia media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
+ return false;
+ }
+ MediaMetadata metadata = info.getMetadata();
+ FeedItem fi = media.getItem();
+ if (fi == null || metadata == null
+ || !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) {
+ return false;
+ }
+ Feed feed = fi.getFeed();
+ return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url());
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they
+ * represent the same podcast episode.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link RemoteMedia} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, RemoteMedia media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
+ return false;
+ }
+ MediaMetadata metadata = info.getMetadata();
+ return metadata != null
+ && TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier())
+ && TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl());
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they
+ * represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
+ * and want to avoid unnecessary conversions.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link Playable} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, Playable media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (media instanceof RemoteMedia) {
+ return matches(info, (RemoteMedia) media);
+ }
+ return media instanceof FeedMedia && matches(info, (FeedMedia) media);
+ }
+}
diff --git a/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java
new file mode 100644
index 000000000..dd408d4a7
--- /dev/null
+++ b/playback/cast/src/play/java/de/danoeh/antennapod/playback/cast/MediaInfoCreator.java
@@ -0,0 +1,135 @@
+package de.danoeh.antennapod.playback.cast;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.common.images.WebImage;
+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.playback.RemoteMedia;
+import java.util.Calendar;
+
+public class MediaInfoCreator {
+ public static MediaInfo from(RemoteMedia media) {
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
+
+ metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
+ metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle());
+ if (!TextUtils.isEmpty(media.getImageLocation())) {
+ metadata.addImage(new WebImage(Uri.parse(media.getImageLocation())));
+ }
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(media.getPubDate());
+ metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
+ if (!TextUtils.isEmpty(media.getFeedAuthor())) {
+ metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor());
+ }
+ if (!TextUtils.isEmpty(media.getFeedUrl())) {
+ metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl());
+ }
+ if (!TextUtils.isEmpty(media.getFeedLink())) {
+ metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink());
+ }
+ if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier());
+ } else {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl());
+ }
+ if (!TextUtils.isEmpty(media.getEpisodeLink())) {
+ metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink());
+ }
+ String notes = media.getNotes();
+ if (notes != null) {
+ metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
+ }
+ // Default id value
+ metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
+ metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
+ metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
+
+ MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl())
+ .setContentType(media.getMimeType())
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setMetadata(metadata);
+ if (media.getDuration() > 0) {
+ builder.setStreamDuration(media.getDuration());
+ }
+ return builder.build();
+ }
+
+ /**
+ * Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device.
+ * Before using this method, one should make sure isCastable(Playable) returns
+ * {@code true}. This method should not run on the main thread.
+ *
+ * @param media The {@link FeedMedia} object to be converted.
+ * @return {@link MediaInfo} object in a format proper for casting.
+ */
+ public static MediaInfo from(FeedMedia media) {
+ if (media == null) {
+ return null;
+ }
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
+ if (media.getItem() == null) {
+ throw new IllegalStateException("item is null");
+ //media.setItem(DBReader.getFeedItem(media.getItemId()));
+ }
+ FeedItem feedItem = media.getItem();
+ if (feedItem != null) {
+ metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
+ String subtitle = media.getFeedTitle();
+ if (subtitle != null) {
+ metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
+ }
+
+ // Manual because cast does not support embedded images
+ String url = feedItem.getImageUrl() == null ? feedItem.getFeed().getImageUrl() : feedItem.getImageUrl();
+ if (!TextUtils.isEmpty(url)) {
+ metadata.addImage(new WebImage(Uri.parse(url)));
+ }
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(media.getItem().getPubDate());
+ metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
+ Feed feed = feedItem.getFeed();
+ if (feed != null) {
+ if (!TextUtils.isEmpty(feed.getAuthor())) {
+ metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor());
+ }
+ if (!TextUtils.isEmpty(feed.getDownload_url())) {
+ metadata.putString(CastUtils.KEY_FEED_URL, feed.getDownload_url());
+ }
+ if (!TextUtils.isEmpty(feed.getLink())) {
+ metadata.putString(CastUtils.KEY_FEED_WEBSITE, feed.getLink());
+ }
+ }
+ if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier());
+ } else {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl());
+ }
+ if (!TextUtils.isEmpty(feedItem.getLink())) {
+ metadata.putString(CastUtils.KEY_EPISODE_LINK, feedItem.getLink());
+ }
+ }
+ // This field only identifies the id on the device that has the original version.
+ // Idea is to perhaps, on a first approach, check if the version on the local DB with the
+ // same id matches the remote object, and if not then search for episode and feed identifiers.
+ // This at least should make media recognition for a single device much quicker.
+ metadata.putInt(CastUtils.KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue());
+ // A way to identify different casting media formats in case we change it in the future and
+ // senders with different versions share a casting device.
+ metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
+ metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
+
+ MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl())
+ .setContentType(media.getMime_type())
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setMetadata(metadata);
+ if (media.getDuration() > 0) {
+ builder.setStreamDuration(media.getDuration());
+ }
+ return builder.build();
+ }
+}
diff --git a/playback/cast/src/play/res/menu/cast_button.xml b/playback/cast/src/play/res/menu/cast_button.xml
new file mode 100644
index 000000000..6e65bce18
--- /dev/null
+++ b/playback/cast/src/play/res/menu/cast_button.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/media_route_menu_item"
+ android:title=""
+ app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
+ app:showAsAction="always" />
+
+</menu>
diff --git a/settings.gradle b/settings.gradle
index 80fce468f..c7f5e6449 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -10,6 +10,9 @@ include ':net:sync:model'
include ':parser:feed'
include ':parser:media'
+include ':playback:base'
+include ':playback:cast'
+
include ':ui:app-start-intent'
include ':ui:common'
include ':ui:png-icons'
diff --git a/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml b/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml
deleted file mode 100644
index 3e3accd0b..000000000
--- a/ui/png-icons/src/main/res/drawable/ic_notification_cast_off.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:height="30dp" android:viewportHeight="24.0"
- android:viewportWidth="24.0" android:width="30dp">
- <path android:fillColor="#FFFFFFFF" android:pathData="M1.6,1.27L0.25,2.75L1.41,3.8C1.16,4.13 1,4.55 1,5V8H3V5.23L18.2,19H14V21H20.41L22.31,22.72L23.65,21.24M6.5,3L8.7,5H21V16.14L23,17.95V5C23,3.89 22.1,3 21,3M1,10V12A9,9 0 0,1 10,21H12C12,14.92 7.08,10 1,10M1,14V16A5,5 0 0,1 6,21H8A7,7 0 0,0 1,14M1,18V21H4A3,3 0 0,0 1,18Z" />
-</vector> \ No newline at end of file