Chromecast rework (#5518)

This commit is contained in:
ByteHamster 2021-11-28 22:19:14 +01:00 committed by GitHub
parent af2835c59d
commit f0100e61ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 901 additions and 3490 deletions

View File

@ -116,6 +116,8 @@ dependencies {
implementation project(':net:sync:gpoddernet') implementation project(':net:sync:gpoddernet')
implementation project(':net:sync:model') implementation project(':net:sync:model')
implementation project(':parser:feed') implementation project(':parser:feed')
implementation project(':playback:base')
implementation project(':playback:cast')
implementation project(':ui:app-start-intent') implementation project(':ui:app-start-intent')
implementation project(':ui:common') implementation project(':ui:common')

View File

@ -11,6 +11,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule; import androidx.test.rule.ActivityTestRule;
import de.danoeh.antennapod.model.feed.FeedItemFilter; import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.junit.After; import org.junit.After;
@ -32,7 +33,6 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.IntentUtils;

View File

@ -1,9 +1,10 @@
package de.test.antennapod.service.playback; package de.test.antennapod.service.playback;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback { public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
@ -42,14 +43,6 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
originalCallback.onMediaChanged(reloadUI); originalCallback.onMediaChanged(reloadUI);
} }
@Override
public boolean onMediaPlayerInfo(int code, int resourceId) {
if (isCancelled) {
return true;
}
return originalCallback.onMediaPlayerInfo(code, resourceId);
}
@Override @Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) { public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
if (isCancelled) { if (isCancelled) {
@ -82,6 +75,15 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
return originalCallback.getNextInQueue(currentMedia); return originalCallback.getNextInQueue(currentMedia);
} }
@Nullable
@Override
public Playable findMedia(@NonNull String url) {
if (isCancelled) {
return null;
}
return originalCallback.findMedia(url);
}
@Override @Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
if (isCancelled) { if (isCancelled) {
@ -89,4 +91,12 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
} }
originalCallback.onPlaybackEnded(mediaType, stopPlaying); originalCallback.onPlaybackEnded(mediaType, stopPlaying);
} }
@Override
public void ensureMediaInfoLoaded(@NonNull Playable media) {
if (isCancelled) {
return;
}
originalCallback.ensureMediaInfoLoaded(media);
}
} }

View File

@ -1,10 +1,10 @@
package de.test.antennapod.service.playback; package de.test.antennapod.service.playback;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback { public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
@Override @Override
@ -22,11 +22,6 @@ public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallb
} }
@Override
public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
return false;
}
@Override @Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) { public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
@ -47,8 +42,18 @@ public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallb
return null; return null;
} }
@Nullable
@Override
public Playable findMedia(@NonNull String url) {
return null;
}
@Override @Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
} }
@Override
public void ensureMediaInfoLoaded(@NonNull Playable media) {
} }
}

View File

@ -5,6 +5,8 @@ import android.content.Context;
import androidx.test.filters.MediumTest; import androidx.test.filters.MediumTest;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.test.antennapod.EspressoTestUtils; import de.test.antennapod.EspressoTestUtils;
import junit.framework.AssertionFailedError; import junit.framework.AssertionFailedError;
@ -24,8 +26,6 @@ import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.core.service.playback.LocalPSMP; import de.danoeh.antennapod.core.service.playback.LocalPSMP;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.test.antennapod.util.service.download.HTTPBin; import de.test.antennapod.util.service.download.HTTPBin;

View File

@ -1,7 +0,0 @@
package de.danoeh.antennapod.config;
import de.danoeh.antennapod.core.CastCallbacks;
class CastCallbackImpl implements CastCallbacks {
}

View File

@ -1,14 +0,0 @@
package de.danoeh.antennapod.preferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
/**
* Implements functions from PreferenceController that are flavor dependent.
*/
public class PreferenceControllerFlavorHelper {
public static void setupFlavoredUI(PlaybackPreferencesFragment ui) {
ui.findPreference(UserPreferences.PREF_CAST_ENABLED).setEnabled(false);
}
}

View File

@ -54,6 +54,9 @@
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/> android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/>
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="de.danoeh.antennapod.playback.cast.CastOptionsProvider" />
<!-- Version < 3.0. DeX Mode and Screen Mirroring support --> <!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/> <meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>

View File

@ -38,6 +38,7 @@ import com.bumptech.glide.Glide;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;

View File

@ -43,7 +43,6 @@ import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.Converter;
@ -62,6 +61,8 @@ import de.danoeh.antennapod.dialog.SleepTimerDialog;
import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;

View File

@ -15,6 +15,5 @@ class ClientConfigurator {
ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME; ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME;
ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl(); ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl();
ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl(); ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl();
ClientConfig.castCallbacks = new CastCallbackImpl();
} }
} }

View File

@ -32,6 +32,7 @@ import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.event.playback.SpeedChangedEvent; import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@ -41,7 +42,6 @@ import java.text.NumberFormat;
import java.util.List; import java.util.List;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.CastEnabledActivity;
import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.event.FavoritesEvent; import de.danoeh.antennapod.event.FavoritesEvent;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;

View File

@ -17,6 +17,7 @@ import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@ -24,7 +25,6 @@ import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.ChaptersListAdapter; import de.danoeh.antennapod.adapter.ChaptersListAdapter;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.ChapterUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.model.feed.Chapter; import de.danoeh.antennapod.model.feed.Chapter;

View File

@ -23,9 +23,9 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.core.util.playback.PlaybackController;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.view.PlayButton; import de.danoeh.antennapod.view.PlayButton;
import io.reactivex.Maybe; import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;

View File

@ -16,7 +16,6 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil; import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
import de.danoeh.antennapod.dialog.SkipPreferenceDialog; import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
import de.danoeh.antennapod.dialog.VariableSpeedDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog;
import de.danoeh.antennapod.preferences.PreferenceControllerFlavorHelper;
import java.util.Map; import java.util.Map;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
@ -31,7 +30,6 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
addPreferencesFromResource(R.xml.preferences_playback); addPreferencesFromResource(R.xml.preferences_playback);
setupPlaybackScreen(); setupPlaybackScreen();
PreferenceControllerFlavorHelper.setupFlavoredUI(this);
buildSmartMarkAsPlayedPreference(); buildSmartMarkAsPlayedPreference();
} }

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/media_route_menu_item"
android:title="@string/cast_media_route_menu_title"
custom:actionProviderClass="de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider"
custom:showAsAction="ifRoom"/>
</menu>

View File

@ -127,11 +127,5 @@
android:title="@string/media_player" android:title="@string/media_player"
android:summary="@string/pref_media_player_message" android:summary="@string/pref_media_player_message"
android:entryValues="@array/media_player_values"/> android:entryValues="@array/media_player_values"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:enabled="true"
android:key="prefCast"
android:summary="@string/pref_cast_message"
android:title="@string/pref_cast_title"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@ -1,157 +0,0 @@
package de.danoeh.antennapod.activity;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.os.Bundle;
import androidx.preference.PreferenceManager;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import com.google.android.gms.cast.ApplicationMetadata;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.cast.CastButtonVisibilityManager;
import de.danoeh.antennapod.core.cast.CastConsumer;
import de.danoeh.antennapod.core.cast.CastManager;
import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
import de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import java.util.ArrayList;
import java.util.List;
/**
* Activity that allows for showing the MediaRouter button whenever there's a cast device in the
* network.
*/
public abstract class CastEnabledActivity extends AppCompatActivity
implements SharedPreferences.OnSharedPreferenceChangeListener {
public static final String TAG = "CastEnabledActivity";
private CastConsumer castConsumer;
private CastManager castManager;
private final List<CastButtonVisibilityManager> castButtons = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!CastManager.isInitialized()) {
return;
}
PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
.registerOnSharedPreferenceChangeListener(this);
castConsumer = new DefaultCastConsumer() {
@Override
public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
onCastConnectionChanged(true);
}
@Override
public void onDisconnected() {
onCastConnectionChanged(false);
}
};
castManager = CastManager.getInstance();
castManager.addCastConsumer(castConsumer);
CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager(castManager);
castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
onCastConnectionChanged(castManager.isConnected());
castButtons.add(castButtonVisibilityManager);
}
@Override
protected void onDestroy() {
if (!CastManager.isInitialized()) {
super.onDestroy();
return;
}
PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
.unregisterOnSharedPreferenceChangeListener(this);
castManager.removeCastConsumer(castConsumer);
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
if (!CastManager.isInitialized()) {
return;
}
for (CastButtonVisibilityManager castButton : castButtons) {
castButton.setResumed(true);
}
}
@Override
protected void onPause() {
super.onPause();
if (!CastManager.isInitialized()) {
return;
}
for (CastButtonVisibilityManager castButton : castButtons) {
castButton.setResumed(false);
}
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (UserPreferences.PREF_CAST_ENABLED.equals(key)) {
boolean newValue = UserPreferences.isCastEnabled();
Log.d(TAG, "onSharedPreferenceChanged(), isCastEnabled set to " + newValue);
for (CastButtonVisibilityManager castButton : castButtons) {
castButton.setPrefEnabled(newValue);
}
// PlaybackService has its own listener, so if it's active we don't have to take action here.
if (!newValue && !PlaybackService.isRunning) {
CastManager.getInstance().disconnect();
}
}
}
private void onCastConnectionChanged(boolean connected) {
if (connected) {
for (CastButtonVisibilityManager castButton : castButtons) {
castButton.onConnected();
}
setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
} else {
for (CastButtonVisibilityManager castButton : castButtons) {
castButton.onDisconnected();
}
setVolumeControlStream(AudioManager.STREAM_MUSIC);
}
}
/**
* Should be called by any activity or fragment for which the cast button should be shown.
*/
public final void requestCastButton(Menu menu) {
if (!CastManager.isInitialized()) {
return;
}
MenuItem mediaRouteButton = menu.findItem(R.id.media_route_menu_item);
if (mediaRouteButton == null) {
getMenuInflater().inflate(R.menu.cast_enabled, menu);
mediaRouteButton = menu.findItem(R.id.media_route_menu_item);
}
SwitchableMediaRouteActionProvider mediaRouteActionProvider =
CastManager.getInstance().addMediaRouterButton(mediaRouteButton);
CastButtonVisibilityManager castButtonVisibilityManager =
new CastButtonVisibilityManager(CastManager.getInstance());
castButtonVisibilityManager.setMenu(menu);
castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
castButtonVisibilityManager.mediaRouteActionProvider = mediaRouteActionProvider;
castButtonVisibilityManager.setResumed(true);
castButtonVisibilityManager.requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS);
mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable());
}
}

