diff options
Diffstat (limited to 'core')
36 files changed, 122 insertions, 3545 deletions
diff --git a/core/build.gradle b/core/build.gradle index ce76fec70..b3954c879 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -27,6 +27,8 @@ dependencies { implementation project(':net:sync:model') implementation project(':parser:feed') implementation project(':parser:media') + implementation project(':playback:base') + implementation project(':playback:cast') implementation project(':ui:app-start-intent') implementation project(':ui:common') implementation project(':ui:png-icons') @@ -61,9 +63,6 @@ dependencies { implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" // Non-free dependencies: - playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1' - playApi 'androidx.mediarouter:mediarouter:1.0.0' - playApi "com.google.android.gms:play-services-cast:$playServicesVersion" playApi "com.google.android.support:wearable:$wearableSupportVersion" compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" diff --git a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java deleted file mode 100644 index 2e266c736..000000000 --- a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.danoeh.antennapod.core; - -/** - * Callbacks for Chromecast support on the core module - */ -public interface CastCallbacks { -} diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java deleted file mode 100644 index 837cb1bd0..000000000 --- a/core/src/free/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.content.Context; -import androidx.annotation.StringRes; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; - -/** - * Class intended to work along PlaybackService and provide support for different flavors. - */ -class PlaybackServiceFlavorHelper { - - private final PlaybackService.FlavorHelperCallback callback; - - PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) { - this.callback = callback; - } - - void initializeMediaPlayer(Context context) { - callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback())); - } - - void removeCastConsumer() { - // no-op - } - - boolean castDisconnect(boolean castDisconnect) { - return false; - } - - boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) { - return false; - } - - void registerWifiBroadcastReceiver() { - // no-op - } - - void unregisterWifiBroadcastReceiver() { - // no-op - } - - boolean onSharedPreference(String key) { - return false; - } - - void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) { - // no-op - } - - void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) { - // no-op - } -} diff --git a/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java new file mode 100644 index 000000000..373b24bc8 --- /dev/null +++ b/core/src/free/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; + +class WearMediaSession { + static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, + CharSequence name, int icon) { + // no-op + } + + static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) { + // no-op + } +} diff --git a/core/src/free/res/values/strings.xml b/core/src/free/res/values/strings.xml deleted file mode 100644 index fb49bbbe7..000000000 --- a/core/src/free/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <string name="pref_cast_message" translatable="false">@string/pref_cast_message_free_flavor</string> -</resources> diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java index 755bec14e..ac67fb042 100644 --- a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java @@ -30,8 +30,6 @@ public class ClientConfig { public static DownloadServiceCallbacks downloadServiceCallbacks; - public static CastCallbacks castCallbacks; - private static boolean initialized = false; public static synchronized void initialize(Context context) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java index 8d80ef32b..f0c61403f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -8,8 +8,8 @@ import android.util.Log; import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.greenrobot.eventbus.EventBus; import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java index 79363e872..7ce06a9fb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java @@ -40,6 +40,7 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.playback.IPlayer; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index 5648024de..34fc7d699 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -17,8 +17,9 @@ import androidx.media.AudioManagerCompat; import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.playback.BufferUpdateEvent; import de.danoeh.antennapod.event.playback.SpeedChangedEvent; -import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.MediaPlayerError; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.antennapod.audio.MediaPlayer; import java.io.File; @@ -39,7 +40,7 @@ import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.RewindAfterPauseUtils; +import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils; import de.danoeh.antennapod.core.util.playback.AudioPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.model.playback.Playable; @@ -148,7 +149,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } public LocalPSMP(@NonNull Context context, - @NonNull PSMPCallback callback) { + @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) { super(context, callback); this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); this.playerLock = new PlayerLock(); @@ -265,9 +266,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { LocalPSMP.this.startWhenPrepared.set(startWhenPrepared); setPlayerStatus(PlayerStatus.INITIALIZING, media); try { - if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) { - ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId())); - } + callback.ensureMediaInfoLoaded(media); callback.onMediaChanged(false); setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence()); if (stream) { @@ -1098,7 +1097,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { EventBus.getDefault().post(BufferUpdateEvent.ended()); return true; default: - return callback.onMediaPlayerInfo(what, 0); + return true; } } @@ -1148,4 +1147,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { executor.submit(r); } } + + @Override + public boolean isCasting() { + return false; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 60ccb5c9e..949c0ff9d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -37,6 +37,7 @@ import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; @@ -47,6 +48,10 @@ import de.danoeh.antennapod.event.playback.BufferUpdateEvent; import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; +import de.danoeh.antennapod.playback.cast.CastPsmp; +import de.danoeh.antennapod.playback.cast.CastStateListener; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -103,24 +108,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ private static final String TAG = "PlaybackService"; - /** - * Parcelable of type Playable. - */ public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - /** - * True if cast session should disconnect. - */ - public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect"; - /** - * True if media should be streamed. - */ public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream"; public static final String EXTRA_ALLOW_STREAM_THIS_TIME = "extra.de.danoeh.antennapod.core.service.allowStream"; public static final String EXTRA_ALLOW_STREAM_ALWAYS = "extra.de.danoeh.antennapod.core.service.allowStreamAlways"; - /** - * True if playback should be started immediately after media has been - * prepared. - */ public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared"; public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately"; @@ -200,10 +191,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { private PlaybackServiceMediaPlayer mediaPlayer; private PlaybackServiceTaskManager taskManager; - private PlaybackServiceFlavorHelper flavorHelper; private PlaybackServiceStateManager stateManager; private Disposable positionEventTimer; private PlaybackServiceNotificationBuilder notificationBuilder; + private CastStateListener castStateListener; private String autoSkippedFeedMediaId = null; @@ -280,7 +271,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { EventBus.getDefault().register(this); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); - flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback); PreferenceManager.getDefaultSharedPreferences(this) .registerOnSharedPreferenceChangeListener(prefListener); @@ -305,12 +295,36 @@ public class PlaybackService extends MediaBrowserServiceCompat { npe.printStackTrace(); } - flavorHelper.initializeMediaPlayer(PlaybackService.this); + recreateMediaPlayer(); mediaSession.setActive(true); - + castStateListener = new CastStateListener(this) { + @Override + public void onSessionStartedOrEnded() { + recreateMediaPlayer(); + } + }; EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED)); } + void recreateMediaPlayer() { + Playable media = null; + boolean wasPlaying = false; + if (mediaPlayer != null) { + media = mediaPlayer.getPlayable(); + wasPlaying = mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING; + mediaPlayer.pause(true, false); + mediaPlayer.shutdown(); + } + mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback); + if (mediaPlayer == null) { + mediaPlayer = new LocalPSMP(this, mediaPlayerCallback); // Cast not supported or not connected + } + if (media != null) { + mediaPlayer.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true); + } + isCasting = mediaPlayer.isCasting(); + } + @Override public void onDestroy() { super.onDestroy(); @@ -324,6 +338,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.stopForeground(!UserPreferences.isPersistNotify()); isRunning = false; currentMediaType = MediaType.UNKNOWN; + castStateListener.destroy(); cancelPositionObserver(); PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener); @@ -337,8 +352,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { unregisterReceiver(audioBecomingNoisy); unregisterReceiver(skipCurrentEpisodeReceiver); unregisterReceiver(pausePlayCurrentEpisodeReceiver); - flavorHelper.removeCastConsumer(); - flavorHelper.unregisterWifiBroadcastReceiver(); mediaPlayer.shutdown(); taskManager.shutdown(); } @@ -483,9 +496,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false); - final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false); Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); - if (keycode == -1 && playable == null && !castDisconnect) { + if (keycode == -1 && playable == null) { Log.e(TAG, "PlaybackService was started with no arguments"); stateManager.stopService(); return Service.START_NOT_STICKY; @@ -509,7 +521,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.stopService(); return Service.START_NOT_STICKY; } - } else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) { + } else { stateManager.validStartCommandWasReceived(); boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true); boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false); @@ -553,9 +565,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.stopService(); }); return Service.START_NOT_STICKY; - } else { - Log.d(TAG, "Did not handle intent to PlaybackService: " + intent); - Log.d(TAG, "Extras: " + intent.getExtras()); } } @@ -781,8 +790,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME); } - - @Override public WidgetUpdater.WidgetState requestWidgetState() { return new WidgetUpdater.WidgetState(getPlayable(), getStatus(), @@ -873,11 +880,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) { - return flavorHelper.onMediaPlayerInfo(PlaybackService.this, code, resourceId); - } - - @Override public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) { PlaybackService.this.onPostPlayback(media, ended, skipped, playingNext); @@ -916,10 +918,24 @@ public class PlaybackService extends MediaBrowserServiceCompat { return PlaybackService.this.getNextInQueue(currentMedia); } + @Nullable + @Override + public Playable findMedia(@NonNull String url) { + FeedItem item = DBReader.getFeedItemByGuidOrEpisodeUrl(null, url); + return item != null ? item.getMedia() : null; + } + @Override public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { PlaybackService.this.onPlaybackEnded(mediaType, stopPlaying); } + + @Override + public void ensureMediaInfoLoaded(@NonNull Playable media) { + if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) { + ((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId())); + } + } }; @Subscribe(threadMode = ThreadMode.MAIN) @@ -1248,15 +1264,15 @@ public class PlaybackService extends MediaBrowserServiceCompat { // This would give the PIP of videos a play button capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY; if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) { - flavorHelper.sessionStateAddActionForWear(sessionState, + WearMediaSession.sessionStateAddActionForWear(sessionState, CUSTOM_ACTION_REWIND, getString(R.string.rewind_label), android.R.drawable.ic_media_rew); - flavorHelper.sessionStateAddActionForWear(sessionState, + WearMediaSession.sessionStateAddActionForWear(sessionState, CUSTOM_ACTION_FAST_FORWARD, getString(R.string.fast_forward_label), android.R.drawable.ic_media_ff); - flavorHelper.mediaSessionSetExtraForWear(mediaSession); + WearMediaSession.mediaSessionSetExtraForWear(mediaSession); } } @@ -1338,7 +1354,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationBuilder.setPlayable(playable); notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken()); notificationBuilder.setPlayerStatus(playerStatus); - notificationBuilder.setCasting(isCasting); notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed()); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); @@ -1901,93 +1916,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { (sharedPreferences, key) -> { if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) { updateNotificationAndMediaSession(getPlayable()); - } else { - flavorHelper.onSharedPreference(key); } }; - - interface FlavorHelperCallback { - PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback(); - - void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer); - - PlaybackServiceMediaPlayer getMediaPlayer(); - - void setIsCasting(boolean isCasting); - - void sendNotificationBroadcast(int type, int code); - - void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position); - - void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info); - - MediaSessionCompat getMediaSession(); - - Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter); - - void unregisterReceiver(BroadcastReceiver receiver); - } - - private final FlavorHelperCallback flavorHelperCallback = new FlavorHelperCallback() { - @Override - public PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback() { - return PlaybackService.this.mediaPlayerCallback; - } - - @Override - public void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer) { - PlaybackService.this.mediaPlayer = mediaPlayer; - } - - @Override - public PlaybackServiceMediaPlayer getMediaPlayer() { - return PlaybackService.this.mediaPlayer; - } - - @Override - public void setIsCasting(boolean isCasting) { - PlaybackService.isCasting = isCasting; - stateManager.validStartCommandWasReceived(); - } - - @Override - public void sendNotificationBroadcast(int type, int code) { - PlaybackService.this.sendNotificationBroadcast(type, code); - } - - @Override - public void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) { - PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position); - } - - @Override - public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) { - if (connected) { - PlaybackService.this.updateNotificationAndMediaSession(info.playable); - } else { - PlayerStatus status = info.playerStatus; - if (status == PlayerStatus.PLAYING || status == PlayerStatus.SEEKING - || status == PlayerStatus.PREPARING || UserPreferences.isPersistNotify()) { - PlaybackService.this.updateNotificationAndMediaSession(info.playable); - } else if (!UserPreferences.isPersistNotify()) { - stateManager.stopForeground(true); - } - } - } - - @Override - public MediaSessionCompat getMediaSession() { - return PlaybackService.this.mediaSession; - } - - @Override - public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { - return PlaybackService.this.registerReceiver(receiver, filter); - } - - @Override - public void unregisterReceiver(BroadcastReceiver receiver) { - PlaybackService.this.unregisterReceiver(receiver); - } - }; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java deleted file mode 100644 index 623ad58bb..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java +++ /dev/null @@ -1,380 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -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; - -import java.util.List; -import java.util.concurrent.Future; - -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.model.playback.Playable; - - -/* - * An inconvenience of an implementation like this is that some members and methods that once were - * private are now protected, allowing for access from classes of the same package, namely - * PlaybackService. A workaround would be to move this to a dedicated package. - */ -/** - * Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local - * and remote (cast devices) playback. - */ -public abstract class PlaybackServiceMediaPlayer { - private static final String TAG = "PlaybackSvcMediaPlayer"; - - /** - * Return value of some PSMP methods if the method call failed. - */ - static final int INVALID_TIME = -1; - - private volatile PlayerStatus oldPlayerStatus; - 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; - - PlaybackServiceMediaPlayer(@NonNull Context context, - @NonNull PSMPCallback callback){ - this.context = context; - this.callback = callback; - - playerStatus = PlayerStatus.STOPPED; - } - - /** - * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing - * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will - * not do anything. - * Whether playback starts immediately depends on the given parameters. See below for more details. - * <p/> - * States: - * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. - * <p/> - * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If - * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. - * <p/> - * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object - * will enter the ERROR state. - * <p/> - * This method is executed on an internal executor service. - * - * @param playable The Playable object that is supposed to be played. This parameter must not be null. - * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via - * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by - * the Android MediaPlayer via getStreamUrl. - * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the - * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared - * for playback immediately (see 'prepareImmediately' parameter for more details) - * @param prepareImmediately Set to true if the method should also prepare the episode for playback. - */ - public abstract void playMediaObject(@NonNull Playable playable, boolean stream, boolean startWhenPrepared, boolean prepareImmediately); - - /** - * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. - * nothing will happen. - * <p/> - * This method is executed on an internal executor service. - */ - public abstract void resume(); - - /** - * Saves the current position and pauses playback. Note that, if audiofocus - * is abandoned, the lockscreen controls will also disapear. - * <p/> - * This method is executed on an internal executor service. - * - * @param abandonFocus is true if the service should release audio focus - * @param reinit is true if service should reinit after pausing if the media - * file is being streamed - */ - public abstract void pause(boolean abandonFocus, boolean reinit); - - /** - * Prepared media player for playback if the service is in the INITALIZED - * state. - * <p/> - * This method is executed on an internal executor service. - */ - public abstract void prepare(); - - /** - * Resets the media player and moves it into INITIALIZED state. - * <p/> - * This method is executed on an internal executor service. - */ - public abstract void reinit(); - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * Invalid time values (< 0) will be ignored. - * <p/> - * This method is executed on an internal executor service. - */ - public abstract void seekTo(int t); - - /** - * Seek a specific position from the current position - * - * @param d offset from current position (positive or negative) - */ - public abstract void seekDelta(int d); - - /** - * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. - */ - public abstract int getDuration(); - - /** - * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. - */ - public abstract int getPosition(); - - public abstract boolean isStartWhenPrepared(); - - public abstract void setStartWhenPrepared(boolean startWhenPrepared); - - /** - * Sets the playback parameters. - * - Speed - * - SkipSilence (ExoPlayer only) - * This method is executed on an internal executor service. - */ - public abstract void setPlaybackParams(final float speed, final boolean skipSilence); - - /** - * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. - */ - public abstract float getPlaybackSpeed(); - - /** - * Sets the playback volume. - * This method is executed on an internal executor service. - */ - public abstract void setVolume(float volumeLeft, float volumeRight); - - /** - * Returns true if the mediaplayer can mix stereo down to mono - */ - public abstract boolean canDownmix(); - - public abstract void setDownmix(boolean enable); - - public abstract MediaType getCurrentMediaType(); - - public abstract boolean isStreaming(); - - /** - * Releases internally used resources. This method should only be called when the object is not used anymore. - */ - public abstract void shutdown(); - - /** - * Releases internally used resources. This method should only be called when the object is not used anymore. - * This method is executed on an internal executor service. - */ - public abstract void shutdownQuietly(); - - public abstract void setVideoSurface(SurfaceHolder surface); - - public abstract void resetVideoSurface(); - - /** - * Return width and height of the currently playing video as a pair. - * - * @return Width and height as a Pair or null if the video size could not be determined. The method might still - * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return - * invalid values. - */ - public abstract Pair<Integer, Integer> getVideoSize(); - - /** - * Returns a PSMInfo object that contains information about the current state of the PSMP object. - * - * @return The PSMPInfo object. - */ - public final synchronized PSMPInfo getPSMPInfo() { - return new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable()); - } - - /** - * Returns the current status, if you need the media and the player status together, you should - * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition - * could result in nonsensical results (like a status of PLAYING, but a null playable) - * @return the current player status - */ - public synchronized PlayerStatus getPlayerStatus() { - return playerStatus; - } - - /** - * Returns the current media, if you need the media and the player status together, you should - * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition - * could result in nonsensical results (like a status of PLAYING, but a null playable) - * @return the current media. May be null - */ - public abstract Playable getPlayable(); - - protected abstract void setPlayable(Playable playable); - - public abstract List<String> getAudioTracks(); - - public abstract void setAudioTrack(int track); - - public abstract int getSelectedAudioTrack(); - - public void skip() { - endPlayback(false, true, true, true); - } - - /** - * Ends playback of current media (if any) and moves into INDETERMINATE state, unless - * {@param toStoppedState} is set to true, in which case it moves into STOPPED state. - * - * @see #endPlayback(boolean, boolean, boolean, boolean) - */ - public Future<?> stopPlayback(boolean toStoppedState) { - return endPlayback(false, false, false, toStoppedState); - } - - /** - * Internal method that handles end of playback. - * - * Currently, it has 5 use cases: - * <ul> - * <li>Media playback has completed: call with (true, false, true, true)</li> - * <li>User asks to skip to next episode: call with (false, true, true, true)</li> - * <li>Skipping to next episode due to playback error: call with (false, false, true, true)</li> - * <li>Stopping the media player: call with (false, false, false, true)</li> - * <li>We want to change the media player implementation: call with (false, false, false, false)</li> - * </ul> - * - * @param hasEnded If true, we assume the current media's playback has ended, for - * purposes of post playback processing. - * @param wasSkipped Whether the user chose to skip the episode (by pressing the skip - * button). - * @param shouldContinue If true, the media player should try to load, and possibly play, - * the next item, based on the user preferences and whether such item - * exists. - * @param toStoppedState If true, the playback state gets set to STOPPED if the media player - * is not loading/playing after this call, and the UI will reflect that. - * Only relevant if {@param shouldContinue} is set to false, otherwise - * this method's behavior defaults as if this parameter was true. - * - * @return a Future, just for the purpose of tracking its execution. - */ - protected abstract Future<?> endPlayback(boolean hasEnded, boolean wasSkipped, - boolean shouldContinue, boolean toStoppedState); - - /** - * @return {@code true} if the WifiLock feature should be used, {@code false} otherwise. - */ - protected abstract boolean shouldLockWifi(); - - final synchronized void acquireWifiLockIfNecessary() { - if (shouldLockWifi()) { - if (wifiLock == null) { - wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE)) - .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); - wifiLock.setReferenceCounted(false); - } - wifiLock.acquire(); - } - } - - final synchronized void releaseWifiLockIfNecessary() { - if (wifiLock != null && wifiLock.isHeld()) { - wifiLock.release(); - } - } - - /** - * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time - * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null). - * <p/> - * This method will notify the callback about the change of the player status (even if the new status is the same - * as the old one). - * <p/> - * It will also call {@link PSMPCallback#onPlaybackPause(Playable, int)} or {@link PSMPCallback#onPlaybackStart(Playable, int)} - * depending on the status change. - * - * @param newStatus The new PlayerStatus. This must not be null. - * @param newMedia The new playable object of the PSMP object. This can be null. - * @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) { - Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus); - - this.oldPlayerStatus = playerStatus; - this.playerStatus = newStatus; - setPlayable(newMedia); - - if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) { - if (oldPlayerStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING) { - callback.onPlaybackPause(newMedia, position); - } else if (oldPlayerStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING) { - callback.onPlaybackStart(newMedia, position); - } - } - - callback.statusChanged(new PSMPInfo(oldPlayerStatus, playerStatus, getPlayable())); - } - - public boolean isAudioChannelInUse() { - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - return (audioManager.getMode() != AudioManager.MODE_NORMAL || audioManager.isMusicActive()); - } - - /** - * @see #setPlayerStatus(PlayerStatus, Playable, int) - */ - final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { - setPlayerStatus(newStatus, newMedia, INVALID_TIME); - } - - public interface PSMPCallback { - void statusChanged(PSMPInfo newInfo); - - void shouldStop(); - - 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); - - void onPlaybackPause(Playable playable, int position); - - Playable getNextInQueue(Playable currentMedia); - - void onPlaybackEnded(MediaType mediaType, boolean stopPlaying); - } - - /** - * Holds information about a PSMP object. - */ - public static class PSMPInfo { - public final PlayerStatus oldPlayerStatus; - public PlayerStatus playerStatus; - public Playable playable; - - PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) { - this.oldPlayerStatus = oldPlayerStatus; - this.playerStatus = playerStatus; - this.playable = playable; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java index 5aee8c24c..c348f5773 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java @@ -31,17 +31,17 @@ import de.danoeh.antennapod.model.playback.Playable; import java.util.ArrayList; import java.util.concurrent.ExecutionException; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.apache.commons.lang3.ArrayUtils; public class PlaybackServiceNotificationBuilder { private static final String TAG = "PlaybackSrvNotification"; private static Bitmap defaultIcon = null; - private Context context; + private final Context context; private Playable playable; private MediaSessionCompat.Token mediaSessionToken; private PlayerStatus playerStatus; - private boolean isCasting; private Bitmap icon; private String position; @@ -140,7 +140,7 @@ public class PlaybackServiceNotificationBuilder { if (playable != null) { notification.setContentTitle(playable.getFeedTitle()); notification.setContentText(playable.getEpisodeTitle()); - addActions(notification, mediaSessionToken, playerStatus, isCasting); + addActions(notification, mediaSessionToken, playerStatus); if (icon != null) { notification.setLargeIcon(icon); @@ -175,23 +175,10 @@ public class PlaybackServiceNotificationBuilder { } private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken, - PlayerStatus playerStatus, boolean isCasting) { + PlayerStatus playerStatus) { ArrayList<Integer> compactActionList = new ArrayList<>(); int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction - - if (isCasting) { - Intent stopCastingIntent = new Intent(context, PlaybackService.class); - stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true); - PendingIntent stopCastingPendingIntent = PendingIntent.getService(context, - numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - notification.addAction(R.drawable.ic_notification_cast_off, - context.getString(R.string.cast_disconnect_label), - stopCastingPendingIntent); - numActions++; - } - // always let them rewind PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_REWIND, numActions); @@ -270,10 +257,6 @@ public class PlaybackServiceNotificationBuilder { this.playerStatus = playerStatus; } - public void setCasting(boolean casting) { - isCasting = casting; - } - public PlayerStatus getPlayerStatus() { return playerStatus; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java index edb8bc3a9..43837a473 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdater.java @@ -4,6 +4,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; class PlaybackVolumeUpdater { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java deleted file mode 100644 index 4f2ae34f8..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -public enum PlayerStatus { - INDETERMINATE(0), // player is currently changing its state, listeners should wait until the player has left this state. - ERROR(-1), - PREPARING(19), - PAUSED(30), - PLAYING(40), - STOPPED(5), - PREPARED(20), - SEEKING(29), - INITIALIZING(9), // playback service is loading the Playable's metadata - INITIALIZED(10); // playback service was started, data source of media player was set. - - private final int statusValue; - private static final PlayerStatus[] fromOrdinalLookup; - - static { - fromOrdinalLookup = PlayerStatus.values(); - } - - PlayerStatus(int val) { - statusValue = val; - } - - public static PlayerStatus fromOrdinal(int o) { - return fromOrdinalLookup[o]; - } - - public boolean isAtLeast(PlayerStatus other) { - return other == null || this.statusValue>=other.statusValue; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java deleted file mode 100644 index 813c6d0f7..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import java.util.concurrent.TimeUnit; - -/** - * This class calculates the proper rewind time after the pause and resume. - * <p> - * User might loose context if he/she pauses and resumes the media after longer time. - * Media file should be "rewinded" x seconds after user resumes the playback. - */ -public class RewindAfterPauseUtils { - private RewindAfterPauseUtils(){} - - public static final long ELAPSED_TIME_FOR_SHORT_REWIND = TimeUnit.MINUTES.toMillis(1); - public static final long ELAPSED_TIME_FOR_MEDIUM_REWIND = TimeUnit.HOURS.toMillis(1); - public static final long ELAPSED_TIME_FOR_LONG_REWIND = TimeUnit.DAYS.toMillis(1); - - public static final long SHORT_REWIND = TimeUnit.SECONDS.toMillis(3); - public static final long MEDIUM_REWIND = TimeUnit.SECONDS.toMillis(10); - public static final long LONG_REWIND = TimeUnit.SECONDS.toMillis(20); - - /** - * @param currentPosition current position in a media file in ms - * @param lastPlayedTime timestamp when was media paused - * @return new rewinded position for playback in milliseconds - */ - public static int calculatePositionWithRewind(int currentPosition, long lastPlayedTime) { - if (currentPosition > 0 && lastPlayedTime > 0) { - long elapsedTime = System.currentTimeMillis() - lastPlayedTime; - long rewindTime = 0; - - if (elapsedTime > ELAPSED_TIME_FOR_LONG_REWIND) { - rewindTime = LONG_REWIND; - } else if (elapsedTime > ELAPSED_TIME_FOR_MEDIUM_REWIND) { - rewindTime = MEDIUM_REWIND; - } else if (elapsedTime > ELAPSED_TIME_FOR_SHORT_REWIND) { - rewindTime = SHORT_REWIND; - } - - int newPosition = currentPosition - (int) rewindTime; - - return Math.max(newPosition, 0); - } else { - return currentPosition; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index b436d80b2..549171c76 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -22,9 +22,9 @@ import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java index 5275e7080..2762fb9fe 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdater.java @@ -25,11 +25,11 @@ import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.receiver.PlayerWidget; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlayerStatus; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; diff --git a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java index b14fb3b0b..325c508c5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/widget/WidgetUpdaterJobService.java @@ -6,9 +6,9 @@ import androidx.annotation.NonNull; import androidx.core.app.SafeJobIntentService; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlayableUtils; +import de.danoeh.antennapod.playback.base.PlayerStatus; public class WidgetUpdaterJobService extends SafeJobIntentService { private static final int JOB_ID = -17001; diff --git a/core/src/main/res/values-land/dimens.xml b/core/src/main/res/values-land/dimens.xml deleted file mode 100644 index 73b2b2e98..000000000 --- a/core/src/main/res/values-land/dimens.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <dimen name="media_router_controller_playback_control_start_padding">@dimen/media_router_controller_playback_control_horizontal_spacing</dimen> -</resources> diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml index d1e200d1d..4b2247492 100644 --- a/core/src/main/res/values/dimens.xml +++ b/core/src/main/res/values/dimens.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <dimen name="widget_margin">0dp</dimen> <dimen name="external_player_height">64dp</dimen> <dimen name="text_size_micro">12sp</dimen> @@ -28,11 +27,5 @@ <dimen name="audioplayer_playercontrols_length_big">64dp</dimen> <dimen name="audioplayer_playercontrols_margin">12dp</dimen> - <dimen name="media_router_controller_playback_control_vertical_padding">16dp</dimen> - <dimen name="media_router_controller_playback_control_horizontal_spacing">12dp</dimen> - <dimen name="media_router_controller_playback_control_start_padding">24dp</dimen> - <dimen name="media_router_controller_bottom_margin">8dp</dimen> - <dimen name="nav_drawer_max_screen_size">480dp</dimen> - </resources> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 1ab5b2184..59b335bc8 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -502,9 +502,6 @@ <string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_sum">Set a network proxy</string> <string name="pref_no_browser_found">No web browser found.</string> - <string name="pref_cast_title">Chromecast support</string> - <string name="pref_cast_message_play_flavor">Enable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV)</string> - <string name="pref_cast_message_free_flavor" tools:ignore="UnusedResources">Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod</string> <string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string> <string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string> <string name="media_player_builtin">Built-in Android player (deprecated) </string> @@ -664,7 +661,6 @@ <string name="pref_pausePlaybackForFocusLoss_title">Pause for Interruptions</string> <string name="pref_resumeAfterCall_sum">Resume playback after a phone call completes</string> <string name="pref_resumeAfterCall_title">Resume after Call</string> - <string name="pref_restart_required">AntennaPod has to be restarted for this change to take effect.</string> <!-- Online feed view --> <string name="subscribe_label">Subscribe</string> @@ -808,21 +804,6 @@ <!-- Subscriptions fragment --> <string name="subscription_num_columns">Number of columns</string> - <!-- Casting --> - <string name="cast_media_route_menu_title">Play on…</string> - <string name="cast_disconnect_label">Disconnect the cast session</string> - <string name="cast_not_castable">Media selected is not compatible with cast device</string> - <string name="cast_failed_to_play">Failed to start the playback of media</string> - <string name="cast_failed_to_stop">Failed to stop the playback of media</string> - <string name="cast_failed_to_pause">Failed to pause the playback of media</string> - <string name="cast_failed_setting_volume">Failed to set the volume</string> - <string name="cast_failed_no_connection">No connection to the cast device is present</string> - <string name="cast_failed_no_connection_trans">Connection to the cast device has been lost. Application is trying to re-establish the connection, if possible. Please wait for a few seconds and try again.</string> - <string name="cast_failed_status_request">Failed to sync up with the cast device</string> - <string name="cast_failed_seek">Failed to seek to the new position on the cast device</string> - <string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string> - <string name="cast_failed_media_error_skipping">Error playing media. Skipping…</string> - <!-- Notification channels --> <string name="notification_group_errors">Errors</string> <string name="notification_group_news">News</string> diff --git a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java deleted file mode 100644 index 27f985a4c..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.danoeh.antennapod.core; - -import androidx.annotation.Nullable; -import androidx.mediarouter.app.MediaRouteDialogFactory; - -/** - * Callbacks for Chromecast support on the core module - */ -public interface CastCallbacks { - - @Nullable MediaRouteDialogFactory getMediaRouterDialogFactory(); -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java deleted file mode 100644 index 48de7c6e1..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java +++ /dev/null @@ -1,64 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.content.Context; -import android.util.Log; -import de.danoeh.antennapod.core.cast.CastManager; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; -import de.danoeh.antennapod.core.preferences.UsageStatistics; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.gui.NotificationUtils; -import de.danoeh.antennapod.net.ssl.SslProviderInstaller; - -import java.io.File; - -/** - * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables. - * Apps using the core module of AntennaPod should register implementations of all interfaces here. - */ -public class ClientConfig { - private static final String TAG = "ClientConfig"; - - private ClientConfig(){} - - /** - * Should be used when setting User-Agent header for HTTP-requests. - */ - public static String USER_AGENT; - - public static ApplicationCallbacks applicationCallbacks; - - public static DownloadServiceCallbacks downloadServiceCallbacks; - - public static CastCallbacks castCallbacks; - - private static boolean initialized = false; - - public static synchronized void initialize(Context context) { - if (initialized) { - return; - } - PodDBAdapter.init(context); - UserPreferences.init(context); - UsageStatistics.init(context); - PlaybackPreferences.init(context); - SslProviderInstaller.install(context); - NetworkUtils.init(context); - // Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary - // Google Play Service usage. - // Down side: when the user decides to enable casting, AntennaPod needs to be restarted - // for it to take effect. - if (UserPreferences.isCastEnabled()) { - CastManager.init(context); - } else { - Log.v(TAG, "Cast is disabled. All Cast-related initialization will be skipped."); - } - AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp")); - SleepTimerPreferences.init(context); - NotificationUtils.createChannels(context); - initialized = true; - } -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java deleted file mode 100644 index 8d0e40116..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastButtonVisibilityManager.java +++ /dev/null @@ -1,120 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; - -import de.danoeh.antennapod.core.R; - -public class CastButtonVisibilityManager { - private static final String TAG = "CastBtnVisibilityMgr"; - private final CastManager castManager; - private volatile boolean prefEnabled = false; - private volatile boolean viewRequested = false; - private volatile boolean resumed = false; - private volatile boolean connected = false; - private volatile int showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; - private Menu menu; - public SwitchableMediaRouteActionProvider mediaRouteActionProvider; - - public CastButtonVisibilityManager(CastManager castManager) { - this.castManager = castManager; - } - - public synchronized void setPrefEnabled(boolean newValue) { - if (prefEnabled != newValue && resumed && (viewRequested || connected)) { - if (newValue) { - castManager.incrementUiCounter(); - } else { - castManager.decrementUiCounter(); - } - } - prefEnabled = newValue; - if (mediaRouteActionProvider != null) { - mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); - } - } - - public synchronized void setResumed(boolean newValue) { - if (resumed == newValue) { - Log.e(TAG, "resumed should never change to the same value"); - return; - } - resumed = newValue; - if (prefEnabled && (viewRequested || connected)) { - if (resumed) { - castManager.incrementUiCounter(); - } else { - castManager.decrementUiCounter(); - } - } - } - - public synchronized void setViewRequested(boolean newValue) { - if (viewRequested != newValue && resumed && prefEnabled && !connected) { - if (newValue) { - castManager.incrementUiCounter(); - } else { - castManager.decrementUiCounter(); - } - } - viewRequested = newValue; - if (mediaRouteActionProvider != null) { - mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); - } - } - - public synchronized void setConnected(boolean newValue) { - if (connected != newValue && resumed && prefEnabled && !prefEnabled) { - if (newValue) { - castManager.incrementUiCounter(); - } else { - castManager.decrementUiCounter(); - } - } - connected = newValue; - if (mediaRouteActionProvider != null) { - mediaRouteActionProvider.setEnabled(prefEnabled && (viewRequested || connected)); - } - } - - public synchronized boolean shouldEnable() { - return prefEnabled && viewRequested; - } - - public void setMenu(Menu menu) { - setViewRequested(false); - showAsAction = MenuItem.SHOW_AS_ACTION_IF_ROOM; - this.menu = menu; - setShowAsAction(); - } - - public void requestCastButton(int showAsAction) { - setViewRequested(true); - this.showAsAction = showAsAction; - setShowAsAction(); - } - - public void onConnected() { - setConnected(true); - setShowAsAction(); - } - - public void onDisconnected() { - setConnected(false); - setShowAsAction(); - } - - private void setShowAsAction() { - if (menu == null) { - Log.d(TAG, "setShowAsAction() without a menu"); - return; - } - MenuItem item = menu.findItem(R.id.media_route_menu_item); - if (item == null) { - Log.e(TAG, "setShowAsAction(), but cast button not inflated"); - return; - } - item.setShowAsAction(connected ? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction); - } -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java deleted file mode 100644 index 213dd1875..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastConsumer.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumer; - -public interface CastConsumer extends VideoCastConsumer{ - - /** - * Called when the stream's volume is changed. - */ - void onStreamVolumeChanged(double value, boolean isMute); -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java deleted file mode 100644 index dd07b9cd8..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java +++ /dev/null @@ -1,1091 +0,0 @@ -/* - * Copyright (C) 2015 Google Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * ------------------------------------------------------------------------ - * - * Changes made by Domingos Lopes <domingos86lopes@gmail.com> - * - * original can be found at http://www.github.com/googlecast/CastCompanionLibrary-android - */ - -package de.danoeh.antennapod.core.cast; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.core.view.ActionProvider; -import androidx.core.view.MenuItemCompat; -import androidx.mediarouter.media.MediaRouter; -import android.util.Log; -import android.view.MenuItem; - -import com.google.android.gms.cast.ApplicationMetadata; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.CastMediaControlIntent; -import com.google.android.gms.cast.CastStatusCodes; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.cast.MediaQueueItem; -import com.google.android.gms.cast.MediaStatus; -import com.google.android.gms.cast.RemoteMediaPlayer; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; -import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.OnFailedListener; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException; - -import org.json.JSONObject; - -import java.io.IOException; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.R; - -import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_PLAY; -import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_UNCHANGED; - -/** - * A subclass of {@link BaseCastManager} that is suitable for casting video contents (it - * also provides a single custom data channel/namespace if an out-of-band communication is - * needed). - * <p> - * Clients need to initialize this class by calling - * {@link #init(android.content.Context)} in the Application's - * {@code onCreate()} method. To access the (singleton) instance of this class, clients - * need to call {@link #getInstance()}. - * <p>This - * class manages various states of the remote cast device. Client applications, however, can - * complement the default behavior of this class by hooking into various callbacks that it provides - * (see {@link CastConsumer}). - * Since the number of these callbacks is usually much larger than what a single application might - * be interested in, there is a no-op implementation of this interface (see - * {@link DefaultCastConsumer}) that applications can subclass to override only those methods that - * they are interested in. Since this library depends on the cast functionalities provided by the - * Google Play services, the library checks to ensure that the right version of that service is - * installed. It also provides a simple static method {@code checkGooglePlayServices()} that clients - * can call at an early stage of their applications to provide a dialog for users if they need to - * update/activate their Google Play Services library. - * - * @see CastConfiguration - */ -public class CastManager extends BaseCastManager implements OnFailedListener { - public static final String TAG = "CastManager"; - - public static final String CAST_APP_ID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; - - private MediaStatus mediaStatus; - private static CastManager INSTANCE; - private RemoteMediaPlayer remoteMediaPlayer; - private int state = MediaStatus.PLAYER_STATE_IDLE; - private final Set<CastConsumer> castConsumers = new CopyOnWriteArraySet<>(); - - public static final int QUEUE_OPERATION_LOAD = 1; - public static final int QUEUE_OPERATION_APPEND = 9; - - private CastManager(Context context, CastConfiguration castConfiguration) { - super(context, castConfiguration); - Log.d(TAG, "CastManager is instantiated"); - } - - public static synchronized CastManager init(Context context) { - if (INSTANCE == null) { - CastConfiguration castConfiguration = new CastConfiguration.Builder(CAST_APP_ID) - .enableDebug() - .enableAutoReconnect() - .enableWifiReconnection() - .setLaunchOptions(true, Locale.getDefault()) - .setMediaRouteDialogFactory(ClientConfig.castCallbacks.getMediaRouterDialogFactory()) - .build(); - Log.d(TAG, "New instance of CastManager is created"); - if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance() - .isGooglePlayServicesAvailable(context)) { - Log.e(TAG, "Couldn't find the appropriate version of Google Play Services"); - } - INSTANCE = new CastManager(context, castConfiguration); - } - return INSTANCE; - } - - /** - * Returns a (singleton) instance of this class. Clients should call this method in order to - * get a hold of this singleton instance, only after it is initialized. If it is not initialized - * yet, an {@link IllegalStateException} will be thrown. - * - */ - public static CastManager getInstance() { - if (INSTANCE == null) { - String msg = "No CastManager instance was found, did you forget to initialize it?"; - Log.e(TAG, msg); - throw new IllegalStateException(msg); - } - return INSTANCE; - } - - public static boolean isInitialized() { - return INSTANCE != null; - } - - /** - * Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media - * control APIs that this library do not provide a wrapper for, client applications can call - * those methods directly after obtaining an instance of the active {@link RemoteMediaPlayer}. - */ - public final RemoteMediaPlayer getRemoteMediaPlayer() { - return remoteMediaPlayer; - } - - /* - * A simple check to make sure remoteMediaPlayer is not null - */ - private void checkRemoteMediaPlayerAvailable() throws NoConnectionException { - if (remoteMediaPlayer == null) { - throw new NoConnectionException(); - } - } - - /** - * Indicates if the remote media is currently playing (or buffering). - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isRemoteMediaPlaying() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - return state == MediaStatus.PLAYER_STATE_BUFFERING - || state == MediaStatus.PLAYER_STATE_PLAYING; - } - - /** - * Returns <code>true</code> if the remote connected device is playing a movie. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isRemoteMediaPaused() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - return state == MediaStatus.PLAYER_STATE_PAUSED; - } - - /** - * Returns <code>true</code> only if there is a media on the remote being played, paused or - * buffered. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isRemoteMediaLoaded() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - return isRemoteMediaPaused() || isRemoteMediaPlaying(); - } - - /** - * Gets the remote's system volume. It internally detects what type of volume is used. - * - * @throws NoConnectionException If no connectivity to the device exists - * @throws TransientNetworkDisconnectionException If framework is still trying to recover from - * a possibly transient loss of network - */ - public double getStreamVolume() throws TransientNetworkDisconnectionException, NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getMediaStatus().getStreamVolume(); - } - - /** - * Sets the stream volume. - * - * @param volume Should be a value between 0 and 1, inclusive. - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - * @throws CastException If setting system volume fails - */ - public void setStreamVolume(double volume) throws CastException, - TransientNetworkDisconnectionException, NoConnectionException { - checkConnectivity(); - if (volume > 1.0) { - volume = 1.0; - } else if (volume < 0) { - volume = 0.0; - } - - RemoteMediaPlayer mediaPlayer = getRemoteMediaPlayer(); - if (mediaPlayer == null) { - throw new NoConnectionException(); - } - mediaPlayer.setStreamVolume(mApiClient, volume).setResultCallback( - (result) -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_setting_volume, - result.getStatus().getStatusCode()); - } else { - CastManager.this.onStreamVolumeChanged(); - } - }); - } - - /** - * Returns <code>true</code> if remote Stream is muted. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isStreamMute() throws TransientNetworkDisconnectionException, NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getMediaStatus().isMute(); - } - - /** - * Returns the duration of the media that is loaded, in milliseconds. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public long getMediaDuration() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getStreamDuration(); - } - - /** - * Returns the current (approximate) position of the current media, in milliseconds. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public long getCurrentMediaPosition() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getApproximateStreamPosition(); - } - - public int getApplicationStandbyState() throws IllegalStateException { - Log.d(TAG, "getApplicationStandbyState()"); - return Cast.CastApi.getStandbyState(mApiClient); - } - - private void onApplicationDisconnected(int errorCode) { - Log.d(TAG, "onApplicationDisconnected() reached with error code: " + errorCode); - mApplicationErrorCode = errorCode; - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationDisconnected(errorCode); - } - if (mMediaRouter != null) { - Log.d(TAG, "onApplicationDisconnected(): Cached RouteInfo: " + getRouteInfo()); - Log.d(TAG, "onApplicationDisconnected(): Selected RouteInfo: " - + mMediaRouter.getSelectedRoute()); - if (getRouteInfo() == null || mMediaRouter.getSelectedRoute().equals(getRouteInfo())) { - Log.d(TAG, "onApplicationDisconnected(): Setting route to default"); - mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute()); - } - } - onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); - } - - private void onApplicationStatusChanged() { - if (!isConnected()) { - return; - } - try { - String appStatus = Cast.CastApi.getApplicationStatus(mApiClient); - Log.d(TAG, "onApplicationStatusChanged() reached: " + appStatus); - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationStatusChanged(appStatus); - } - } catch (IllegalStateException e) { - Log.e(TAG, "onApplicationStatusChanged()", e); - } - } - - private void onDeviceVolumeChanged() { - Log.d(TAG, "onDeviceVolumeChanged() reached"); - double volume; - try { - volume = getDeviceVolume(); - boolean isMute = isDeviceMute(); - for (CastConsumer consumer : castConsumers) { - consumer.onVolumeChanged(volume, isMute); - } - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Failed to get volume", e); - } - - } - - private void onStreamVolumeChanged() { - Log.d(TAG, "onStreamVolumeChanged() reached"); - double volume; - try { - volume = getStreamVolume(); - boolean isMute = isStreamMute(); - for (CastConsumer consumer : castConsumers) { - consumer.onStreamVolumeChanged(volume, isMute); - } - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Failed to get volume", e); - } - } - - @Override - protected void onApplicationConnected(ApplicationMetadata appMetadata, - String applicationStatus, String sessionId, boolean wasLaunched) { - Log.d(TAG, "onApplicationConnected() reached with sessionId: " + sessionId - + ", and mReconnectionStatus=" + mReconnectionStatus); - mApplicationErrorCode = NO_APPLICATION_ERROR; - if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) { - // we have tried to reconnect and successfully launched the app, so - // it is time to select the route and make the cast icon happy :-) - List<MediaRouter.RouteInfo> routes = mMediaRouter.getRoutes(); - if (routes != null) { - String routeId = mPreferenceAccessor.getStringFromPreference(PREFS_KEY_ROUTE_ID); - for (MediaRouter.RouteInfo routeInfo : routes) { - if (routeId.equals(routeInfo.getId())) { - // found the right route - Log.d(TAG, "Found the correct route during reconnection attempt"); - mReconnectionStatus = RECONNECTION_STATUS_FINALIZED; - mMediaRouter.selectRoute(routeInfo); - break; - } - } - } - } - try { - //attachDataChannel(); - attachMediaChannel(); - mSessionId = sessionId; - // saving device for future retrieval; we only save the last session info - mPreferenceAccessor.saveStringToPreference(PREFS_KEY_SESSION_ID, mSessionId); - remoteMediaPlayer.requestStatus(mApiClient).setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_status_request, - result.getStatus().getStatusCode()); - } - }); - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationConnected(appMetadata, mSessionId, wasLaunched); - } - } catch (TransientNetworkDisconnectionException e) { - Log.e(TAG, "Failed to attach media/data channel due to network issues", e); - onFailed(R.string.cast_failed_no_connection_trans, NO_STATUS_CODE); - } catch (NoConnectionException e) { - Log.e(TAG, "Failed to attach media/data channel due to network issues", e); - onFailed(R.string.cast_failed_no_connection, NO_STATUS_CODE); - } - } - - /* - * (non-Javadoc) - * @see com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager - * #onConnectivityRecovered() - */ - @Override - public void onConnectivityRecovered() { - reattachMediaChannel(); - //reattachDataChannel(); - super.onConnectivityRecovered(); - } - - /* - * (non-Javadoc) - * @see com.google.android.gms.cast.CastClient.Listener#onApplicationStopFailed (int) - */ - @Override - public void onApplicationStopFailed(int errorCode) { - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationStopFailed(errorCode); - } - } - - @Override - public void onApplicationConnectionFailed(int errorCode) { - Log.d(TAG, "onApplicationConnectionFailed() reached with errorCode: " + errorCode); - mApplicationErrorCode = errorCode; - if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) { - if (errorCode == CastStatusCodes.APPLICATION_NOT_RUNNING) { - // while trying to re-establish session, we found out that the app is not running - // so we need to disconnect - mReconnectionStatus = RECONNECTION_STATUS_INACTIVE; - onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); - } - } else { - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationConnectionFailed(errorCode); - } - onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); - if (mMediaRouter != null) { - Log.d(TAG, "onApplicationConnectionFailed(): Setting route to default"); - mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute()); - } - } - } - - /** - * Loads a media. For this to succeed, you need to have successfully launched the application. - * - * @param media The media to be loaded - * @param autoPlay If <code>true</code>, playback starts after load - * @param position Where to start the playback (only used if autoPlay is <code>true</code>. - * Units is milliseconds. - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void loadMedia(MediaInfo media, boolean autoPlay, int position) - throws TransientNetworkDisconnectionException, NoConnectionException { - loadMedia(media, autoPlay, position, null); - } - - /** - * Loads a media. For this to succeed, you need to have successfully launched the application. - * - * @param media The media to be loaded - * @param autoPlay If <code>true</code>, playback starts after load - * @param position Where to start the playback (only used if autoPlay is <code>true</code>). - * Units is milliseconds. - * @param customData Optional {@link JSONObject} data to be passed to the cast device - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void loadMedia(MediaInfo media, boolean autoPlay, int position, JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - loadMedia(media, null, autoPlay, position, customData); - } - - /** - * Loads a media. For this to succeed, you need to have successfully launched the application. - * - * @param media The media to be loaded - * @param activeTracks An array containing the list of track IDs to be set active for this - * media upon a successful load - * @param autoPlay If <code>true</code>, playback starts after load - * @param position Where to start the playback (only used if autoPlay is <code>true</code>). - * Units is milliseconds. - * @param customData Optional {@link JSONObject} data to be passed to the cast device - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void loadMedia(MediaInfo media, final long[] activeTracks, boolean autoPlay, - int position, JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "loadMedia"); - checkConnectivity(); - if (media == null) { - return; - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to load a video with no active media session"); - throw new NoConnectionException(); - } - - Log.d(TAG, "remoteMediaPlayer.load() with media=" + media.getMetadata().getString(MediaMetadata.KEY_TITLE) - + ", position=" + position + ", autoplay=" + autoPlay); - remoteMediaPlayer.load(mApiClient, media, autoPlay, position, activeTracks, customData) - .setResultCallback(result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaLoadResult(result.getStatus().getStatusCode()); - } - }); - } - - /** - * Loads and optionally starts playback of a new queue of media items. - * - * @param items Array of items to load, in the order that they should be played. Must not be - * {@code null} or empty. - * @param startIndex The array index of the item in the {@code items} array that should be - * played first (i.e., it will become the currentItem).If {@code repeatMode} - * is {@link MediaStatus#REPEAT_MODE_REPEAT_OFF} playback will end when the - * last item in the array is played. - * <p> - * This may be useful for continuation scenarios where the user was already - * using the sender application and in the middle decides to cast. This lets - * the sender application avoid mapping between the local and remote queue - * positions and/or avoid issuing an extra request to update the queue. - * <p> - * This value must be less than the length of {@code items}. - * @param repeatMode The repeat playback mode for the queue. One of - * {@link MediaStatus#REPEAT_MODE_REPEAT_OFF}, - * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL}, - * {@link MediaStatus#REPEAT_MODE_REPEAT_SINGLE} and - * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE}. - * @param customData Custom application-specific data to pass along with the request, may be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queueLoad(final MediaQueueItem[] items, final int startIndex, final int repeatMode, - final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queueLoad"); - checkConnectivity(); - if (items == null || items.length == 0) { - return; - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to queue one or more videos with no active media session"); - throw new NoConnectionException(); - } - Log.d(TAG, "remoteMediaPlayer.queueLoad() with " + items.length + "items, starting at " - + startIndex); - remoteMediaPlayer - .queueLoad(mApiClient, items, startIndex, repeatMode, customData) - .setResultCallback(result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_LOAD, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Plays the loaded media. - * - * @param position Where to start the playback. Units is milliseconds. - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void play(int position) throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - Log.d(TAG, "attempting to play media at position " + position + " seconds"); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to play a video with no active media session"); - throw new NoConnectionException(); - } - seekAndPlay(position); - } - - /** - * Resumes the playback from where it was left (can be the beginning). - * - * @param customData Optional {@link JSONObject} data to be passed to the cast device - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void play(JSONObject customData) throws - TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "play(customData)"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to play a video with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer.play(mApiClient, customData) - .setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_to_play, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Resumes the playback from where it was left (can be the beginning). - * - * @throws CastException - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void play() throws CastException, TransientNetworkDisconnectionException, - NoConnectionException { - play(null); - } - - /** - * Stops the playback of media/stream - * - * @param customData Optional {@link JSONObject} - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void stop(JSONObject customData) throws - TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "stop()"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to stop a stream with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer.stop(mApiClient, customData).setResultCallback( - result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_to_stop, - result.getStatus().getStatusCode()); - } - } - ); - } - - /** - * Stops the playback of media/stream - * - * @throws CastException - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void stop() throws CastException, - TransientNetworkDisconnectionException, NoConnectionException { - stop(null); - } - - /** - * Pauses the playback. - * - * @throws CastException - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void pause() throws CastException, TransientNetworkDisconnectionException, - NoConnectionException { - pause(null); - } - - /** - * Pauses the playback. - * - * @param customData Optional {@link JSONObject} data to be passed to the cast device - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void pause(JSONObject customData) throws - TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "attempting to pause media"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to pause a video with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer.pause(mApiClient, customData) - .setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_to_pause, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Seeks to the given point without changing the state of the player, i.e. after seek is - * completed, it resumes what it was doing before the start of seek. - * - * @param position in milliseconds - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void seek(int position) throws TransientNetworkDisconnectionException, - NoConnectionException { - Log.d(TAG, "attempting to seek media"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to seek a video with no active media session"); - throw new NoConnectionException(); - } - Log.d(TAG, "remoteMediaPlayer.seek() to position " + position); - remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_UNCHANGED).setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode()); - } - }); - } - - /** - * Fast forwards the media by the given amount. If {@code lengthInMillis} is negative, it - * rewinds the media. - * - * @param lengthInMillis The amount to fast forward the media, given in milliseconds - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void forward(int lengthInMillis) throws TransientNetworkDisconnectionException, - NoConnectionException { - Log.d(TAG, "forward(): attempting to forward media by " + lengthInMillis); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to seek a video with no active media session"); - throw new NoConnectionException(); - } - long position = remoteMediaPlayer.getApproximateStreamPosition() + lengthInMillis; - seek((int) position); - } - - /** - * Seeks to the given point and starts playback regardless of the starting state. - * - * @param position in milliseconds - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void seekAndPlay(int position) throws TransientNetworkDisconnectionException, - NoConnectionException { - Log.d(TAG, "attempting to seek media"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to seekAndPlay a video with no active media session"); - throw new NoConnectionException(); - } - Log.d(TAG, "remoteMediaPlayer.seek() to position " + position + "and play"); - remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_PLAY) - .setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode()); - } - }); - } - - private void attachMediaChannel() throws TransientNetworkDisconnectionException, - NoConnectionException { - Log.d(TAG, "attachMediaChannel()"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - remoteMediaPlayer = new RemoteMediaPlayer(); - - remoteMediaPlayer.setOnStatusUpdatedListener( - () -> { - Log.d(TAG, "RemoteMediaPlayer::onStatusUpdated() is reached"); - CastManager.this.onRemoteMediaPlayerStatusUpdated(); - } - ); - - remoteMediaPlayer.setOnPreloadStatusUpdatedListener( - () -> { - Log.d(TAG, "RemoteMediaPlayer::onPreloadStatusUpdated() is reached"); - CastManager.this.onRemoteMediaPreloadStatusUpdated(); - }); - - - remoteMediaPlayer.setOnMetadataUpdatedListener( - () -> { - Log.d(TAG, "RemoteMediaPlayer::onMetadataUpdated() is reached"); - CastManager.this.onRemoteMediaPlayerMetadataUpdated(); - } - ); - - remoteMediaPlayer.setOnQueueStatusUpdatedListener( - () -> { - Log.d(TAG, "RemoteMediaPlayer::onQueueStatusUpdated() is reached"); - mediaStatus = remoteMediaPlayer.getMediaStatus(); - if (mediaStatus != null - && mediaStatus.getQueueItems() != null) { - List<MediaQueueItem> queueItems = mediaStatus - .getQueueItems(); - int itemId = mediaStatus.getCurrentItemId(); - MediaQueueItem item = mediaStatus - .getQueueItemById(itemId); - int repeatMode = mediaStatus.getQueueRepeatMode(); - onQueueUpdated(queueItems, item, repeatMode, false); - } else { - onQueueUpdated(null, null, - MediaStatus.REPEAT_MODE_REPEAT_OFF, false); - } - }); - - } - try { - Log.d(TAG, "Registering MediaChannel namespace"); - Cast.CastApi.setMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace(), - remoteMediaPlayer); - } catch (IOException | IllegalStateException e) { - Log.e(TAG, "attachMediaChannel()", e); - } - } - - private void reattachMediaChannel() { - if (remoteMediaPlayer != null && mApiClient != null) { - try { - Log.d(TAG, "Registering MediaChannel namespace"); - Cast.CastApi.setMessageReceivedCallbacks(mApiClient, - remoteMediaPlayer.getNamespace(), remoteMediaPlayer); - } catch (IOException | IllegalStateException e) { - Log.e(TAG, "reattachMediaChannel()", e); - } - } - } - - private void detachMediaChannel() { - Log.d(TAG, "trying to detach media channel"); - if (remoteMediaPlayer != null) { - try { - Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, - remoteMediaPlayer.getNamespace()); - } catch (IOException | IllegalStateException e) { - Log.e(TAG, "detachMediaChannel()", e); - } - remoteMediaPlayer = null; - } - } - - /** - * Returns the latest retrieved value for the {@link MediaStatus}. This value is updated - * whenever the onStatusUpdated callback is called. - */ - public final MediaStatus getMediaStatus() { - return mediaStatus; - } - - /* - * This is called by onStatusUpdated() of the RemoteMediaPlayer - */ - private void onRemoteMediaPlayerStatusUpdated() { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated() reached"); - if (mApiClient == null || remoteMediaPlayer == null) { - Log.d(TAG, "mApiClient or remoteMediaPlayer is null, so will not proceed"); - return; - } - mediaStatus = remoteMediaPlayer.getMediaStatus(); - if (mediaStatus == null) { - Log.d(TAG, "MediaStatus is null, so will not proceed"); - return; - } else { - List<MediaQueueItem> queueItems = mediaStatus.getQueueItems(); - if (queueItems != null) { - int itemId = mediaStatus.getCurrentItemId(); - MediaQueueItem item = mediaStatus.getQueueItemById(itemId); - int repeatMode = mediaStatus.getQueueRepeatMode(); - onQueueUpdated(queueItems, item, repeatMode, false); - } else { - onQueueUpdated(null, null, MediaStatus.REPEAT_MODE_REPEAT_OFF, false); - } - state = mediaStatus.getPlayerState(); - int idleReason = mediaStatus.getIdleReason(); - - if (state == MediaStatus.PLAYER_STATE_PLAYING) { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = playing"); - } else if (state == MediaStatus.PLAYER_STATE_PAUSED) { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = paused"); - } else if (state == MediaStatus.PLAYER_STATE_IDLE) { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = IDLE with reason: " - + idleReason); - if (idleReason == MediaStatus.IDLE_REASON_ERROR) { - // something bad happened on the cast device - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): IDLE reason = ERROR"); - onFailed(R.string.cast_failed_receiver_player_error, NO_STATUS_CODE); - } - } else if (state == MediaStatus.PLAYER_STATE_BUFFERING) { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = buffering"); - } else { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = unknown"); - } - } - for (CastConsumer consumer : castConsumers) { - consumer.onRemoteMediaPlayerStatusUpdated(); - } - if (mediaStatus != null) { - double volume = mediaStatus.getStreamVolume(); - boolean isMute = mediaStatus.isMute(); - for (CastConsumer consumer : castConsumers) { - consumer.onStreamVolumeChanged(volume, isMute); - } - } - } - - private void onRemoteMediaPreloadStatusUpdated() { - MediaQueueItem item = null; - mediaStatus = remoteMediaPlayer.getMediaStatus(); - if (mediaStatus != null) { - item = mediaStatus.getQueueItemById(mediaStatus.getPreloadedItemId()); - } - Log.d(TAG, "onRemoteMediaPreloadStatusUpdated() " + item); - for (CastConsumer consumer : castConsumers) { - consumer.onRemoteMediaPreloadStatusUpdated(item); - } - } - - /* - * This is called by onQueueStatusUpdated() of RemoteMediaPlayer - */ - private void onQueueUpdated(List<MediaQueueItem> queueItems, MediaQueueItem item, - int repeatMode, boolean shuffle) { - Log.d(TAG, "onQueueUpdated() reached"); - Log.d(TAG, String.format(Locale.US, "Queue Items size: %d, Item: %s, Repeat Mode: %d, Shuffle: %s", - queueItems == null ? 0 : queueItems.size(), item, repeatMode, shuffle)); - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueUpdated(queueItems, item, repeatMode, shuffle); - } - } - - /* - * This is called by onMetadataUpdated() of RemoteMediaPlayer - */ - public void onRemoteMediaPlayerMetadataUpdated() { - Log.d(TAG, "onRemoteMediaPlayerMetadataUpdated() reached"); - for (CastConsumer consumer : castConsumers) { - consumer.onRemoteMediaPlayerMetadataUpdated(); - } - } - - /** - * Registers a {@link CastConsumer} interface with this class. - * Registered listeners will be notified of changes to a variety of - * lifecycle and media status changes through the callbacks that the interface provides. - * - * @see DefaultCastConsumer - */ - public synchronized void addCastConsumer(CastConsumer listener) { - if (listener != null) { - addBaseCastConsumer(listener); - castConsumers.add(listener); - Log.d(TAG, "Successfully added the new CastConsumer listener " + listener); - } - } - - /** - * Unregisters a {@link CastConsumer}. - */ - public synchronized void removeCastConsumer(CastConsumer listener) { - if (listener != null) { - removeBaseCastConsumer(listener); - castConsumers.remove(listener); - } - } - - @Override - protected void onDeviceUnselected() { - detachMediaChannel(); - //removeDataChannel(); - state = MediaStatus.PLAYER_STATE_IDLE; - mediaStatus = null; - } - - @Override - protected Cast.CastOptions.Builder getCastOptionBuilder(CastDevice device) { - Cast.CastOptions.Builder builder = new Cast.CastOptions.Builder(mSelectedCastDevice, new CastListener()); - if (isFeatureEnabled(CastConfiguration.FEATURE_DEBUGGING)) { - builder.setVerboseLoggingEnabled(true); - } - return builder; - } - - @Override - public void onConnectionFailed(ConnectionResult result) { - super.onConnectionFailed(result); - state = MediaStatus.PLAYER_STATE_IDLE; - mediaStatus = null; - } - - @Override - public void onDisconnected(boolean stopAppOnExit, boolean clearPersistedConnectionData, - boolean setDefaultRoute) { - super.onDisconnected(stopAppOnExit, clearPersistedConnectionData, setDefaultRoute); - state = MediaStatus.PLAYER_STATE_IDLE; - mediaStatus = null; - } - - class CastListener extends Cast.Listener { - - /* - * (non-Javadoc) - * @see com.google.android.gms.cast.Cast.Listener#onApplicationDisconnected (int) - */ - @Override - public void onApplicationDisconnected(int statusCode) { - CastManager.this.onApplicationDisconnected(statusCode); - } - - /* - * (non-Javadoc) - * @see com.google.android.gms.cast.Cast.Listener#onApplicationStatusChanged () - */ - @Override - public void onApplicationStatusChanged() { - CastManager.this.onApplicationStatusChanged(); - } - - @Override - public void onVolumeChanged() { - CastManager.this.onDeviceVolumeChanged(); - } - } - - @Override - public void onFailed(int resourceId, int statusCode) { - Log.d(TAG, "onFailed: " + mContext.getString(resourceId) + ", code: " + statusCode); - super.onFailed(resourceId, statusCode); - } - - /** - * Checks whether the selected Cast Device has the specified audio or video capabilities. - * - * @param capability capability from: - * <ul> - * <li>{@link CastDevice#CAPABILITY_AUDIO_IN}</li> - * <li>{@link CastDevice#CAPABILITY_AUDIO_OUT}</li> - * <li>{@link CastDevice#CAPABILITY_VIDEO_IN}</li> - * <li>{@link CastDevice#CAPABILITY_VIDEO_OUT}</li> - * </ul> - * @param defaultVal value to return whenever there's no device selected. - * @return {@code true} if the selected device has the specified capability, - * {@code false} otherwise. - */ - public boolean hasCapability(final int capability, final boolean defaultVal) { - if (mSelectedCastDevice != null) { - return mSelectedCastDevice.hasCapability(capability); - } else { - return defaultVal; - } - } - - /** - * Adds and wires up the Switchable Media Router cast button. It returns a reference to the - * {@link SwitchableMediaRouteActionProvider} associated with the button if the caller needs - * such reference. It is assumed that the enclosing - * {@link android.app.Activity} inherits (directly or indirectly) from - * {@link androidx.appcompat.app.AppCompatActivity}. - * - * @param menuItem MenuItem of the Media Router cast button. - */ - public final SwitchableMediaRouteActionProvider addMediaRouterButton(@NonNull MenuItem menuItem) { - ActionProvider actionProvider = MenuItemCompat.getActionProvider(menuItem); - if (!(actionProvider instanceof SwitchableMediaRouteActionProvider)) { - Log.wtf(TAG, "MenuItem provided to addMediaRouterButton() is not compatible with " + - "SwitchableMediaRouteActionProvider." + - ((actionProvider == null) ? " Its action provider is null!" : ""), - new ClassCastException()); - return null; - } - SwitchableMediaRouteActionProvider mediaRouteActionProvider = - (SwitchableMediaRouteActionProvider) actionProvider; - mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector); - if (mCastConfiguration.getMediaRouteDialogFactory() != null) { - mediaRouteActionProvider.setDialogFactory(mCastConfiguration.getMediaRouteDialogFactory()); - } - return mediaRouteActionProvider; - } - - /* (non-Javadoc) - * These methods startReconnectionService and stopReconnectionService simply override the ones - * from BaseCastManager with empty implementations because we handle the service ourselves, but - * need to allow BaseCastManager to save current network information. - */ - @Override - protected void startReconnectionService(long mediaDurationLeft) { - // Do nothing - } - - @Override - protected void stopReconnectionService() { - // Do nothing - } -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java deleted file mode 100644 index e1f52aa9f..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastUtils.java +++ /dev/null @@ -1,303 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import android.content.ContentResolver; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.common.images.WebImage; - -import java.util.Calendar; -import java.util.List; - -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.model.playback.RemoteMedia; -import de.danoeh.antennapod.core.storage.DBReader; - -/** - * Helper functions for Cast support. - */ -public class CastUtils { - private CastUtils(){} - - private static final String TAG = "CastUtils"; - - public static final String KEY_MEDIA_ID = "de.danoeh.antennapod.core.cast.MediaId"; - - public static final String KEY_EPISODE_IDENTIFIER = "de.danoeh.antennapod.core.cast.EpisodeId"; - public static final String KEY_EPISODE_LINK = "de.danoeh.antennapod.core.cast.EpisodeLink"; - public static final String KEY_FEED_URL = "de.danoeh.antennapod.core.cast.FeedUrl"; - public static final String KEY_FEED_WEBSITE = "de.danoeh.antennapod.core.cast.FeedWebsite"; - public static final String KEY_EPISODE_NOTES = "de.danoeh.antennapod.core.cast.EpisodeNotes"; - - /** - * The field <code>AntennaPod.FormatVersion</code> specifies which version of MediaMetaData - * fields we're using. Future implementations should try to be backwards compatible with earlier - * versions, and earlier versions should be forward compatible until the version indicated by - * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code>. If an update makes the format unreadable for - * an earlier version, then its version number should be greater than the - * <code>MAX_VERSION_FORWARD_COMPATIBILITY</code> value set on the earlier one, so that it - * doesn't try to parse the object. - */ - public static final String KEY_FORMAT_VERSION = "de.danoeh.antennapod.core.cast.FormatVersion"; - public static final int FORMAT_VERSION_VALUE = 1; - public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999; - - public static boolean isCastable(Playable media) { - if (media == null) { - return false; - } - if (media instanceof FeedMedia || media instanceof RemoteMedia) { - String url = media.getStreamUrl(); - if (url == null || url.isEmpty()) { - return false; - } - if (url.startsWith(ContentResolver.SCHEME_CONTENT)) { - return false; // Local feed - } - switch (media.getMediaType()) { - case UNKNOWN: - return false; - case AUDIO: - return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT, true); - case VIDEO: - return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT, true); - } - } - return false; - } - - /** - * Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device. - * Before using this method, one should make sure {@link #isCastable(Playable)} returns - * {@code true}. This method should not run on the main thread. - * - * @param media The {@link FeedMedia} object to be converted. - * @return {@link MediaInfo} object in a format proper for casting. - */ - public static MediaInfo convertFromFeedMedia(FeedMedia media){ - if (media == null) { - return null; - } - MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC); - if (media.getItem() == null) { - media.setItem(DBReader.getFeedItem(media.getItemId())); - } - FeedItem feedItem = media.getItem(); - if (feedItem != null) { - metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle()); - String subtitle = media.getFeedTitle(); - if (subtitle != null) { - metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle); - } - - if (!TextUtils.isEmpty(feedItem.getImageLocation())) { - metadata.addImage(new WebImage(Uri.parse(feedItem.getImageLocation()))); - } - Calendar calendar = Calendar.getInstance(); - calendar.setTime(media.getItem().getPubDate()); - metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar); - Feed feed = feedItem.getFeed(); - if (feed != null) { - if (!TextUtils.isEmpty(feed.getAuthor())) { - metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor()); - } - if (!TextUtils.isEmpty(feed.getDownload_url())) { - metadata.putString(KEY_FEED_URL, feed.getDownload_url()); - } - if (!TextUtils.isEmpty(feed.getLink())) { - metadata.putString(KEY_FEED_WEBSITE, feed.getLink()); - } - } - if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) { - metadata.putString(KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier()); - } else { - metadata.putString(KEY_EPISODE_IDENTIFIER, media.getStreamUrl()); - } - if (!TextUtils.isEmpty(feedItem.getLink())) { - metadata.putString(KEY_EPISODE_LINK, feedItem.getLink()); - } - try { - DBReader.loadDescriptionOfFeedItem(feedItem); - metadata.putString(KEY_EPISODE_NOTES, feedItem.getDescription()); - } catch (Exception e) { - Log.e(TAG, "Unable to load FeedMedia notes", e); - } - } - // This field only identifies the id on the device that has the original version. - // Idea is to perhaps, on a first approach, check if the version on the local DB with the - // same id matches the remote object, and if not then search for episode and feed identifiers. - // This at least should make media recognition for a single device much quicker. - metadata.putInt(KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue()); - // A way to identify different casting media formats in case we change it in the future and - // senders with different versions share a casting device. - metadata.putInt(KEY_FORMAT_VERSION, FORMAT_VERSION_VALUE); - - MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl()) - .setContentType(media.getMime_type()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(metadata); - if (media.getDuration() > 0) { - builder.setStreamDuration(media.getDuration()); - } - return builder.build(); - } - - //TODO make unit tests for all the conversion methods - /** - * Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}. - * - * Unless <code>searchFeedMedia</code> is set to <code>false</code>, this method should not run - * on the GUI thread. - * - * @param media The {@link MediaInfo} object to be converted. - * @param searchFeedMedia If set to <code>true</code>, the database will be queried to find a - * {@link FeedMedia} instance that matches {@param media}. - * @return {@link Playable} object in a format proper for casting. - */ - public static Playable getPlayable(MediaInfo media, boolean searchFeedMedia) { - Log.d(TAG, "getPlayable called with searchFeedMedia=" + searchFeedMedia); - if (media == null) { - Log.d(TAG, "MediaInfo object provided is null, not converting to any Playable instance"); - return null; - } - MediaMetadata metadata = media.getMetadata(); - int version = metadata.getInt(KEY_FORMAT_VERSION); - if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) { - Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this" + - "version of AntennaPod CastUtils, curVer=" + FORMAT_VERSION_VALUE + - ", object version=" + version); - return null; - } - Playable result = null; - if (searchFeedMedia) { - long mediaId = metadata.getInt(KEY_MEDIA_ID); - if (mediaId > 0) { - FeedMedia fMedia = DBReader.getFeedMedia(mediaId); - if (fMedia != null) { - if (matches(media, fMedia)) { - result = fMedia; - Log.d(TAG, "FeedMedia object obtained matches the MediaInfo provided. id=" + mediaId); - } else { - Log.d(TAG, "FeedMedia object obtained does NOT match the MediaInfo provided. id=" + mediaId); - } - } else { - Log.d(TAG, "Unable to find in database a FeedMedia with id=" + mediaId); - } - } - if (result == null) { - FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(null, - metadata.getString(KEY_EPISODE_IDENTIFIER)); - if (feedItem != null) { - result = feedItem.getMedia(); - Log.d(TAG, "Found episode that matches the MediaInfo provided. Using its media, if existing."); - } - } - } - if (result == null) { - List<WebImage> imageList = metadata.getImages(); - String imageUrl = null; - if (!imageList.isEmpty()) { - imageUrl = imageList.get(0).getUrl().toString(); - } - String notes = metadata.getString(KEY_EPISODE_NOTES); - result = new RemoteMedia(media.getContentId(), - metadata.getString(KEY_EPISODE_IDENTIFIER), - metadata.getString(KEY_FEED_URL), - metadata.getString(MediaMetadata.KEY_SUBTITLE), - metadata.getString(MediaMetadata.KEY_TITLE), - metadata.getString(KEY_EPISODE_LINK), - metadata.getString(MediaMetadata.KEY_ARTIST), - imageUrl, - metadata.getString(KEY_FEED_WEBSITE), - media.getContentType(), - metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime(), - notes); - Log.d(TAG, "Converted MediaInfo into RemoteMedia"); - } - if (result.getDuration() == 0 && media.getStreamDuration() > 0) { - result.setDuration((int) media.getStreamDuration()); - } - return result; - } - - /** - * Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they - * represent the same podcast episode. - * - * @param info the {@link MediaInfo} object to be compared. - * @param media the {@link FeedMedia} object to be compared. - * @return <true>true</true> if there's a match, <code>false</code> otherwise. - * - * @see RemoteMedia#equals(Object) - */ - public static boolean matches(MediaInfo info, FeedMedia media) { - if (info == null || media == null) { - return false; - } - if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) { - return false; - } - MediaMetadata metadata = info.getMetadata(); - FeedItem fi = media.getItem(); - if (fi == null || metadata == null || - !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) { - return false; - } - Feed feed = fi.getFeed(); - return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url()); - } - - /** - * Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they - * represent the same podcast episode. - * - * @param info the {@link MediaInfo} object to be compared. - * @param media the {@link RemoteMedia} object to be compared. - * @return <true>true</true> if there's a match, <code>false</code> otherwise. - * - * @see RemoteMedia#equals(Object) - */ - public static boolean matches(MediaInfo info, RemoteMedia media) { - if (info == null || media == null) { - return false; - } - if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) { - return false; - } - MediaMetadata metadata = info.getMetadata(); - return metadata != null && - TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier()) && - TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl()); - } - - /** - * Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they - * represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device - * and want to avoid unnecessary conversions. - * - * @param info the {@link MediaInfo} object to be compared. - * @param media the {@link Playable} object to be compared. - * @return <true>true</true> if there's a match, <code>false</code> otherwise. - * - * @see RemoteMedia#equals(Object) - */ - public static boolean matches(MediaInfo info, Playable media) { - if (info == null || media == null) { - return false; - } - if (media instanceof RemoteMedia) { - return matches(info, (RemoteMedia) media); - } - return media instanceof FeedMedia && matches(info, (FeedMedia) media); - } - - - //TODO Queue handling perhaps -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java b/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java deleted file mode 100644 index fe4183d54..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumerImpl; - -public class DefaultCastConsumer extends VideoCastConsumerImpl implements CastConsumer { - @Override - public void onStreamVolumeChanged(double value, boolean isMute) { - // no-op - } -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java b/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java deleted file mode 100644 index 00011ef05..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/MediaInfoCreator.java +++ /dev/null @@ -1,57 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import android.net.Uri; -import android.text.TextUtils; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.common.images.WebImage; -import de.danoeh.antennapod.model.playback.RemoteMedia; -import java.util.Calendar; - -public class MediaInfoCreator { - public static MediaInfo from(RemoteMedia media) { - MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC); - - metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle()); - metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle()); - if (!TextUtils.isEmpty(media.getImageLocation())) { - metadata.addImage(new WebImage(Uri.parse(media.getImageLocation()))); - } - Calendar calendar = Calendar.getInstance(); - calendar.setTime(media.getPubDate()); - metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar); - if (!TextUtils.isEmpty(media.getFeedAuthor())) { - metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor()); - } - if (!TextUtils.isEmpty(media.getFeedUrl())) { - metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl()); - } - if (!TextUtils.isEmpty(media.getFeedLink())) { - metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink()); - } - if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) { - metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier()); - } else { - metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl()); - } - if (!TextUtils.isEmpty(media.getEpisodeLink())) { - metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink()); - } - String notes = media.getNotes(); - if (notes != null) { - metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes); - } - // Default id value - metadata.putInt(CastUtils.KEY_MEDIA_ID, 0); - metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE); - - MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl()) - .setContentType(media.getMimeType()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(metadata); - if (media.getDuration() > 0) { - builder.setStreamDuration(media.getDuration()); - } - return builder.build(); - } -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java b/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java deleted file mode 100644 index 5a6a0aa2b..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java +++ /dev/null @@ -1,106 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import android.app.Activity; -import android.content.Context; -import android.content.ContextWrapper; -import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.mediarouter.app.MediaRouteActionProvider; -import androidx.mediarouter.app.MediaRouteChooserDialogFragment; -import androidx.mediarouter.app.MediaRouteControllerDialogFragment; -import androidx.mediarouter.media.MediaRouter; -import android.util.Log; - -/** - * <p>Action Provider that extends {@link MediaRouteActionProvider} and allows the client to - * disable completely the button by calling {@link #setEnabled(boolean)}.</p> - * - * <p>It is disabled by default, so if a client wants to initially have it enabled it must call - * <code>setEnabled(true)</code>.</p> - */ -public class SwitchableMediaRouteActionProvider extends MediaRouteActionProvider { - public static final String TAG = "SwitchblMediaRtActProv"; - - private static final String CHOOSER_FRAGMENT_TAG = - "android.support.v7.mediarouter:MediaRouteChooserDialogFragment"; - private static final String CONTROLLER_FRAGMENT_TAG = - "android.support.v7.mediarouter:MediaRouteControllerDialogFragment"; - private boolean enabled; - - public SwitchableMediaRouteActionProvider(Context context) { - super(context); - enabled = false; - } - - /** - * <p>Sets whether the Media Router button should be allowed to become visible or not.</p> - * - * <p>It's invisible by default.</p> - */ - public void setEnabled(boolean newVal) { - enabled = newVal; - refreshVisibility(); - } - - @Override - public boolean isVisible() { - return enabled && super.isVisible(); - } - - @Override - public boolean onPerformDefaultAction() { - if (!super.onPerformDefaultAction()) { - // there is no button, but we should still show the dialog if it's the case. - if (!isVisible()) { - return false; - } - FragmentManager fm = getFragmentManager(); - if (fm == null) { - return false; - } - MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute(); - if (route.isDefault() || !route.matchesSelector(getRouteSelector())) { - if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) { - Log.w(TAG, "showDialog(): Route chooser dialog already showing!"); - return false; - } - MediaRouteChooserDialogFragment f = - getDialogFactory().onCreateChooserDialogFragment(); - f.setRouteSelector(getRouteSelector()); - f.show(fm, CHOOSER_FRAGMENT_TAG); - } else { - if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) { - Log.w(TAG, "showDialog(): Route controller dialog already showing!"); - return false; - } - MediaRouteControllerDialogFragment f = - getDialogFactory().onCreateControllerDialogFragment(); - f.show(fm, CONTROLLER_FRAGMENT_TAG); - } - return true; - - } else { - return true; - } - } - - private FragmentManager getFragmentManager() { - Activity activity = getActivity(); - if (activity instanceof FragmentActivity) { - return ((FragmentActivity)activity).getSupportFragmentManager(); - } - return null; - } - - private Activity getActivity() { - // Gross way of unwrapping the Activity so we can get the FragmentManager - Context context = getContext(); - while (context instanceof ContextWrapper) { - if (context instanceof Activity) { - return (Activity)context; - } - context = ((ContextWrapper)context).getBaseContext(); - } - return null; - } -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java deleted file mode 100644 index 41fd01441..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java +++ /dev/null @@ -1,314 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.NetworkInfo; -import android.net.wifi.WifiManager; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; -import androidx.mediarouter.media.MediaRouter; -import android.support.wearable.media.MediaControlConstants; -import android.util.Log; -import android.widget.Toast; - -import com.google.android.gms.cast.ApplicationMetadata; -import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import de.danoeh.antennapod.core.cast.CastConsumer; -import de.danoeh.antennapod.core.cast.CastManager; -import de.danoeh.antennapod.core.cast.DefaultCastConsumer; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.model.playback.MediaType; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.NetworkUtils; -import org.greenrobot.eventbus.EventBus; - -/** - * Class intended to work along PlaybackService and provide support for different flavors. - */ -public class PlaybackServiceFlavorHelper { - public static final String TAG = "PlaybackSrvFlavorHelper"; - - /** - * Time in seconds during which the CastManager will try to reconnect to the Cast Device after - * the Wifi Connection is regained. - */ - private static final int RECONNECTION_ATTEMPT_PERIOD_S = 15; - /** - * Stores the state of the cast playback just before it disconnects. - */ - private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection; - - private boolean wifiConnectivity = true; - private BroadcastReceiver wifiBroadcastReceiver; - - private CastManager castManager; - private MediaRouter mediaRouter; - private PlaybackService.FlavorHelperCallback callback; - private CastConsumer castConsumer; - - PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) { - this.callback = callback; - if (!CastManager.isInitialized()) { - return; - } - mediaRouter = MediaRouter.getInstance(context.getApplicationContext()); - setCastConsumer(context); - } - - void initializeMediaPlayer(Context context) { - if (!CastManager.isInitialized()) { - callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback())); - return; - } - castManager = CastManager.getInstance(); - castManager.addCastConsumer(castConsumer); - boolean isCasting = castManager.isConnected(); - callback.setIsCasting(isCasting); - if (isCasting) { - if (UserPreferences.isCastEnabled()) { - onCastAppConnected(context, false); - } else { - castManager.disconnect(); - } - } else { - callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback())); - } - } - - void removeCastConsumer() { - if (!CastManager.isInitialized()) { - return; - } - castManager.removeCastConsumer(castConsumer); - } - - boolean castDisconnect(boolean castDisconnect) { - if (!CastManager.isInitialized()) { - return false; - } - if (castDisconnect) { - castManager.disconnect(); - } - return castDisconnect; - } - - boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) { - if (!CastManager.isInitialized()) { - return false; - } - switch (code) { - case RemotePSMP.CAST_ERROR: - EventBus.getDefault().post(new MessageEvent(context.getString(resourceId))); - return true; - case RemotePSMP.CAST_ERROR_PRIORITY_HIGH: - Toast.makeText(context, resourceId, Toast.LENGTH_SHORT).show(); - return true; - default: - return false; - } - } - - private void setCastConsumer(Context context) { - castConsumer = new DefaultCastConsumer() { - @Override - public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) { - onCastAppConnected(context, wasLaunched); - } - - @Override - public void onDisconnectionReason(int reason) { - Log.d(TAG, "onDisconnectionReason() with code " + reason); - // This is our final chance to update the underlying stream position - // In onDisconnected(), the underlying CastPlayback#mVideoCastConsumer - // is disconnected and hence we update our local value of stream position - // to the latest position. - PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer(); - if (mediaPlayer != null) { - callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME); - infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo(); - if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT && - infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) { - // If it's NOT based on user action, we shouldn't automatically resume local playback - infoBeforeCastDisconnection.playerStatus = PlayerStatus.PAUSED; - } - } - } - - @Override - public void onDisconnected() { - Log.d(TAG, "onDisconnected()"); - callback.setIsCasting(false); - PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection; - infoBeforeCastDisconnection = null; - PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer(); - if (info == null && mediaPlayer != null) { - info = mediaPlayer.getPSMPInfo(); - } - if (info == null) { - info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.INDETERMINATE, - PlayerStatus.STOPPED, null); - } - switchMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback()), - info, true); - if (info.playable != null) { - callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_RELOAD, - info.playable.getMediaType() == MediaType.AUDIO ? - PlaybackService.EXTRA_CODE_AUDIO : PlaybackService.EXTRA_CODE_VIDEO); - } else { - Log.d(TAG, "Cast session disconnected, but no current media"); - callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END, 0); - } - // hardware volume buttons control the local device volume - mediaRouter.setMediaSessionCompat(null); - unregisterWifiBroadcastReceiver(); - callback.setupNotification(false, info); - } - }; - } - - private void onCastAppConnected(Context context, boolean wasLaunched) { - Log.d(TAG, "A cast device application was " + (wasLaunched ? "launched" : "joined")); - callback.setIsCasting(true); - PlaybackServiceMediaPlayer.PSMPInfo info = null; - PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer(); - if (mediaPlayer != null) { - info = mediaPlayer.getPSMPInfo(); - if (info.playerStatus == PlayerStatus.PLAYING) { - // could be pause, but this way we make sure the new player will get the correct position, - // since pause runs asynchronously and we could be directing the new player to play even before - // the old player gives us back the position. - callback.saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME); - } - } - if (info == null) { - info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.INDETERMINATE, PlayerStatus.STOPPED, null); - } - callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_RELOAD, - PlaybackService.EXTRA_CODE_CAST); - RemotePSMP remotePSMP = new RemotePSMP(context, callback.getMediaPlayerCallback()); - switchMediaPlayer(remotePSMP, info, wasLaunched); - remotePSMP.init(); - // hardware volume buttons control the remote device volume - mediaRouter.setMediaSessionCompat(callback.getMediaSession()); - registerWifiBroadcastReceiver(); - callback.setupNotification(true, info); - } - - private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer, - @NonNull PlaybackServiceMediaPlayer.PSMPInfo info, - boolean wasLaunched) { - PlaybackServiceMediaPlayer mediaPlayer = callback.getMediaPlayer(); - if (mediaPlayer != null) { - try { - mediaPlayer.stopPlayback(false).get(2, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - Log.e(TAG, "There was a problem stopping playback while switching media players", e); - } - mediaPlayer.shutdownQuietly(); - } - mediaPlayer = newPlayer; - callback.setMediaPlayer(mediaPlayer); - Log.d(TAG, "switched to " + mediaPlayer.getClass().getSimpleName()); - if (!wasLaunched) { - PlaybackServiceMediaPlayer.PSMPInfo candidate = mediaPlayer.getPSMPInfo(); - if (candidate.playable != null && - candidate.playerStatus.isAtLeast(PlayerStatus.PREPARING)) { - // do not automatically send new media to cast device - info.playable = null; - } - } - if (info.playable != null) { - mediaPlayer.playMediaObject(info.playable, - !info.playable.localFileAvailable(), - info.playerStatus == PlayerStatus.PLAYING, - info.playerStatus.isAtLeast(PlayerStatus.PREPARING)); - } - } - - void registerWifiBroadcastReceiver() { - if (!CastManager.isInitialized()) { - return; - } - if (wifiBroadcastReceiver != null) { - return; - } - wifiBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { - NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); - boolean isConnected = info.isConnected(); - //apparently this method gets called twice when a change happens, but one run is enough. - if (isConnected && !wifiConnectivity) { - wifiConnectivity = true; - castManager.startCastDiscovery(); - castManager.reconnectSessionIfPossible(RECONNECTION_ATTEMPT_PERIOD_S, NetworkUtils.getWifiSsid()); - } else { - wifiConnectivity = isConnected; - } - } - } - }; - callback.registerReceiver(wifiBroadcastReceiver, - new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION)); - } - - void unregisterWifiBroadcastReceiver() { - if (!CastManager.isInitialized()) { - return; - } - if (wifiBroadcastReceiver != null) { - callback.unregisterReceiver(wifiBroadcastReceiver); - wifiBroadcastReceiver = null; - } - } - - boolean onSharedPreference(String key) { - if (!CastManager.isInitialized()) { - return false; - } - if (UserPreferences.PREF_CAST_ENABLED.equals(key)) { - if (!UserPreferences.isCastEnabled()) { - if (castManager.isConnecting() || castManager.isConnected()) { - Log.d(TAG, "Disconnecting cast device due to a change in user preferences"); - castManager.disconnect(); - } - } - return true; - } - return false; - } - - void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) { - if (!CastManager.isInitialized()) { - return; - } - PlaybackStateCompat.CustomAction.Builder actionBuilder = - new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon); - Bundle actionExtras = new Bundle(); - actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true); - actionBuilder.setExtras(actionExtras); - - sessionState.addCustomAction(actionBuilder.build()); - } - - void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) { - if (!CastManager.isInitialized()) { - return; - } - Bundle sessionExtras = new Bundle(); - sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true); - sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true); - mediaSession.setExtras(sessionExtras); - } -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java deleted file mode 100644 index 38b469e8e..000000000 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java +++ /dev/null @@ -1,680 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -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 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; - -/** - * Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices. - */ -public class RemotePSMP 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; - - private volatile Playable media; - private volatile MediaType mediaType; - private volatile MediaInfo remoteMedia; - private volatile int remoteState; - - private final AtomicBoolean isBuffering; - - private final AtomicBoolean startWhenPrepared; - - public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) { - super(context, callback); - - castMgr = CastManager.getInstance(); - media = null; - mediaType = null; - startWhenPrepared = new AtomicBoolean(false); - isBuffering = new AtomicBoolean(false); - remoteState = MediaStatus.PLAYER_STATE_UNKNOWN; - } - - public void init() { - try { - if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) { - onRemoteMediaPlayerStatusUpdated(); - } - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to do initial check for loaded media", e); - } - - castMgr.addCastConsumer(castConsumer); - } - - private CastConsumer castConsumer = new DefaultCastConsumer() { - @Override - public void onRemoteMediaPlayerMetadataUpdated() { - RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(); - } - - @Override - public void onRemoteMediaPlayerStatusUpdated() { - RemotePSMP.this.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"); - } - } - - @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); - } - }; - - private void setBuffering(boolean buffering) { - if (buffering && isBuffering.compareAndSet(false, true)) { - callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0); - } else if (!buffering && isBuffering.compareAndSet(true, false)) { - callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0); - } - } - - private Playable localVersion(MediaInfo info){ - if (info == null) { - return null; - } - if (CastUtils.matches(info, media)) { - return media; - } - return CastUtils.getPlayable(info, true); - } - - private MediaInfo remoteVersion(Playable playable) { - if (playable == null) { - return null; - } - if (CastUtils.matches(remoteMedia, playable)) { - return remoteMedia; - } - if (playable instanceof FeedMedia) { - return CastUtils.convertFromFeedMedia((FeedMedia) playable); - } - if (playable instanceof RemoteMedia) { - return MediaInfoCreator.from((RemoteMedia) playable); - } - return null; - } - - private void onRemoteMediaPlayerStatusUpdated() { - MediaStatus status = castMgr.getMediaStatus(); - if (status == null) { - Log.d(TAG, "Received null MediaStatus"); - return; - } else { - Log.d(TAG, "Received remote status/media update. New state=" + status.getPlayerState()); - } - int state = status.getPlayerState(); - int oldState = remoteState; - remoteMedia = status.getMediaInfo(); - boolean mediaChanged = !CastUtils.matches(remoteMedia, media); - boolean stateChanged = state != oldState; - if (!mediaChanged && !stateChanged) { - Log.d(TAG, "Both media and state haven't changed, so nothing to do"); - return; - } - Playable currentMedia = mediaChanged ? localVersion(remoteMedia) : media; - Playable oldMedia = media; - int position = (int) status.getStreamPosition(); - // check for incompatible states - if ((state == MediaStatus.PLAYER_STATE_PLAYING || state == MediaStatus.PLAYER_STATE_PAUSED) - && currentMedia == null) { - Log.w(TAG, "RemoteMediaPlayer returned playing or pausing state, but with no media"); - state = MediaStatus.PLAYER_STATE_UNKNOWN; - stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN; - } - - if (stateChanged) { - remoteState = state; - } - - 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); - } - - setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING); - - switch (state) { - case MediaStatus.PLAYER_STATE_PLAYING: - if (!stateChanged) { - //These steps are necessary because they won't be performed by setPlayerStatus() - if (position >= 0) { - currentMedia.setPosition(position); - } - currentMedia.onPlaybackStart(); - } - setPlayerStatus(PlayerStatus.PLAYING, currentMedia, position); - break; - case MediaStatus.PLAYER_STATE_PAUSED: - setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position); - break; - case MediaStatus.PLAYER_STATE_BUFFERING: - setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING) ? - PlayerStatus.PREPARING : PlayerStatus.SEEKING, - currentMedia, - currentMedia != null ? currentMedia.getPosition() : INVALID_TIME); - break; - case MediaStatus.PLAYER_STATE_IDLE: - int reason = status.getIdleReason(); - switch (reason) { - case MediaStatus.IDLE_REASON_CANCELED: - // Essentially means stopped at the request of a user - callback.onPlaybackEnded(null, true); - setPlayerStatus(PlayerStatus.STOPPED, currentMedia); - if (oldMedia != null) { - if (position >= 0) { - oldMedia.setPosition(position); - } - callback.onPostPlayback(oldMedia, false, false, false); - } - // onPlaybackEnded pretty much takes care of updating the UI - return; - case MediaStatus.IDLE_REASON_INTERRUPTED: - // Means that a request to load a different media was sent - // Not sure if currentMedia already reflects the to be loaded one - if (mediaChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING) { - callback.onPlaybackPause(null, INVALID_TIME); - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); - } - setPlayerStatus(PlayerStatus.PREPARING, currentMedia); - break; - case MediaStatus.IDLE_REASON_NONE: - // This probably only happens when we connected but no command has been sent yet. - setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia); - break; - case MediaStatus.IDLE_REASON_FINISHED: - // This is our onCompletionListener... - if (mediaChanged && currentMedia != null) { - media = currentMedia; - } - 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); - endPlayback(false, false, true, true); - return; - } - break; - case MediaStatus.PLAYER_STATE_UNKNOWN: - if (playerStatus != PlayerStatus.INDETERMINATE || media != currentMedia) { - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); - } - break; - default: - Log.wtf(TAG, "Remote media state undetermined!"); - } - if (mediaChanged) { - callback.onMediaChanged(true); - if (oldMedia != null) { - callback.onPostPlayback(oldMedia, false, false, currentMedia != null); - } - } - } - - @Override - 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 - * 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)) { - Log.d(TAG, "media provided is not compatible with cast device"); - callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable); - Playable nextPlayable = playable; - do { - nextPlayable = callback.getNextInQueue(nextPlayable); - } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable)); - if (nextPlayable != null) { - playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately); - } - return; - } - - if (media != null) { - if (!forceReset && media.getIdentifier().equals(playable.getIdentifier()) - && playerStatus == PlayerStatus.PLAYING) { - // episode is already playing -> ignore method call - Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); - return; - } else { - // 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); - } - if (isPlaying) { - callback.onPlaybackPause(media, position); - } - if (!media.getIdentifier().equals(playable.getIdentifier())) { - final Playable oldMedia = media; - callback.onPostPlayback(oldMedia, false, false, true); - } - - setPlayerStatus(PlayerStatus.INDETERMINATE, null); - } - } - - this.media = playable; - remoteMedia = remoteVersion(playable); - 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.onMediaChanged(true); - setPlayerStatus(PlayerStatus.INITIALIZED, media); - if (prepareImmediately) { - prepare(); - } - } - - @Override - public void resume() { - try { - if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { - 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); - } - } - - @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); - } - } - - @Override - public void prepare() { - 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); - } - } - } - - @Override - public void reinit() { - Log.d(TAG, "reinit() called"); - if (media != null) { - playMediaObject(media, true, false, startWhenPrepared.get(), false); - } else { - Log.d(TAG, "Call to reinit was ignored: media was null"); - } - } - - @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); - } - } - - @Override - public void seekDelta(int d) { - int position = getPosition(); - if (position != INVALID_TIME) { - seekTo(position + d); - } else { - Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); - } - } - - @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) { - 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) { - retVal = media.getPosition(); - } - Log.d(TAG, "getPosition() -> " + retVal); - return retVal; - } - - @Override - public boolean isStartWhenPrepared() { - return startWhenPrepared.get(); - } - - @Override - public void setStartWhenPrepared(boolean startWhenPrepared) { - this.startWhenPrepared.set(startWhenPrepared); - } - - @Override - public void setPlaybackParams(float speed, boolean skipSilence) { - //Can be safely ignored as neither set speed not skipSilence is supported - } - - @Override - public float getPlaybackSpeed() { - return 1; - } - - @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); - } - } - - @Override - public boolean canDownmix() { - return false; - } - - @Override - public void setDownmix(boolean enable) { - throw new UnsupportedOperationException("Setting downmix unsupported in Remote Media Player"); - } - - @Override - public MediaType getCurrentMediaType() { - return mediaType; - } - - @Override - public boolean isStreaming() { - return true; - } - - @Override - public void shutdown() { - castMgr.removeCastConsumer(castConsumer); - } - - @Override - public void shutdownQuietly() { - shutdown(); - } - - @Override - public void setVideoSurface(SurfaceHolder surface) { - throw new UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player"); - } - - @Override - public void resetVideoSurface() { - Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player"); - } - - @Override - public Pair<Integer, Integer> getVideoSize() { - return null; - } - - @Override - public Playable getPlayable() { - return media; - } - - @Override - protected void setPlayable(Playable playable) { - if (playable != media) { - media = playable; - remoteMedia = remoteVersion(playable); - } - } - - @Override - public List<String> getAudioTracks() { - return Collections.emptyList(); - } - - public void setAudioTrack(int track) { - - } - - public int getSelectedAudioTrack() { - return -1; - } - - @Override - protected Future<?> endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue, - boolean toStoppedState) { - Log.d(TAG, "endPlayback() called"); - boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - if (playerStatus != PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media); - } - if (media != null && wasSkipped) { - // current position only really matters when we skip - int position = getPosition(); - if (position >= 0) { - media.setPosition(position); - } - } - final Playable currentMedia = media; - Playable nextMedia = null; - if (shouldContinue) { - nextMedia = callback.getNextInQueue(currentMedia); - - boolean playNextEpisode = isPlaying && nextMedia != null; - if (playNextEpisode) { - Log.d(TAG, "Playback of next episode will start immediately."); - } else if (nextMedia == null){ - Log.d(TAG, "No more episodes available to play"); - } else { - Log.d(TAG, "Loading next episode, but not playing automatically."); - } - - if (nextMedia != null) { - 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); - } - } - 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) { - // Otherwise we rely on the chromecast callback to tell us the playback has stopped. - callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, nextMedia != null); - } - } else if (isPlaying) { - callback.onPlaybackPause(currentMedia, - currentMedia != null ? currentMedia.getPosition() : INVALID_TIME); - } - - 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; - } -} diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java new file mode 100644 index 000000000..2167d9f2c --- /dev/null +++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/WearMediaSession.java @@ -0,0 +1,28 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.os.Bundle; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.wearable.media.MediaControlConstants; + +public class WearMediaSession { + public static final String TAG = "WearMediaSession"; + + static void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, + CharSequence name, int icon) { + PlaybackStateCompat.CustomAction.Builder actionBuilder = + new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon); + Bundle actionExtras = new Bundle(); + actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true); + actionBuilder.setExtras(actionExtras); + + sessionState.addCustomAction(actionBuilder.build()); + } + + static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) { + Bundle sessionExtras = new Bundle(); + sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true); + sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true); + mediaSession.setExtras(sessionExtras); + } +} diff --git a/core/src/play/res/values/strings.xml b/core/src/play/res/values/strings.xml deleted file mode 100644 index 7307849d2..000000000 --- a/core/src/play/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <string name="pref_cast_message" translatable="false">@string/pref_cast_message_play_flavor</string> -</resources> diff --git a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java index 4890c471a..92c0e8e3d 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/service/playback/PlaybackVolumeUpdaterTest.java @@ -6,6 +6,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.model.playback.Playable; +import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer; +import de.danoeh.antennapod.playback.base.PlayerStatus; import org.junit.Before; import org.junit.Test; diff --git a/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java b/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java deleted file mode 100644 index dc64f6ae0..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtilTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Tests for {@link RewindAfterPauseUtils}. - */ -public class RewindAfterPauseUtilTest { - - @Test - public void testCalculatePositionWithRewindNoRewind() { - final int ORIGINAL_POSITION = 10000; - long lastPlayed = System.currentTimeMillis(); - int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed); - - assertEquals(ORIGINAL_POSITION, position); - } - - @Test - public void testCalculatePositionWithRewindSmallRewind() { - final int ORIGINAL_POSITION = 10000; - long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_SHORT_REWIND - 1000; - int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed); - - assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.SHORT_REWIND, position); - } - - @Test - public void testCalculatePositionWithRewindMediumRewind() { - final int ORIGINAL_POSITION = 10000; - long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_MEDIUM_REWIND - 1000; - int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed); - - assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.MEDIUM_REWIND, position); - } - - @Test - public void testCalculatePositionWithRewindLongRewind() { - final int ORIGINAL_POSITION = 30000; - long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_LONG_REWIND - 1000; - int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed); - - assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.LONG_REWIND, position); - } - - @Test - public void testCalculatePositionWithRewindNegativeNumber() { - final int ORIGINAL_POSITION = 100; - long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_LONG_REWIND - 1000; - int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed); - - assertEquals(0, position); - } -} |