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 { @@ -42,14 +43,6 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa originalCallback.onMediaChanged(reloadUI); } - @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) { @@ -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 @@ + 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 @@ - - - - - \ 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"/> - 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 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 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 @@ - - - - - - - - - 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 @@ - - - @string/pref_cast_message_free_flavor - diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java similarity index 97% rename from core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java rename to 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(), @@ -872,11 +879,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { updateNotificationAndMediaSession(getPlayable()); } - @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) { @@ -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 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 @@ - - - @dimen/media_router_controller_playback_control_horizontal_spacing - 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 @@ - 0dp 64dp 12sp @@ -28,11 +27,5 @@ 64dp 12dp - 16dp - 12dp - 24dp - 8dp - 480dp - 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 @@ Proxy Set a network proxy No web browser found. - Chromecast support - Enable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV) - Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod Enqueue Downloaded Add downloaded episodes to the queue Built-in Android player (deprecated) @@ -664,7 +661,6 @@ Pause for Interruptions Resume playback after a phone call completes Resume after Call - AntennaPod has to be restarted for this change to take effect. Subscribe @@ -808,21 +804,6 @@ Number of columns - - Play on… - Disconnect the cast session - Media selected is not compatible with cast device - Failed to start the playback of media - Failed to stop the playback of media - Failed to pause the playback of media - Failed to set the volume - No connection to the cast device is present - 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. - Failed to sync up with the cast device - Failed to seek to the new position on the cast device - Receiver player has encountered a severe error - Error playing media. Skipping… - Errors News 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 - * - * 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). - *

- * 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()}. - *

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 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 true 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 true 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 true 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 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 true, playback starts after load - * @param position Where to start the playback (only used if autoPlay is true. - * 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 true, playback starts after load - * @param position Where to start the playback (only used if autoPlay is true). - * 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 true, playback starts after load - * @param position Where to start the playback (only used if autoPlay is true). - * 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. - *

- * 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. - *

- * 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 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 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 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: - *

    - *
  • {@link CastDevice#CAPABILITY_AUDIO_IN}
  • - *
  • {@link CastDevice#CAPABILITY_AUDIO_OUT}
  • - *
  • {@link CastDevice#CAPABILITY_VIDEO_IN}
  • - *
  • {@link CastDevice#CAPABILITY_VIDEO_OUT}
  • - *
- * @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 AntennaPod.FormatVersion 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 - * MAX_VERSION_FORWARD_COMPATIBILITY. If an update makes the format unreadable for - * an earlier version, then its version number should be greater than the - * MAX_VERSION_FORWARD_COMPATIBILITY 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 searchFeedMedia is set to false, this method should not run - * on the GUI thread. - * - * @param media The {@link MediaInfo} object to be converted. - * @param searchFeedMedia If set to true, 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 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 if there's a match, false 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 if there's a match, false 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 if there's a match, false 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; - -/** - *

Action Provider that extends {@link MediaRouteActionProvider} and allows the client to - * disable completely the button by calling {@link #setEnabled(boolean)}.

- * - *

It is disabled by default, so if a client wants to initially have it enabled it must call - * setEnabled(true).

- */ -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; - } - - /** - *

Sets whether the Media Router button should be allowed to become visible or not.

- * - *

It's invisible by default.

- */ - 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 @@ - - - @string/pref_cast_message_play_flavor - 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 @@ + 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 similarity index 93% rename from core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java rename to 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 similarity index 97% rename from core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java rename to 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 similarity index 98% rename from core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java rename to 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 similarity index 90% rename from app/src/free/java/de/danoeh/antennapod/activity/CastEnabledActivity.java rename to 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 @@ + 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 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 similarity index 57% rename from core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java rename to 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 class CastPsmp extends PlaybackServiceMediaPlayer { - public static final String TAG = "RemotePSMP"; - - 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() { + private final RemoteMediaClient.Callback remoteMediaClientCallback = new RemoteMediaClient.Callback() { @Override - public void onRemoteMediaPlayerMetadataUpdated() { - RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(); + public void onMetadataUpdated() { + super.onMetadataUpdated(); + onRemoteMediaPlayerStatusUpdated(); } @Override - public void onRemoteMediaPlayerStatusUpdated() { - RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(); + public void onPreloadStatusUpdated() { + super.onPreloadStatusUpdated(); + 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 onStatusUpdated() { + super.onStatusUpdated(); + 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); - } - } - - @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 { + 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 AntennaPod.FormatVersion 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 + * MAX_VERSION_FORWARD_COMPATIBILITY. If an update makes the format unreadable for + * an earlier version, then its version number should be greater than the + * MAX_VERSION_FORWARD_COMPATIBILITY 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 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 if there's a match, false 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 if there's a match, false 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 if there's a match, false 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 @@ + + + + + + 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 @@ - - - \ No newline at end of file