View File

@ -1,21 +0,0 @@
package de.danoeh.antennapod.config;
import androidx.annotation.NonNull;
import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
import androidx.mediarouter.app.MediaRouteDialogFactory;
import de.danoeh.antennapod.core.CastCallbacks;
import de.danoeh.antennapod.fragment.CustomMRControllerDialogFragment;
public class CastCallbackImpl implements CastCallbacks {
@Override
public MediaRouteDialogFactory getMediaRouterDialogFactory() {
return new MediaRouteDialogFactory() {
@NonNull
@Override
public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
return new CustomMRControllerDialogFragment();
}
};
}
}

View File

@ -1,480 +0,0 @@
package de.danoeh.antennapod.dialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import androidx.core.util.Pair;
import androidx.core.view.MarginLayoutParamsCompat;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.mediarouter.app.MediaRouteControllerDialog;
import androidx.palette.graphics.Palette;
import androidx.mediarouter.media.MediaRouter;
import androidx.appcompat.widget.AppCompatImageView;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.Target;
import java.util.concurrent.ExecutionException;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
public class CustomMRControllerDialog extends MediaRouteControllerDialog {
public static final String TAG = "CustomMRContrDialog";
private MediaRouter mediaRouter;
private MediaSessionCompat.Token token;
private ImageView artView;
private TextView titleView;
private TextView subtitleView;
private ImageButton playPauseButton;
private LinearLayout rootView;
private boolean viewsCreated = false;
private Disposable fetchArtSubscription;
private MediaControllerCompat mediaController;
private MediaControllerCompat.Callback mediaControllerCallback;
public CustomMRControllerDialog(Context context) {
this(context, 0);
}
private CustomMRControllerDialog(Context context, int theme) {
super(context, theme);
mediaRouter = MediaRouter.getInstance(getContext());
token = mediaRouter.getMediaSessionToken();
try {
if (token != null) {
mediaController = new MediaControllerCompat(getContext(), token);
}
} catch (RemoteException e) {
Log.e(TAG, "Error creating media controller", e);
}
if (mediaController != null) {
mediaControllerCallback = new MediaControllerCompat.Callback() {
@Override
public void onSessionDestroyed() {
if (mediaController != null) {
mediaController.unregisterCallback(mediaControllerCallback);
mediaController = null;
}
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
updateViews();
}
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
updateState();
}
};
mediaController.registerCallback(mediaControllerCallback);
}
}
@Override
public View onCreateMediaControlView(Bundle savedInstanceState) {
boolean landscape = getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
if (landscape) {
/*
* When a horizontal LinearLayout measures itself, it first measures its children and
* settles their widths on the first pass, and only then figures out its height, never
* revisiting the widths measurements.
* When one has a child view that imposes a certain aspect ratio (such as an ImageView),
* then its width and height are related to each other, and so if one allows for a large
* height, then it will request for itself a large width as well. However, on the first
* child measurement, the LinearLayout imposes a very relaxed height bound, that the
* child uses to tell the width it wants, a value which the LinearLayout will interpret
* as final, even though the child will want to change it once a more restrictive height
* bound is imposed later.
*
* Our solution is, given that the heights of the children do not depend on their widths
* in this case, we first figure out the layout's height and only then perform the
* usual sequence of measurements.
*
* Note: this solution does not take into account any vertical paddings nor children's
* vertical margins in determining the height, as this View as well as its children are
* defined in code and no paddings/margins that would influence these computations are
* introduced.
*
* There were no resources online for this type of issue as far as I could gather.
*/
rootView = new LinearLayout(getContext()) {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// We'd like to find the overall height before adjusting the widths within the LinearLayout
int maxHeight = Integer.MIN_VALUE;
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
for (int i = 0; i < getChildCount(); i++) {
int height = Integer.MIN_VALUE;
View child = getChildAt(i);
ViewGroup.LayoutParams lp = child.getLayoutParams();
// we only measure children whose layout_height is not MATCH_PARENT
if (lp.height >= 0) {
height = lp.height;
} else if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
child.measure(widthMeasureSpec, heightMeasureSpec);
height = child.getMeasuredHeight();
}
maxHeight = Math.max(maxHeight, height);
}
}
if (maxHeight > 0) {
super.onMeasure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY));
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
};
rootView.setOrientation(LinearLayout.HORIZONTAL);
} else {
rootView = new LinearLayout(getContext());
rootView.setOrientation(LinearLayout.VERTICAL);
}
FrameLayout.LayoutParams rootParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
rootParams.setMargins(0, 0, 0,
getContext().getResources().getDimensionPixelSize(R.dimen.media_router_controller_bottom_margin));
rootView.setLayoutParams(rootParams);
// Start the session activity when a content item (album art, title or subtitle) is clicked.
View.OnClickListener onClickListener = v -> {
if (mediaController != null) {
PendingIntent pi = mediaController.getSessionActivity();
if (pi != null) {
try {
pi.send();
dismiss();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, pi + " was not sent, it had been canceled.");
}
}
}
};
LinearLayout.LayoutParams artParams;
/*
* On portrait orientation, we want to limit the artView's height to 9/16 of the available
* width. Reason is that we need to choose the height wisely otherwise we risk the dialog
* being much larger than the screen, and there doesn't seem to be a good way to know the
* available height beforehand.
*
* On landscape orientation, we want to limit the artView's width to its available height.
* Otherwise, horizontal images would take too much space and severely restrict the space
* for episode title and play/pause button.
*
* Internal implementation of ImageView only uses the source image's aspect ratio, but we
* want to impose our own and fallback to the source image's when it is more favorable.
* Solutions were inspired, among other similar sources, on
* http://stackoverflow.com/questions/18077325/scale-image-to-fill-imageview-width-and-keep-aspect-ratio
*/
if (landscape) {
artView = new AppCompatImageView(getContext()) {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredWidth = widthMeasureSpec;
int desiredMeasureMode = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY ?
MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
Drawable drawable = getDrawable();
if (drawable != null) {
int intrHeight = drawable.getIntrinsicHeight();
int intrWidth = drawable.getIntrinsicWidth();
int originalHeight = MeasureSpec.getSize(heightMeasureSpec);
if (intrHeight < intrWidth) {
desiredWidth = MeasureSpec.makeMeasureSpec(
originalHeight, desiredMeasureMode);
} else {
desiredWidth = MeasureSpec.makeMeasureSpec(
Math.round((float) originalHeight * intrWidth / intrHeight),
desiredMeasureMode);
}
}
}
super.onMeasure(desiredWidth, heightMeasureSpec);
}
};
artParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT);
MarginLayoutParamsCompat.setMarginStart(artParams,
getContext().getResources().getDimensionPixelSize(R.dimen.media_router_controller_playback_control_horizontal_spacing));
} else {
artView = new AppCompatImageView(getContext()) {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredHeight = heightMeasureSpec;
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
Drawable drawable = getDrawable();
if (drawable != null) {
int originalWidth = MeasureSpec.getSize(widthMeasureSpec);
int intrHeight = drawable.getIntrinsicHeight();
int intrWidth = drawable.getIntrinsicWidth();
float scale;
if (intrHeight*16 > intrWidth*9) {
// image is taller than 16:9
scale = (float) originalWidth * 9 / 16 / intrHeight;
} else {
// image is more horizontal than 16:9
scale = (float) originalWidth / intrWidth;
}
desiredHeight = MeasureSpec.makeMeasureSpec(
Math.round(intrHeight * scale),
MeasureSpec.EXACTLY);
}
}
super.onMeasure(widthMeasureSpec, desiredHeight);
}
};
artParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
// When we fetch the bitmap, we want to know if we should set a background color or not.
artView.setTag(landscape);
artView.setScaleType(ImageView.ScaleType.FIT_CENTER);
artView.setOnClickListener(onClickListener);
artView.setLayoutParams(artParams);
rootView.addView(artView);
ViewGroup wrapper = rootView;
if (landscape) {
// Here we wrap with a frame layout because we want to set different layout parameters
// for landscape orientation.
wrapper = new FrameLayout(getContext());
wrapper.setLayoutParams(new LinearLayout.LayoutParams(
0,
ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
rootView.addView(wrapper);
rootView.setWeightSum(1f);
}
View playbackControlLayout = View.inflate(getContext(), R.layout.media_router_controller, wrapper);
titleView = playbackControlLayout.findViewById(R.id.mrc_control_title);
subtitleView = playbackControlLayout.findViewById(R.id.mrc_control_subtitle);
playbackControlLayout.findViewById(R.id.mrc_control_title_container).setOnClickListener(onClickListener);
playPauseButton = playbackControlLayout.findViewById(R.id.mrc_control_play_pause);
playPauseButton.setOnClickListener(v -> {
PlaybackStateCompat state;
if (mediaController != null && (state = mediaController.getPlaybackState()) != null) {
boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_PLAYING;
if (isPlaying) {
mediaController.getTransportControls().pause();
} else {
mediaController.getTransportControls().play();
}
// Announce the action for accessibility.
AccessibilityManager accessibilityManager = (AccessibilityManager)
getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (accessibilityManager != null && accessibilityManager.isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
event.setPackageName(getContext().getPackageName());
event.setClassName(getClass().getName());
int resId = isPlaying ? R.string.mr_controller_pause : R.string.mr_controller_play;
event.getText().add(getContext().getString(resId));
accessibilityManager.sendAccessibilityEvent(event);
}
}
});
viewsCreated = true;
updateViews();
return rootView;
}
@Override
public void onDetachedFromWindow() {
if (fetchArtSubscription != null) {
fetchArtSubscription.dispose();
fetchArtSubscription = null;
}
super.onDetachedFromWindow();
}
private void updateViews() {
if (!viewsCreated || token == null || mediaController == null) {
rootView.setVisibility(View.GONE);
return;
}
MediaMetadataCompat metadata = mediaController.getMetadata();
MediaDescriptionCompat description = metadata == null ? null : metadata.getDescription();
if (description == null) {
rootView.setVisibility(View.GONE);
return;
}
PlaybackStateCompat state = mediaController.getPlaybackState();
MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute();
CharSequence title = description.getTitle();
boolean hasTitle = !TextUtils.isEmpty(title);
CharSequence subtitle = description.getSubtitle();
boolean hasSubtitle = !TextUtils.isEmpty(subtitle);
boolean showTitle = false;
boolean showSubtitle = false;
if (route.getPresentationDisplay() != null &&
route.getPresentationDisplay().getDisplayId() != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) {
// The user is currently casting screen.
titleView.setText(R.string.mr_controller_casting_screen);
showTitle = true;
} else if (state == null || state.getState() == PlaybackStateCompat.STATE_NONE) {
// Show "No media selected" as we don't yet know the playback state.
// (Only exception is bluetooth where we don't show anything.)
if (!route.isBluetooth()) {
titleView.setText(R.string.mr_controller_no_media_selected);
showTitle = true;
}
} else if (!hasTitle && !hasSubtitle) {
titleView.setText(R.string.mr_controller_no_info_available);
showTitle = true;
} else {
if (hasTitle) {
titleView.setText(title);
showTitle = true;
}
if (hasSubtitle) {
subtitleView.setText(subtitle);
showSubtitle = true;
}
}
if (showSubtitle) {
titleView.setSingleLine();
} else {
titleView.setMaxLines(2);
}
titleView.setVisibility(showTitle ? View.VISIBLE : View.GONE);
subtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
updateState();
if(rootView.getVisibility() != View.VISIBLE) {
artView.setVisibility(View.GONE);
rootView.setVisibility(View.VISIBLE);
}
if (fetchArtSubscription != null) {
fetchArtSubscription.dispose();
}
fetchArtSubscription = Observable.fromCallable(() -> fetchArt(description))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
fetchArtSubscription = null;
if (artView == null) {
return;
}
if (result.first != null) {
if (!((Boolean) artView.getTag())) {
artView.setBackgroundColor(result.second);
}
artView.setImageBitmap(result.first);
artView.setVisibility(View.VISIBLE);
} else {
artView.setVisibility(View.GONE);
}
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
private void updateState() {
PlaybackStateCompat state;
if (!viewsCreated || mediaController == null ||
(state = mediaController.getPlaybackState()) == null) {
return;
}
boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_BUFFERING
|| state.getState() == PlaybackStateCompat.STATE_PLAYING;
boolean supportsPlay = (state.getActions() & (PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0;
boolean supportsPause = (state.getActions() & (PlaybackStateCompat.ACTION_PAUSE
| PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0;
if (isPlaying && supportsPause) {
playPauseButton.setVisibility(View.VISIBLE);
playPauseButton.setImageResource(getThemeResource(getContext(), R.attr.mediaRoutePauseDrawable));
playPauseButton.setContentDescription(getContext().getResources().getText(R.string.mr_controller_pause));
} else if (!isPlaying && supportsPlay) {
playPauseButton.setVisibility(View.VISIBLE);
playPauseButton.setImageResource(getThemeResource(getContext(), R.attr.mediaRoutePlayDrawable));
playPauseButton.setContentDescription(getContext().getResources().getText(R.string.mr_controller_play));
} else {
playPauseButton.setVisibility(View.GONE);
}
}
private static int getThemeResource(Context context, int attr) {
TypedValue value = new TypedValue();
return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0;
}
@NonNull
private Pair<Bitmap, Integer> fetchArt(@NonNull MediaDescriptionCompat description) {
Bitmap iconBitmap = description.getIconBitmap();
Uri iconUri = description.getIconUri();
Bitmap art = null;
if (iconBitmap != null) {
art = iconBitmap;
} else if (iconUri != null) {
try {
art = Glide.with(getContext().getApplicationContext())
.asBitmap()
.load(iconUri.toString())
.apply(RequestOptions.diskCacheStrategyOf(ApGlideSettings.AP_DISK_CACHE_STRATEGY))
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
.get();
} catch (InterruptedException | ExecutionException e) {
Log.e(TAG, "Image art load failed", e);
}
}
int backgroundColor = 0;
if (art != null && art.getWidth()*9 < art.getHeight()*16) {
// Portrait art requires dominant color as background color.
Palette palette = new Palette.Builder(art).maximumColorCount(1).generate();
backgroundColor = palette.getSwatches().isEmpty()
? 0 : palette.getSwatches().get(0).getRgb();
}
return new Pair<>(art, backgroundColor);
}
}

View File

@ -1,15 +0,0 @@
package de.danoeh.antennapod.fragment;
import android.content.Context;
import android.os.Bundle;
import androidx.mediarouter.app.MediaRouteControllerDialog;
import androidx.mediarouter.app.MediaRouteControllerDialogFragment;
import de.danoeh.antennapod.dialog.CustomMRControllerDialog;
public class CustomMRControllerDialogFragment extends MediaRouteControllerDialogFragment {
@Override
public MediaRouteControllerDialog onCreateControllerDialog(Context context, Bundle savedInstanceState) {
return new CustomMRControllerDialog(context);
}
}

View File

@ -1,48 +0,0 @@
package de.danoeh.antennapod.preferences;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
/**
* Implements functions from PreferenceController that are flavor dependent.
*/
public class PreferenceControllerFlavorHelper {
public static void setupFlavoredUI(PlaybackPreferencesFragment ui) {
//checks whether Google Play Services is installed on the device (condition necessary for Cast support)
ui.findPreference(UserPreferences.PREF_CAST_ENABLED).setOnPreferenceChangeListener((preference, o) -> {
if (o instanceof Boolean && ((Boolean) o)) {
final int googlePlayServicesCheck = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(ui.getActivity());
if (googlePlayServicesCheck == ConnectionResult.SUCCESS) {
displayRestartRequiredDialog(ui.requireContext());
return true;
} else {
GoogleApiAvailability.getInstance()
.getErrorDialog(ui.getActivity(), googlePlayServicesCheck, 0)
.show();
return false;
}
}
return true;
});
}
private static void displayRestartRequiredDialog(@NonNull Context context) {
AlertDialog.Builder dialog = new AlertDialog.Builder(context);
dialog.setTitle(android.R.string.dialog_alert_title);
dialog.setMessage(R.string.pref_restart_required);
dialog.setPositiveButton(android.R.string.ok, (dialog1, which) -> PodcastApp.forceRestart());
dialog.setCancelable(false);
dialog.show();
}
}

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mrc_playback_control"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/media_router_controller_playback_control_vertical_padding"
android:paddingBottom="@dimen/media_router_controller_playback_control_vertical_padding"
android:paddingLeft="@dimen/media_router_controller_playback_control_start_padding"
android:paddingStart="@dimen/media_router_controller_playback_control_start_padding"
android:paddingRight="@dimen/media_router_controller_playback_control_horizontal_spacing"
android:paddingEnd="@dimen/media_router_controller_playback_control_horizontal_spacing">
<ImageButton android:id="@+id/mrc_control_play_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/media_router_controller_playback_control_horizontal_spacing"
android:layout_marginStart="@dimen/media_router_controller_playback_control_horizontal_spacing"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:contentDescription="@string/mr_controller_play"
android:background="?android:attr/selectableItemBackground"/>
<LinearLayout android:id="@+id/mrc_control_title_container"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_toLeftOf="@id/mrc_control_play_pause"
android:layout_toStartOf="@id/mrc_control_play_pause"
android:layout_centerVertical="true">
<TextView android:id="@+id/mrc_control_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.MediaRouter.PrimaryText"/>
<TextView android:id="@+id/mrc_control_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.MediaRouter.SecondaryText"
android:singleLine="true" />
</LinearLayout>
</RelativeLayout>

View File

@ -27,6 +27,8 @@ dependencies {
implementation project(':net:sync:model') implementation project(':net:sync:model')
implementation project(':parser:feed') implementation project(':parser:feed')
implementation project(':parser:media') implementation project(':parser:media')
implementation project(':playback:base')
implementation project(':playback:cast')
implementation project(':ui:app-start-intent') implementation project(':ui:app-start-intent')
implementation project(':ui:common') implementation project(':ui:common')
implementation project(':ui:png-icons') implementation project(':ui:png-icons')
@ -61,9 +63,6 @@ dependencies {
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
// Non-free dependencies: // 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" playApi "com.google.android.support:wearable:$wearableSupportVersion"
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"

View File

@ -1,7 +0,0 @@
package de.danoeh.antennapod.core;
/**
* Callbacks for Chromecast support on the core module
*/
public interface CastCallbacks {
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -30,8 +30,6 @@ public class ClientConfig {
public static DownloadServiceCallbacks downloadServiceCallbacks; public static DownloadServiceCallbacks downloadServiceCallbacks;
public static CastCallbacks castCallbacks;
private static boolean initialized = false; private static boolean initialized = false;
public static synchronized void initialize(Context context) { public static synchronized void initialize(Context context) {

View File

@ -8,8 +8,8 @@ import android.util.Log;
import de.danoeh.antennapod.event.PlayerStatusEvent; import de.danoeh.antennapod.event.PlayerStatusEvent;
import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.MediaType; 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.model.playback.Playable;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;

View File

@ -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.service.download.HttpDownloader;
import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable; import io.reactivex.disposables.Disposable;

View File

@ -17,8 +17,9 @@ import androidx.media.AudioManagerCompat;
import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.BufferUpdateEvent; import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.event.playback.SpeedChangedEvent; 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.core.util.playback.MediaPlayerError;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.antennapod.audio.MediaPlayer; import org.antennapod.audio.MediaPlayer;
import java.io.File; 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.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences; 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.AudioPlayer;
import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
@ -148,7 +149,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
} }
public LocalPSMP(@NonNull Context context, public LocalPSMP(@NonNull Context context,
@NonNull PSMPCallback callback) { @NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) {
super(context, callback); super(context, callback);
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.playerLock = new PlayerLock(); this.playerLock = new PlayerLock();
@ -265,9 +266,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
LocalPSMP.this.startWhenPrepared.set(startWhenPrepared); LocalPSMP.this.startWhenPrepared.set(startWhenPrepared);
setPlayerStatus(PlayerStatus.INITIALIZING, media); setPlayerStatus(PlayerStatus.INITIALIZING, media);
try { try {
if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) { callback.ensureMediaInfoLoaded(media);
((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
}
callback.onMediaChanged(false); callback.onMediaChanged(false);
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence()); setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence());
if (stream) { if (stream) {
@ -1098,7 +1097,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
EventBus.getDefault().post(BufferUpdateEvent.ended()); EventBus.getDefault().post(BufferUpdateEvent.ended());
return true; return true;
default: default:
return callback.onMediaPlayerInfo(what, 0); return true;
} }
} }
@ -1148,4 +1147,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
executor.submit(r); executor.submit(r);
} }
} }
@Override
public boolean isCasting() {
return false;
}
} }

View File

@ -37,6 +37,7 @@ import android.widget.Toast;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; 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.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent; import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; 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.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
@ -103,24 +108,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
*/ */
private static final String TAG = "PlaybackService"; private static final String TAG = "PlaybackService";
/**
* Parcelable of type Playable.
*/
public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; 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_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_THIS_TIME = "extra.de.danoeh.antennapod.core.service.allowStream";
public static final String EXTRA_ALLOW_STREAM_ALWAYS = "extra.de.danoeh.antennapod.core.service.allowStreamAlways"; public static final String 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_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared";
public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately"; 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 PlaybackServiceMediaPlayer mediaPlayer;
private PlaybackServiceTaskManager taskManager; private PlaybackServiceTaskManager taskManager;
private PlaybackServiceFlavorHelper flavorHelper;
private PlaybackServiceStateManager stateManager; private PlaybackServiceStateManager stateManager;
private Disposable positionEventTimer; private Disposable positionEventTimer;
private PlaybackServiceNotificationBuilder notificationBuilder; private PlaybackServiceNotificationBuilder notificationBuilder;
private CastStateListener castStateListener;
private String autoSkippedFeedMediaId = null; private String autoSkippedFeedMediaId = null;
@ -280,7 +271,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
EventBus.getDefault().register(this); EventBus.getDefault().register(this);
taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback);
PreferenceManager.getDefaultSharedPreferences(this) PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(prefListener); .registerOnSharedPreferenceChangeListener(prefListener);
@ -305,12 +295,36 @@ public class PlaybackService extends MediaBrowserServiceCompat {
npe.printStackTrace(); npe.printStackTrace();
} }
flavorHelper.initializeMediaPlayer(PlaybackService.this); recreateMediaPlayer();
mediaSession.setActive(true); mediaSession.setActive(true);
castStateListener = new CastStateListener(this) {
@Override
public void onSessionStartedOrEnded() {
recreateMediaPlayer();
}
};
EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED)); 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 @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy(); super.onDestroy();
@ -324,6 +338,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopForeground(!UserPreferences.isPersistNotify()); stateManager.stopForeground(!UserPreferences.isPersistNotify());
isRunning = false; isRunning = false;
currentMediaType = MediaType.UNKNOWN; currentMediaType = MediaType.UNKNOWN;
castStateListener.destroy();
cancelPositionObserver(); cancelPositionObserver();
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener); PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener);
@ -337,8 +352,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
unregisterReceiver(audioBecomingNoisy); unregisterReceiver(audioBecomingNoisy);
unregisterReceiver(skipCurrentEpisodeReceiver); unregisterReceiver(skipCurrentEpisodeReceiver);
unregisterReceiver(pausePlayCurrentEpisodeReceiver); unregisterReceiver(pausePlayCurrentEpisodeReceiver);
flavorHelper.removeCastConsumer();
flavorHelper.unregisterWifiBroadcastReceiver();
mediaPlayer.shutdown(); mediaPlayer.shutdown();
taskManager.shutdown(); taskManager.shutdown();
} }
@ -483,9 +496,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false); final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false);
final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); 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"); Log.e(TAG, "PlaybackService was started with no arguments");
stateManager.stopService(); stateManager.stopService();
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
@ -509,7 +521,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopService(); stateManager.stopService();
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
} }
} else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) { } else {
stateManager.validStartCommandWasReceived(); stateManager.validStartCommandWasReceived();
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true); boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true);
boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false); boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false);
@ -553,9 +565,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.stopService(); stateManager.stopService();
}); });
return Service.START_NOT_STICKY; 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); saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
} }
@Override @Override
public WidgetUpdater.WidgetState requestWidgetState() { public WidgetUpdater.WidgetState requestWidgetState() {
return new WidgetUpdater.WidgetState(getPlayable(), getStatus(), return new WidgetUpdater.WidgetState(getPlayable(), getStatus(),
@ -872,11 +879,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
updateNotificationAndMediaSession(getPlayable()); updateNotificationAndMediaSession(getPlayable());
} }
@Override
public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
return flavorHelper.onMediaPlayerInfo(PlaybackService.this, code, resourceId);
}
@Override @Override
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped,
boolean playingNext) { boolean playingNext) {
@ -916,10 +918,24 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return PlaybackService.this.getNextInQueue(currentMedia); return PlaybackService.this.getNextInQueue(currentMedia);
} }
@Nullable
@Override
public Playable findMedia(@NonNull String url) {
FeedItem item = DBReader.getFeedItemByGuidOrEpisodeUrl(null, url);
return item != null ? item.getMedia() : null;
}
@Override @Override
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
PlaybackService.this.onPlaybackEnded(mediaType, 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) @Subscribe(threadMode = ThreadMode.MAIN)
@ -1248,15 +1264,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
// This would give the PIP of videos a play button // This would give the PIP of videos a play button
capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY; capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY;
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) { if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) {
flavorHelper.sessionStateAddActionForWear(sessionState, WearMediaSession.sessionStateAddActionForWear(sessionState,
CUSTOM_ACTION_REWIND, CUSTOM_ACTION_REWIND,
getString(R.string.rewind_label), getString(R.string.rewind_label),
android.R.drawable.ic_media_rew); android.R.drawable.ic_media_rew);
flavorHelper.sessionStateAddActionForWear(sessionState, WearMediaSession.sessionStateAddActionForWear(sessionState,
CUSTOM_ACTION_FAST_FORWARD, CUSTOM_ACTION_FAST_FORWARD,
getString(R.string.fast_forward_label), getString(R.string.fast_forward_label),
android.R.drawable.ic_media_ff); 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.setPlayable(playable);
notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken()); notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken());
notificationBuilder.setPlayerStatus(playerStatus); notificationBuilder.setPlayerStatus(playerStatus);
notificationBuilder.setCasting(isCasting);
notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed()); notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed());
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
@ -1901,93 +1916,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
(sharedPreferences, key) -> { (sharedPreferences, key) -> {
if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) { if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) {
updateNotificationAndMediaSession(getPlayable()); 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);
} }
}; };
} }

View File

@ -31,17 +31,17 @@ import de.danoeh.antennapod.model.playback.Playable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
public class PlaybackServiceNotificationBuilder { public class PlaybackServiceNotificationBuilder {
private static final String TAG = "PlaybackSrvNotification"; private static final String TAG = "PlaybackSrvNotification";
private static Bitmap defaultIcon = null; private static Bitmap defaultIcon = null;
private Context context; private final Context context;
private Playable playable; private Playable playable;
private MediaSessionCompat.Token mediaSessionToken; private MediaSessionCompat.Token mediaSessionToken;
private PlayerStatus playerStatus; private PlayerStatus playerStatus;
private boolean isCasting;
private Bitmap icon; private Bitmap icon;
private String position; private String position;
@ -140,7 +140,7 @@ public class PlaybackServiceNotificationBuilder {
if (playable != null) { if (playable != null) {
notification.setContentTitle(playable.getFeedTitle()); notification.setContentTitle(playable.getFeedTitle());
notification.setContentText(playable.getEpisodeTitle()); notification.setContentText(playable.getEpisodeTitle());
addActions(notification, mediaSessionToken, playerStatus, isCasting); addActions(notification, mediaSessionToken, playerStatus);
if (icon != null) { if (icon != null) {
notification.setLargeIcon(icon); notification.setLargeIcon(icon);
@ -175,23 +175,10 @@ public class PlaybackServiceNotificationBuilder {
} }
private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken, private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken,
PlayerStatus playerStatus, boolean isCasting) { PlayerStatus playerStatus) {
ArrayList<Integer> compactActionList = new ArrayList<>(); ArrayList<Integer> compactActionList = new ArrayList<>();
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction 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 // always let them rewind
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction( PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_REWIND, numActions); KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
@ -270,10 +257,6 @@ public class PlaybackServiceNotificationBuilder {
this.playerStatus = playerStatus; this.playerStatus = playerStatus;
} }
public void setCasting(boolean casting) {
isCasting = casting;
}
public PlayerStatus getPlayerStatus() { public PlayerStatus getPlayerStatus() {
return playerStatus; return playerStatus;
} }

View File

@ -4,6 +4,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
class PlaybackVolumeUpdater { class PlaybackVolumeUpdater {

View File

@ -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;
}
}

View File

@ -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.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService; 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.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.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;

View File

@ -25,11 +25,11 @@ import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.receiver.PlayerWidget; 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.util.Converter;
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
import de.danoeh.antennapod.core.util.TimeSpeedConverter; import de.danoeh.antennapod.core.util.TimeSpeedConverter;
import de.danoeh.antennapod.model.playback.Playable; 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.MainActivityStarter;
import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;

View File

@ -6,9 +6,9 @@ import androidx.annotation.NonNull;
import androidx.core.app.SafeJobIntentService; import androidx.core.app.SafeJobIntentService;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils; import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences; 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.model.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlayableUtils; import de.danoeh.antennapod.core.util.playback.PlayableUtils;
import de.danoeh.antennapod.playback.base.PlayerStatus;
public class WidgetUpdaterJobService extends SafeJobIntentService { public class WidgetUpdaterJobService extends SafeJobIntentService {
private static final int JOB_ID = -17001; private static final int JOB_ID = -17001;

View File

@ -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>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<dimen name="widget_margin">0dp</dimen> <dimen name="widget_margin">0dp</dimen>
<dimen name="external_player_height">64dp</dimen> <dimen name="external_player_height">64dp</dimen>
<dimen name="text_size_micro">12sp</dimen> <dimen name="text_size_micro">12sp</dimen>
@ -28,11 +27,5 @@
<dimen name="audioplayer_playercontrols_length_big">64dp</dimen> <dimen name="audioplayer_playercontrols_length_big">64dp</dimen>
<dimen name="audioplayer_playercontrols_margin">12dp</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> <dimen name="nav_drawer_max_screen_size">480dp</dimen>
</resources> </resources>

View File

@ -502,9 +502,6 @@
<string name="pref_proxy_title">Proxy</string> <string name="pref_proxy_title">Proxy</string>
<string name="pref_proxy_sum">Set a network 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_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_title">Enqueue Downloaded</string>
<string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</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> <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_pausePlaybackForFocusLoss_title">Pause for Interruptions</string>
<string name="pref_resumeAfterCall_sum">Resume playback after a phone call completes</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_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 --> <!-- Online feed view -->
<string name="subscribe_label">Subscribe</string> <string name="subscribe_label">Subscribe</string>
@ -808,21 +804,6 @@
<!-- Subscriptions fragment --> <!-- Subscriptions fragment -->
<string name="subscription_num_columns">Number of columns</string> <string name="subscription_num_columns">Number of columns</string>
<!-- Casting -->
<string name="cast_media_route_menu_title">Play on&#8230;</string>
<string name="cast_disconnect_label">Disconnect the cast session</string>
<string name="cast_not_castable">Media selected is not compatible with cast device</string>
<string name="cast_failed_to_play">Failed to start the playback of media</string>
<string name="cast_failed_to_stop">Failed to stop the playback of media</string>
<string name="cast_failed_to_pause">Failed to pause the playback of media</string>
<string name="cast_failed_setting_volume">Failed to set the volume</string>
<string name="cast_failed_no_connection">No connection to the cast device is present</string>
<string name="cast_failed_no_connection_trans">Connection to the cast device has been lost. Application is trying to re-establish the connection, if possible. Please wait for a few seconds and try again.</string>
<string name="cast_failed_status_request">Failed to sync up with the cast device</string>
<string name="cast_failed_seek">Failed to seek to the new position on the cast device</string>
<string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string>
<string name="cast_failed_media_error_skipping">Error playing media. Skipping&#8230;</string>
<!-- Notification channels --> <!-- Notification channels -->
<string name="notification_group_errors">Errors</string> <string name="notification_group_errors">Errors</string>
<string name="notification_group_news">News</string> <string name="notification_group_news">News</string>

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -6,6 +6,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
import de.danoeh.antennapod.model.playback.Playable; 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.Before;
import org.junit.Test; import org.junit.Test;

3
playback/README.md Normal file
View File

@ -0,0 +1,3 @@
# :playback
This folder contains modules that deal with media playback.

3
playback/base/README.md Normal file
View File

@ -0,0 +1,3 @@
# :playback:base
This module provides the basic interfaces for a PlaybackServiceMediaPlayer.

View File

@ -0,0 +1,10 @@
apply plugin: "com.android.library"
apply from: "../../common.gradle"
dependencies {
implementation project(':model')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
testImplementation 'junit:junit:4.13'
}

View File

@ -0,0 +1 @@
<manifest package="de.danoeh.antennapod.playback.base" />

View File

@ -1,10 +1,9 @@
package de.danoeh.antennapod.core.service.playback; package de.danoeh.antennapod.playback.base;
import android.content.Context; import android.content.Context;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
@ -12,6 +11,7 @@ import android.view.SurfaceHolder;
import java.util.List; import java.util.List;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
@ -31,20 +31,20 @@ public abstract class PlaybackServiceMediaPlayer {
/** /**
* Return value of some PSMP methods if the method call failed. * Return value of some PSMP methods if the method call failed.
*/ */
static final int INVALID_TIME = -1; public static final int INVALID_TIME = -1;
private volatile PlayerStatus oldPlayerStatus; private volatile PlayerStatus oldPlayerStatus;
volatile PlayerStatus playerStatus; protected volatile PlayerStatus playerStatus;
/** /**
* A wifi-lock that is acquired if the media file is being streamed. * A wifi-lock that is acquired if the media file is being streamed.
*/ */
private WifiManager.WifiLock wifiLock; private WifiManager.WifiLock wifiLock;
final PSMPCallback callback; protected final PSMPCallback callback;
final Context context; protected final Context context;
PlaybackServiceMediaPlayer(@NonNull Context context, protected PlaybackServiceMediaPlayer(@NonNull Context context,
@NonNull PSMPCallback callback){ @NonNull PSMPCallback callback){
this.context = context; this.context = context;
this.callback = callback; this.callback = callback;
@ -281,7 +281,9 @@ public abstract class PlaybackServiceMediaPlayer {
*/ */
protected abstract boolean shouldLockWifi(); protected abstract boolean shouldLockWifi();
final synchronized void acquireWifiLockIfNecessary() { public abstract boolean isCasting();
protected final synchronized void acquireWifiLockIfNecessary() {
if (shouldLockWifi()) { if (shouldLockWifi()) {
if (wifiLock == null) { if (wifiLock == null) {
wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE)) wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE))
@ -292,7 +294,7 @@ public abstract class PlaybackServiceMediaPlayer {
} }
} }
final synchronized void releaseWifiLockIfNecessary() { protected final synchronized void releaseWifiLockIfNecessary() {
if (wifiLock != null && wifiLock.isHeld()) { if (wifiLock != null && wifiLock.isHeld()) {
wifiLock.release(); wifiLock.release();
} }
@ -313,7 +315,8 @@ public abstract class PlaybackServiceMediaPlayer {
* @param position The position to be set to the current Playable object in case playback started or paused. * @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}. * Will be ignored if given the value of {@link #INVALID_TIME}.
*/ */
final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) { protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus,
Playable newMedia, int position) {
Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus); Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
this.oldPlayerStatus = playerStatus; this.oldPlayerStatus = playerStatus;
@ -339,7 +342,7 @@ public abstract class PlaybackServiceMediaPlayer {
/** /**
* @see #setPlayerStatus(PlayerStatus, Playable, int) * @see #setPlayerStatus(PlayerStatus, Playable, int)
*/ */
final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
setPlayerStatus(newStatus, newMedia, INVALID_TIME); setPlayerStatus(newStatus, newMedia, INVALID_TIME);
} }
@ -350,8 +353,6 @@ public abstract class PlaybackServiceMediaPlayer {
void onMediaChanged(boolean reloadUI); void onMediaChanged(boolean reloadUI);
boolean onMediaPlayerInfo(int code, @StringRes int resourceId);
void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext); void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext);
void onPlaybackStart(@NonNull Playable playable, int position); void onPlaybackStart(@NonNull Playable playable, int position);
@ -360,7 +361,12 @@ public abstract class PlaybackServiceMediaPlayer {
Playable getNextInQueue(Playable currentMedia); Playable getNextInQueue(Playable currentMedia);
@Nullable
Playable findMedia(@NonNull String url);
void onPlaybackEnded(MediaType mediaType, boolean stopPlaying); void onPlaybackEnded(MediaType mediaType, boolean stopPlaying);
void ensureMediaInfoLoaded(@NonNull Playable media);
} }
/** /**
@ -371,7 +377,7 @@ public abstract class PlaybackServiceMediaPlayer {
public PlayerStatus playerStatus; public PlayerStatus playerStatus;
public Playable playable; public Playable playable;
PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) { public PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
this.oldPlayerStatus = oldPlayerStatus; this.oldPlayerStatus = oldPlayerStatus;
this.playerStatus = playerStatus; this.playerStatus = playerStatus;
this.playable = playable; this.playable = playable;

View File

@ -0,0 +1,33 @@
package de.danoeh.antennapod.playback.base;
public enum PlayerStatus {
INDETERMINATE(0), // player is currently changing its state, listeners should wait until the state is left
ERROR(-1),
PREPARING(19),
PAUSED(30),
PLAYING(40),
STOPPED(5),
PREPARED(20),
SEEKING(29),
INITIALIZING(9), // playback service is loading the Playable's metadata
INITIALIZED(10); // playback service was started, data source of media player was set
private final int statusValue;
private static final PlayerStatus[] fromOrdinalLookup;
static {
fromOrdinalLookup = PlayerStatus.values();
}
PlayerStatus(int val) {
statusValue = val;
}
public static PlayerStatus fromOrdinal(int o) {
return fromOrdinalLookup[o];
}
public boolean isAtLeast(PlayerStatus other) {
return other == null || this.statusValue >= other.statusValue;
}
}

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.util; package de.danoeh.antennapod.playback.base;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.util; package de.danoeh.antennapod.playback.base;
import org.junit.Test; import org.junit.Test;

3
playback/cast/README.md Normal file
View File

@ -0,0 +1,3 @@
# :playback:cast
This module provides Chromecast support for the Google Play version of the app.

View File

@ -0,0 +1,17 @@
apply plugin: "com.android.library"
apply from: "../../common.gradle"
apply from: "../../playFlavor.gradle"
dependencies {
implementation project(':event')
implementation project(':model')
implementation project(':playback:base')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.appcompat:appcompat:$appcompatVersion"
implementation "org.greenrobot:eventbus:$eventbusVersion"
annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbusVersion"
playApi 'androidx.mediarouter:mediarouter:1.2.5'
playApi 'com.google.android.gms:play-services-cast-framework:20.0.0'
}

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.activity; package de.danoeh.antennapod.playback.cast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;

View File

@ -0,0 +1,17 @@
package de.danoeh.antennapod.playback.cast;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
/**
* Stub implementation of CastPsmp for Free build flavour
*/
public class CastPsmp {
@Nullable
public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
@NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) {
return null;
}
}

View File

@ -0,0 +1,15 @@
package de.danoeh.antennapod.playback.cast;
import android.content.Context;
public class CastStateListener {
public CastStateListener(Context context) {
}
public void destroy() {
}
public void onSessionStartedOrEnded() {
}
}

View File

@ -0,0 +1 @@
<manifest package="de.danoeh.antennapod.playback.cast" />

View File

@ -0,0 +1,35 @@
package de.danoeh.antennapod.playback.cast;
import android.os.Bundle;
import android.view.Menu;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.gms.cast.framework.CastButtonFactory;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
/**
* Activity that allows for showing the MediaRouter button whenever there's a cast device in the
* network.
*/
public abstract class CastEnabledActivity extends AppCompatActivity {
private static final String TAG = "CastEnabledActivity";
private boolean canCast = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
canCast = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS;
if (canCast) {
CastContext.getSharedInstance(this);
}
}
public void requestCastButton(Menu menu) {
if (!canCast) {
return;
}
getMenuInflater().inflate(R.menu.cast_button, menu);
CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, R.id.media_route_menu_item);
}
}

View File

@ -0,0 +1,26 @@
package de.danoeh.antennapod.playback.cast;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.android.gms.cast.CastMediaControlIntent;
import com.google.android.gms.cast.framework.CastOptions;
import com.google.android.gms.cast.framework.OptionsProvider;
import com.google.android.gms.cast.framework.SessionProvider;
import java.util.List;
@SuppressWarnings("unused")
public class CastOptionsProvider implements OptionsProvider {
@Override
@NonNull
public CastOptions getCastOptions(@NonNull Context context) {
return new CastOptions.Builder()
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
.build();
}
@Override
public List<SessionProvider> getAdditionalSessionProviders(@NonNull Context context) {
return null;
}
}

View File

@ -1,66 +1,77 @@
package de.danoeh.antennapod.core.service.playback; package de.danoeh.antennapod.playback.cast;
import android.content.Context; import android.content.Context;
import android.media.MediaPlayer;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.view.SurfaceHolder; 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.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import de.danoeh.antennapod.core.R; import androidx.annotation.Nullable;
import de.danoeh.antennapod.core.cast.CastConsumer; import com.google.android.gms.cast.MediaError;
import de.danoeh.antennapod.core.cast.CastManager; import com.google.android.gms.cast.MediaInfo;
import de.danoeh.antennapod.core.cast.CastUtils; import com.google.android.gms.cast.MediaLoadOptions;
import de.danoeh.antennapod.core.cast.DefaultCastConsumer; import com.google.android.gms.cast.MediaLoadRequestData;
import de.danoeh.antennapod.core.storage.DBReader; import com.google.android.gms.cast.MediaSeekOptions;
import de.danoeh.antennapod.model.playback.RemoteMedia; import com.google.android.gms.cast.MediaStatus;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastState;
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.model.playback.RemoteMedia;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils;
import org.greenrobot.eventbus.EventBus;
/** /**
* Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices. * Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
*/ */
public class RemotePSMP extends PlaybackServiceMediaPlayer { public class CastPsmp extends PlaybackServiceMediaPlayer {
public static final String TAG = "RemotePSMP"; public static final String TAG = "CastPSMP";
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 Playable media;
private volatile MediaType mediaType; private volatile MediaType mediaType;
private volatile MediaInfo remoteMedia; private volatile MediaInfo remoteMedia;
private volatile int remoteState; private volatile int remoteState;
private final CastContext castContext;
private final RemoteMediaClient remoteMediaClient;
private final AtomicBoolean isBuffering; private final AtomicBoolean isBuffering;
private final AtomicBoolean startWhenPrepared; private final AtomicBoolean startWhenPrepared;
public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) { @Nullable
public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
@NonNull PSMPCallback callback) {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
return null;
}
if (CastContext.getSharedInstance(context).getCastState() == CastState.CONNECTED) {
return new CastPsmp(context, callback);
} else {
return null;
}
}
public CastPsmp(@NonNull Context context, @NonNull PSMPCallback callback) {
super(context, callback); super(context, callback);
castMgr = CastManager.getInstance(); castContext = CastContext.getSharedInstance(context);
remoteMediaClient = castContext.getSessionManager().getCurrentCastSession().getRemoteMediaClient();
remoteMediaClient.registerCallback(remoteMediaClientCallback);
media = null; media = null;
mediaType = null; mediaType = null;
startWhenPrepared = new AtomicBoolean(false); startWhenPrepared = new AtomicBoolean(false);
@ -68,94 +79,48 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
remoteState = MediaStatus.PLAYER_STATE_UNKNOWN; remoteState = MediaStatus.PLAYER_STATE_UNKNOWN;
} }
public void init() { private final RemoteMediaClient.Callback remoteMediaClientCallback = new RemoteMediaClient.Callback() {
try { @Override
if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) { public void onMetadataUpdated() {
super.onMetadataUpdated();
onRemoteMediaPlayerStatusUpdated(); 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 @Override
public void onRemoteMediaPlayerMetadataUpdated() { public void onPreloadStatusUpdated() {
RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(); super.onPreloadStatusUpdated();
onRemoteMediaPlayerStatusUpdated();
} }
@Override @Override
public void onRemoteMediaPlayerStatusUpdated() { public void onStatusUpdated() {
RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(); super.onStatusUpdated();
onRemoteMediaPlayerStatusUpdated();
} }
@Override @Override
public void onMediaLoadResult(int statusCode) { public void onMediaError(@NonNull MediaError mediaError) {
if (playerStatus == PlayerStatus.PREPARING) { EventBus.getDefault().post(new PlayerErrorEvent(mediaError.getReason()));
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) { private void setBuffering(boolean buffering) {
if (buffering && isBuffering.compareAndSet(false, true)) { if (buffering && isBuffering.compareAndSet(false, true)) {
callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0); EventBus.getDefault().post(BufferUpdateEvent.started());
} else if (!buffering && isBuffering.compareAndSet(true, false)) { } else if (!buffering && isBuffering.compareAndSet(true, false)) {
callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0); EventBus.getDefault().post(BufferUpdateEvent.ended());
} }
} }
private Playable localVersion(MediaInfo info){ private Playable localVersion(MediaInfo info) {
if (info == null) { if (info == null || info.getMetadata() == null) {
return null; return null;
} }
if (CastUtils.matches(info, media)) { if (CastUtils.matches(info, media)) {
return media; return media;
} }
return CastUtils.getPlayable(info, true); String streamUrl = info.getMetadata().getString(CastUtils.KEY_STREAM_URL);
return streamUrl == null ? CastUtils.makeRemoteMedia(info) : callback.findMedia(streamUrl);
} }
private MediaInfo remoteVersion(Playable playable) { private MediaInfo remoteVersion(Playable playable) {
@ -166,7 +131,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
return remoteMedia; return remoteMedia;
} }
if (playable instanceof FeedMedia) { if (playable instanceof FeedMedia) {
return CastUtils.convertFromFeedMedia((FeedMedia) playable); return MediaInfoCreator.from((FeedMedia) playable);
} }
if (playable instanceof RemoteMedia) { if (playable instanceof RemoteMedia) {
return MediaInfoCreator.from((RemoteMedia) playable); return MediaInfoCreator.from((RemoteMedia) playable);
@ -175,7 +140,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
} }
private void onRemoteMediaPlayerStatusUpdated() { private void onRemoteMediaPlayerStatusUpdated() {
MediaStatus status = castMgr.getMediaStatus(); MediaStatus status = remoteMediaClient.getMediaStatus();
if (status == null) { if (status == null) {
Log.d(TAG, "Received null MediaStatus"); Log.d(TAG, "Received null MediaStatus");
return; return;
@ -206,8 +171,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
remoteState = state; remoteState = state;
} }
if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING && if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING
state != MediaStatus.PLAYER_STATE_IDLE) { && state != MediaStatus.PLAYER_STATE_IDLE) {
callback.onPlaybackPause(null, INVALID_TIME); callback.onPlaybackPause(null, INVALID_TIME);
// We don't want setPlayerStatus to handle the onPlaybackPause callback // We don't want setPlayerStatus to handle the onPlaybackPause callback
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
@ -230,9 +195,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position); setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position);
break; break;
case MediaStatus.PLAYER_STATE_BUFFERING: case MediaStatus.PLAYER_STATE_BUFFERING:
setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING) ? setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING)
PlayerStatus.PREPARING : PlayerStatus.SEEKING, ? PlayerStatus.PREPARING : PlayerStatus.SEEKING, currentMedia,
currentMedia,
currentMedia != null ? currentMedia.getPosition() : INVALID_TIME); currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
break; break;
case MediaStatus.PLAYER_STATE_IDLE: case MediaStatus.PLAYER_STATE_IDLE:
@ -271,11 +235,13 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
endPlayback(true, false, true, true); endPlayback(true, false, true, true);
return; return;
case MediaStatus.IDLE_REASON_ERROR: case MediaStatus.IDLE_REASON_ERROR:
Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode..."); Log.w(TAG, "Got an error status from the Chromecast. "
callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, + "Skipping, if possible, to the next episode...");
R.string.cast_failed_media_error_skipping); EventBus.getDefault().post(new PlayerErrorEvent("Chromecast error code 1"));
endPlayback(false, false, true, true); endPlayback(false, false, true, true);
return; return;
default:
return;
} }
break; break;
case MediaStatus.PLAYER_STATE_UNKNOWN: case MediaStatus.PLAYER_STATE_UNKNOWN:
@ -284,7 +250,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
} }
break; break;
default: default:
Log.wtf(TAG, "Remote media state undetermined!"); Log.w(TAG, "Remote media state undetermined!");
} }
if (mediaChanged) { if (mediaChanged) {
callback.onMediaChanged(true); callback.onMediaChanged(true);
@ -295,25 +261,29 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
} }
@Override @Override
public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { public void playMediaObject(@NonNull final Playable playable, final boolean stream,
final boolean startWhenPrepared, final boolean prepareImmediately) {
Log.d(TAG, "playMediaObject() called"); Log.d(TAG, "playMediaObject() called");
playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
} }
/** /**
* Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if * Internal implementation of playMediaObject. This method has an additional parameter that
* allows the caller to force a media player reset even if
* the given playable parameter is the same object as the currently playing media. * the given playable parameter is the same object as the currently playing media.
* *
* @see #playMediaObject(Playable, boolean, boolean, boolean) * @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) { private void playMediaObject(@NonNull final Playable playable, final boolean forceReset,
if (!CastUtils.isCastable(playable)) { final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
if (!CastUtils.isCastable(playable, castContext.getSessionManager().getCurrentCastSession())) {
Log.d(TAG, "media provided is not compatible with cast device"); Log.d(TAG, "media provided is not compatible with cast device");
callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable); EventBus.getDefault().post(new PlayerErrorEvent("Media not compatible with cast device"));
Playable nextPlayable = playable; Playable nextPlayable = playable;
do { do {
nextPlayable = callback.getNextInQueue(nextPlayable); nextPlayable = callback.getNextInQueue(nextPlayable);
} while (nextPlayable != null && !CastUtils.isCastable(nextPlayable)); } while (nextPlayable != null && !CastUtils.isCastable(nextPlayable,
castContext.getSessionManager().getCurrentCastSession()));
if (nextPlayable != null) { if (nextPlayable != null) {
playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately); playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately);
} }
@ -328,14 +298,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
return; return;
} else { } else {
// set temporarily to pause in order to update list with current position // set temporarily to pause in order to update list with current position
boolean isPlaying = playerStatus == PlayerStatus.PLAYING; boolean isPlaying = remoteMediaClient.isPlaying();
int position = media.getPosition(); int position = (int) remoteMediaClient.getApproximateStreamPosition();
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) { if (isPlaying) {
callback.onPlaybackPause(media, position); callback.onPlaybackPause(media, position);
} }
@ -343,7 +307,6 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
final Playable oldMedia = media; final Playable oldMedia = media;
callback.onPostPlayback(oldMedia, false, false, true); callback.onPostPlayback(oldMedia, false, false, true);
} }
setPlayerStatus(PlayerStatus.INDETERMINATE, null); setPlayerStatus(PlayerStatus.INDETERMINATE, null);
} }
} }
@ -353,9 +316,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
this.mediaType = media.getMediaType(); this.mediaType = media.getMediaType();
this.startWhenPrepared.set(startWhenPrepared); this.startWhenPrepared.set(startWhenPrepared);
setPlayerStatus(PlayerStatus.INITIALIZING, media); setPlayerStatus(PlayerStatus.INITIALIZING, media);
if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) { callback.ensureMediaInfoLoaded(media);
((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
}
callback.onMediaChanged(true); callback.onMediaChanged(true);
setPlayerStatus(PlayerStatus.INITIALIZED, media); setPlayerStatus(PlayerStatus.INITIALIZED, media);
if (prepareImmediately) { if (prepareImmediately) {
@ -365,29 +326,16 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override @Override
public void resume() { public void resume() {
try {
if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
media.getPosition(), media.getPosition(),
media.getLastPlayedTime()); media.getLastPlayedTime());
castMgr.play(newPosition); seekTo(newPosition);
} else { remoteMediaClient.play();
castMgr.play();
}
} catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
Log.e(TAG, "Unable to resume remote playback", e);
}
} }
@Override @Override
public void pause(boolean abandonFocus, boolean reinit) { public void pause(boolean abandonFocus, boolean reinit) {
try { remoteMediaClient.pause();
if (castMgr.isRemoteMediaPlaying()) {
castMgr.pause();
}
} catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
Log.e(TAG, "Unable to pause", e);
}
} }
@Override @Override
@ -395,18 +343,16 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
if (playerStatus == PlayerStatus.INITIALIZED) { if (playerStatus == PlayerStatus.INITIALIZED) {
Log.d(TAG, "Preparing media player"); Log.d(TAG, "Preparing media player");
setPlayerStatus(PlayerStatus.PREPARING, media); setPlayerStatus(PlayerStatus.PREPARING, media);
try {
int position = media.getPosition(); int position = media.getPosition();
if (position > 0) { if (position > 0) {
position = RewindAfterPauseUtils.calculatePositionWithRewind( position = RewindAfterPauseUtils.calculatePositionWithRewind(
position, position,
media.getLastPlayedTime()); media.getLastPlayedTime());
} }
castMgr.loadMedia(remoteMedia, startWhenPrepared.get(), position); remoteMediaClient.load(new MediaLoadRequestData.Builder()
} catch (TransientNetworkDisconnectionException | NoConnectionException e) { .setMediaInfo(remoteMedia)
Log.e(TAG, "Error loading media", e); .setAutoplay(startWhenPrepared.get())
setPlayerStatus(PlayerStatus.INITIALIZED, media); .setCurrentTime(position).build());
}
} }
} }
@ -422,19 +368,9 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override @Override
public void seekTo(int t) { 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 new Exception("Seeking to " + t).printStackTrace();
try { remoteMediaClient.seek(new MediaSeekOptions.Builder()
if (castMgr.isRemoteMediaLoaded()) { .setPosition(t).build());
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 @Override
@ -449,49 +385,19 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override @Override
public int getDuration() { public int getDuration() {
int retVal = INVALID_TIME; int retVal = (int) remoteMediaClient.getStreamDuration();
boolean prepared; if (retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
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(); retVal = media.getDuration();
} }
Log.d(TAG, "getDuration() -> " + retVal);
return retVal; return retVal;
} }
@Override @Override
public int getPosition() { public int getPosition() {
int retVal = INVALID_TIME; int retVal = (int) remoteMediaClient.getApproximateStreamPosition();
boolean prepared; if (retVal <= 0 && media != null && media.getPosition() >= 0) {
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(); retVal = media.getPosition();
} }
Log.d(TAG, "getPosition() -> " + retVal);
return retVal; return retVal;
} }
@ -507,29 +413,21 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override @Override
public void setPlaybackParams(float speed, boolean skipSilence) { public void setPlaybackParams(float speed, boolean skipSilence) {
//Can be safely ignored as neither set speed not skipSilence is supported double playbackRate = (float) Math.max(MediaLoadOptions.PLAYBACK_RATE_MIN,
Math.min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed));
remoteMediaClient.setPlaybackRate(playbackRate);
} }
@Override @Override
public float getPlaybackSpeed() { public float getPlaybackSpeed() {
return 1; MediaStatus status = remoteMediaClient.getMediaStatus();
return status != null ? (float) status.getPlaybackRate() : 1.0f;
} }
@Override @Override
public void setVolume(float volumeLeft, float volumeRight) { public void setVolume(float volumeLeft, float volumeRight) {
Log.d(TAG, "Setting the Stream volume on Remote Media Player"); Log.d(TAG, "Setting the Stream volume on Remote Media Player");
double volume = (volumeLeft+volumeRight)/2; remoteMediaClient.setStreamVolume(volumeLeft);
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 @Override
@ -554,7 +452,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
@Override @Override
public void shutdown() { public void shutdown() {
castMgr.removeCastConsumer(castConsumer); remoteMediaClient.unregisterCallback(remoteMediaClientCallback);
} }
@Override @Override
@ -626,7 +524,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
boolean playNextEpisode = isPlaying && nextMedia != null; boolean playNextEpisode = isPlaying && nextMedia != null;
if (playNextEpisode) { if (playNextEpisode) {
Log.d(TAG, "Playback of next episode will start immediately."); Log.d(TAG, "Playback of next episode will start immediately.");
} else if (nextMedia == null){ } else if (nextMedia == null) {
Log.d(TAG, "No more episodes available to play"); Log.d(TAG, "No more episodes available to play");
} else { } else {
Log.d(TAG, "Loading next episode, but not playing automatically."); Log.d(TAG, "Loading next episode, but not playing automatically.");
@ -636,45 +534,34 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode); callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode);
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing // setting media to null signals to playMediaObject() that we're taking care of post-playback processing
media = null; media = null;
playMediaObject(nextMedia, false, true /*TODO for now we always stream*/, playNextEpisode, playNextEpisode); playMediaObject(nextMedia, false, true, playNextEpisode, playNextEpisode);
} }
} }
if (shouldContinue || toStoppedState) { if (shouldContinue || toStoppedState) {
boolean shouldPostProcess = true;
if (nextMedia == null) { if (nextMedia == null) {
try { remoteMediaClient.stop();
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. // Otherwise we rely on the chromecast callback to tell us the playback has stopped.
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, nextMedia != null); callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, false);
} else {
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, true);
} }
} else if (isPlaying) { } else if (isPlaying) {
callback.onPlaybackPause(currentMedia, callback.onPlaybackPause(currentMedia,
currentMedia != null ? currentMedia.getPosition() : INVALID_TIME); currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
} }
FutureTask<?> future = new FutureTask<>(() -> {}, null); FutureTask<?> future = new FutureTask<>(() -> { }, null);
future.run(); future.run();
return future; 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 @Override
protected boolean shouldLockWifi() { protected boolean shouldLockWifi() {
return false; return false;
} }
@Override
public boolean isCasting() {
return true;
}
} }

View File

@ -0,0 +1,69 @@
package de.danoeh.antennapod.playback.cast;
import android.content.Context;
import androidx.annotation.NonNull;
import com.google.android.gms.cast.framework.CastContext;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.cast.framework.SessionManagerListener;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
public class CastStateListener implements SessionManagerListener<CastSession> {
private final CastContext castContext;
public CastStateListener(Context context) {
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
castContext = null;
return;
}
castContext = CastContext.getSharedInstance(context);
castContext.getSessionManager().addSessionManagerListener(this, CastSession.class);
}
public void destroy() {
if (castContext != null) {
castContext.getSessionManager().removeSessionManagerListener(this, CastSession.class);
}
}
@Override
public void onSessionStarting(@NonNull CastSession castSession) {
}
@Override
public void onSessionStarted(@NonNull CastSession session, @NonNull String sessionId) {
onSessionStartedOrEnded();
}
@Override
public void onSessionStartFailed(@NonNull CastSession castSession, int i) {
}
@Override
public void onSessionEnding(@NonNull CastSession castSession) {
}
@Override
public void onSessionResumed(@NonNull CastSession session, boolean wasSuspended) {
}
@Override
public void onSessionResumeFailed(@NonNull CastSession castSession, int i) {
}
@Override
public void onSessionSuspended(@NonNull CastSession castSession, int i) {
}
@Override
public void onSessionEnded(@NonNull CastSession session, int error) {
onSessionStartedOrEnded();
}
@Override
public void onSessionResuming(@NonNull CastSession castSession, @NonNull String s) {
}
public void onSessionStartedOrEnded() {
}
}

View File

@ -0,0 +1,181 @@
package de.danoeh.antennapod.playback.cast;
import android.content.ContentResolver;
import android.util.Log;
import android.text.TextUtils;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.framework.CastSession;
import com.google.android.gms.common.images.WebImage;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.model.playback.RemoteMedia;
import java.util.List;
/**
* Helper functions for Cast support.
*/
public class CastUtils {
private CastUtils() {
}
private static final String TAG = "CastUtils";
public static final String KEY_MEDIA_ID = "de.danoeh.antennapod.core.cast.MediaId";
public static final String KEY_EPISODE_IDENTIFIER = "de.danoeh.antennapod.core.cast.EpisodeId";
public static final String KEY_EPISODE_LINK = "de.danoeh.antennapod.core.cast.EpisodeLink";
public static final String KEY_STREAM_URL = "de.danoeh.antennapod.core.cast.StreamUrl";
public static final String KEY_FEED_URL = "de.danoeh.antennapod.core.cast.FeedUrl";
public static final String KEY_FEED_WEBSITE = "de.danoeh.antennapod.core.cast.FeedWebsite";
public static final String KEY_EPISODE_NOTES = "de.danoeh.antennapod.core.cast.EpisodeNotes";
/**
* The field <code>AntennaPod.FormatVersion</code> specifies which version of MediaMetaData
* fields we're using. Future implementations should try to be backwards compatible with earlier
* versions, and earlier versions should be forward compatible until the version indicated by
* <code>MAX_VERSION_FORWARD_COMPATIBILITY</code>. If an update makes the format unreadable for
* an earlier version, then its version number should be greater than the
* <code>MAX_VERSION_FORWARD_COMPATIBILITY</code> value set on the earlier one, so that it
* doesn't try to parse the object.
*/
public static final String KEY_FORMAT_VERSION = "de.danoeh.antennapod.core.cast.FormatVersion";
public static final int FORMAT_VERSION_VALUE = 1;
public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999;
public static boolean isCastable(Playable media, CastSession castSession) {
if (media == null || castSession == null || castSession.getCastDevice() == null) {
return false;
}
if (media instanceof FeedMedia || media instanceof RemoteMedia) {
String url = media.getStreamUrl();
if (url == null || url.isEmpty()) {
return false;
}
if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
return false; // Local feed
}
switch (media.getMediaType()) {
case AUDIO:
return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT);
case VIDEO:
return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT);
default:
return false;
}
}
return false;
}
/**
* Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}.
* @return {@link Playable} object in a format proper for casting.
*/
public static Playable makeRemoteMedia(MediaInfo media) {
MediaMetadata metadata = media.getMetadata();
int version = metadata.getInt(KEY_FORMAT_VERSION);
if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) {
Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this"
+ "version of AntennaPod CastUtils, curVer=" + FORMAT_VERSION_VALUE
+ ", object version=" + version);
return null;
}
List<WebImage> imageList = metadata.getImages();
String imageUrl = null;
if (!imageList.isEmpty()) {
imageUrl = imageList.get(0).getUrl().toString();
}
String notes = metadata.getString(KEY_EPISODE_NOTES);
RemoteMedia result = new RemoteMedia(media.getContentId(),
metadata.getString(KEY_EPISODE_IDENTIFIER),
metadata.getString(KEY_FEED_URL),
metadata.getString(MediaMetadata.KEY_SUBTITLE),
metadata.getString(MediaMetadata.KEY_TITLE),
metadata.getString(KEY_EPISODE_LINK),
metadata.getString(MediaMetadata.KEY_ARTIST),
imageUrl,
metadata.getString(KEY_FEED_WEBSITE),
media.getContentType(),
metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime(),
notes);
if (result.getDuration() == 0 && media.getStreamDuration() > 0) {
result.setDuration((int) media.getStreamDuration());
}
return result;
}
/**
* Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they
* represent the same podcast episode.
*
* @param info the {@link MediaInfo} object to be compared.
* @param media the {@link FeedMedia} object to be compared.
* @return <true>true</true> if there's a match, <code>false</code> otherwise.
*
* @see RemoteMedia#equals(Object)
*/
public static boolean matches(MediaInfo info, FeedMedia media) {
if (info == null || media == null) {
return false;
}
if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
return false;
}
MediaMetadata metadata = info.getMetadata();
FeedItem fi = media.getItem();
if (fi == null || metadata == null
|| !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) {
return false;
}
Feed feed = fi.getFeed();
return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url());
}
/**
* Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they
* represent the same podcast episode.
*
* @param info the {@link MediaInfo} object to be compared.
* @param media the {@link RemoteMedia} object to be compared.
* @return <true>true</true> if there's a match, <code>false</code> otherwise.
*
* @see RemoteMedia#equals(Object)
*/
public static boolean matches(MediaInfo info, RemoteMedia media) {
if (info == null || media == null) {
return false;
}
if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
return false;
}
MediaMetadata metadata = info.getMetadata();
return metadata != null
&& TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier())
&& TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl());
}
/**
* Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they
* represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
* and want to avoid unnecessary conversions.
*
* @param info the {@link MediaInfo} object to be compared.
* @param media the {@link Playable} object to be compared.
* @return <true>true</true> if there's a match, <code>false</code> otherwise.
*
* @see RemoteMedia#equals(Object)
*/
public static boolean matches(MediaInfo info, Playable media) {
if (info == null || media == null) {
return false;
}
if (media instanceof RemoteMedia) {
return matches(info, (RemoteMedia) media);
}
return media instanceof FeedMedia && matches(info, (FeedMedia) media);
}
}

View File

@ -0,0 +1,135 @@
package de.danoeh.antennapod.playback.cast;
import android.net.Uri;
import android.text.TextUtils;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.common.images.WebImage;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.playback.RemoteMedia;
import java.util.Calendar;
public class MediaInfoCreator {
public static MediaInfo from(RemoteMedia media) {
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle());
if (!TextUtils.isEmpty(media.getImageLocation())) {
metadata.addImage(new WebImage(Uri.parse(media.getImageLocation())));
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(media.getPubDate());
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
if (!TextUtils.isEmpty(media.getFeedAuthor())) {
metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor());
}
if (!TextUtils.isEmpty(media.getFeedUrl())) {
metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl());
}
if (!TextUtils.isEmpty(media.getFeedLink())) {
metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink());
}
if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier());
} else {
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl());
}
if (!TextUtils.isEmpty(media.getEpisodeLink())) {
metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink());
}
String notes = media.getNotes();
if (notes != null) {
metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
}
// Default id value
metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl())
.setContentType(media.getMimeType())
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(metadata);
if (media.getDuration() > 0) {
builder.setStreamDuration(media.getDuration());
}
return builder.build();
}
/**
* Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device.
* Before using this method, one should make sure isCastable(Playable) returns
* {@code true}. This method should not run on the main thread.
*
* @param media The {@link FeedMedia} object to be converted.
* @return {@link MediaInfo} object in a format proper for casting.
*/
public static MediaInfo from(FeedMedia media) {
if (media == null) {
return null;
}
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
if (media.getItem() == null) {
throw new IllegalStateException("item is null");
//media.setItem(DBReader.getFeedItem(media.getItemId()));
}
FeedItem feedItem = media.getItem();
if (feedItem != null) {
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
String subtitle = media.getFeedTitle();
if (subtitle != null) {
metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
}
// Manual because cast does not support embedded images
String url = feedItem.getImageUrl() == null ? feedItem.getFeed().getImageUrl() : feedItem.getImageUrl();
if (!TextUtils.isEmpty(url)) {
metadata.addImage(new WebImage(Uri.parse(url)));
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(media.getItem().getPubDate());
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
Feed feed = feedItem.getFeed();
if (feed != null) {
if (!TextUtils.isEmpty(feed.getAuthor())) {
metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor());
}
if (!TextUtils.isEmpty(feed.getDownload_url())) {
metadata.putString(CastUtils.KEY_FEED_URL, feed.getDownload_url());
}
if (!TextUtils.isEmpty(feed.getLink())) {
metadata.putString(CastUtils.KEY_FEED_WEBSITE, feed.getLink());
}
}
if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) {
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier());
} else {
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl());
}
if (!TextUtils.isEmpty(feedItem.getLink())) {
metadata.putString(CastUtils.KEY_EPISODE_LINK, feedItem.getLink());
}
}
// This field only identifies the id on the device that has the original version.
// Idea is to perhaps, on a first approach, check if the version on the local DB with the
// same id matches the remote object, and if not then search for episode and feed identifiers.
// This at least should make media recognition for a single device much quicker.
metadata.putInt(CastUtils.KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue());
// A way to identify different casting media formats in case we change it in the future and
// senders with different versions share a casting device.
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl())
.setContentType(media.getMime_type())
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setMetadata(metadata);
if (media.getDuration() > 0) {
builder.setStreamDuration(media.getDuration());
}
return builder.build();
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/media_route_menu_item"
android:title=""
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
app:showAsAction="always" />
</menu>

View File

@ -10,6 +10,9 @@ include ':net:sync:model'
include ':parser:feed' include ':parser:feed'
include ':parser:media' include ':parser:media'
include ':playback:base'
include ':playback:cast'
include ':ui:app-start-intent' include ':ui:app-start-intent'
include ':ui:common' include ':ui:common'
include ':ui:png-icons' include ':ui:png-icons'

View File

@ -1,5 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="30dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="30dp">
<path android:fillColor="#FFFFFFFF" android:pathData="M1.6,1.27L0.25,2.75L1.41,3.8C1.16,4.13 1,4.55 1,5V8H3V5.23L18.2,19H14V21H20.41L22.31,22.72L23.65,21.24M6.5,3L8.7,5H21V16.14L23,17.95V5C23,3.89 22.1,3 21,3M1,10V12A9,9 0 0,1 10,21H12C12,14.92 7.08,10 1,10M1,14V16A5,5 0 0,1 6,21H8A7,7 0 0,0 1,14M1,18V21H4A3,3 0 0,0 1,18Z" />
</vector>