Chromecast rework (#5518)
This commit is contained in:
parent
af2835c59d
commit
f0100e61ac
@ -116,6 +116,8 @@ dependencies {
|
||||
implementation project(':net:sync:gpoddernet')
|
||||
implementation project(':net:sync:model')
|
||||
implementation project(':parser:feed')
|
||||
implementation project(':playback:base')
|
||||
implementation project(':playback:cast')
|
||||
implementation project(':ui:app-start-intent')
|
||||
implementation project(':ui:common')
|
||||
|
||||
|
@ -11,6 +11,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
import de.danoeh.antennapod.model.feed.FeedItemFilter;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.junit.After;
|
||||
@ -32,7 +33,6 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.util.IntentUtils;
|
||||
|
@ -1,9 +1,10 @@
|
||||
package de.test.antennapod.service.playback;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import de.danoeh.antennapod.model.playback.MediaType;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
|
||||
public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
|
||||
|
||||
@ -42,14 +43,6 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
|
||||
originalCallback.onMediaChanged(reloadUI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMediaPlayerInfo(int code, int resourceId) {
|
||||
if (isCancelled) {
|
||||
return true;
|
||||
}
|
||||
return originalCallback.onMediaPlayerInfo(code, resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
|
||||
if (isCancelled) {
|
||||
@ -82,6 +75,15 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
|
||||
return originalCallback.getNextInQueue(currentMedia);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Playable findMedia(@NonNull String url) {
|
||||
if (isCancelled) {
|
||||
return null;
|
||||
}
|
||||
return originalCallback.findMedia(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
|
||||
if (isCancelled) {
|
||||
@ -89,4 +91,12 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
|
||||
}
|
||||
originalCallback.onPlaybackEnded(mediaType, stopPlaying);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureMediaInfoLoaded(@NonNull Playable media) {
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
originalCallback.ensureMediaInfoLoaded(media);
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package de.test.antennapod.service.playback;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import de.danoeh.antennapod.model.playback.MediaType;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
|
||||
public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallback {
|
||||
@Override
|
||||
@ -22,11 +22,6 @@ public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallb
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext) {
|
||||
|
||||
@ -47,8 +42,18 @@ public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallb
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Playable findMedia(@NonNull String url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureMediaInfoLoaded(@NonNull Playable media) {
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@ import android.content.Context;
|
||||
import androidx.test.filters.MediumTest;
|
||||
|
||||
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import de.test.antennapod.EspressoTestUtils;
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
||||
@ -24,8 +26,6 @@ import de.danoeh.antennapod.model.feed.FeedItem;
|
||||
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.model.feed.FeedPreferences;
|
||||
import de.danoeh.antennapod.core.service.playback.LocalPSMP;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.core.storage.PodDBAdapter;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.test.antennapod.util.service.download.HTTPBin;
|
||||
|
@ -1,7 +0,0 @@
|
||||
package de.danoeh.antennapod.config;
|
||||
|
||||
import de.danoeh.antennapod.core.CastCallbacks;
|
||||
|
||||
class CastCallbackImpl implements CastCallbacks {
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -54,6 +54,9 @@
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/>
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="de.danoeh.antennapod.playback.cast.CastOptionsProvider" />
|
||||
|
||||
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
|
||||
|
@ -38,6 +38,7 @@ import com.bumptech.glide.Glide;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
@ -43,7 +43,6 @@ import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
|
||||
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.util.Converter;
|
||||
@ -62,6 +61,8 @@ import de.danoeh.antennapod.dialog.SleepTimerDialog;
|
||||
import de.danoeh.antennapod.model.feed.FeedItem;
|
||||
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
|
||||
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
@ -15,6 +15,5 @@ class ClientConfigurator {
|
||||
ClientConfig.USER_AGENT = "AntennaPod/" + BuildConfig.VERSION_NAME;
|
||||
ClientConfig.applicationCallbacks = new ApplicationCallbacksImpl();
|
||||
ClientConfig.downloadServiceCallbacks = new DownloadServiceCallbacksImpl();
|
||||
ClientConfig.castCallbacks = new CastCallbackImpl();
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
|
||||
import de.danoeh.antennapod.event.PlayerErrorEvent;
|
||||
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
|
||||
import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
|
||||
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
@ -41,7 +42,6 @@ import java.text.NumberFormat;
|
||||
import java.util.List;
|
||||
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.activity.CastEnabledActivity;
|
||||
import de.danoeh.antennapod.activity.MainActivity;
|
||||
import de.danoeh.antennapod.event.FavoritesEvent;
|
||||
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||
|
@ -17,6 +17,7 @@ import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
@ -24,7 +25,6 @@ import org.greenrobot.eventbus.ThreadMode;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.adapter.ChaptersListAdapter;
|
||||
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.core.util.ChapterUtils;
|
||||
import de.danoeh.antennapod.core.util.playback.PlaybackController;
|
||||
import de.danoeh.antennapod.model.feed.Chapter;
|
||||
|
@ -23,9 +23,9 @@ import de.danoeh.antennapod.model.playback.MediaType;
|
||||
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
|
||||
import de.danoeh.antennapod.core.glide.ApGlideSettings;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.core.util.playback.PlaybackController;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import de.danoeh.antennapod.view.PlayButton;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
@ -16,7 +16,6 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.util.gui.PictureInPictureUtil;
|
||||
import de.danoeh.antennapod.dialog.SkipPreferenceDialog;
|
||||
import de.danoeh.antennapod.dialog.VariableSpeedDialog;
|
||||
import de.danoeh.antennapod.preferences.PreferenceControllerFlavorHelper;
|
||||
import java.util.Map;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
@ -31,7 +30,6 @@ public class PlaybackPreferencesFragment extends PreferenceFragmentCompat {
|
||||
addPreferencesFromResource(R.xml.preferences_playback);
|
||||
|
||||
setupPlaybackScreen();
|
||||
PreferenceControllerFlavorHelper.setupFlavoredUI(this);
|
||||
buildSmartMarkAsPlayedPreference();
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -127,11 +127,5 @@
|
||||
android:title="@string/media_player"
|
||||
android:summary="@string/pref_media_player_message"
|
||||
android:entryValues="@array/media_player_values"/>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:enabled="true"
|
||||
android:key="prefCast"
|
||||
android:summary="@string/pref_cast_message"
|
||||
android:title="@string/pref_cast_title"/>
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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>
|
@ -27,6 +27,8 @@ dependencies {
|
||||
implementation project(':net:sync:model')
|
||||
implementation project(':parser:feed')
|
||||
implementation project(':parser:media')
|
||||
implementation project(':playback:base')
|
||||
implementation project(':playback:cast')
|
||||
implementation project(':ui:app-start-intent')
|
||||
implementation project(':ui:common')
|
||||
implementation project(':ui:png-icons')
|
||||
@ -61,9 +63,6 @@ dependencies {
|
||||
implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
|
||||
|
||||
// Non-free dependencies:
|
||||
playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1'
|
||||
playApi 'androidx.mediarouter:mediarouter:1.0.0'
|
||||
playApi "com.google.android.gms:play-services-cast:$playServicesVersion"
|
||||
playApi "com.google.android.support:wearable:$wearableSupportVersion"
|
||||
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
package de.danoeh.antennapod.core;
|
||||
|
||||
/**
|
||||
* Callbacks for Chromecast support on the core module
|
||||
*/
|
||||
public interface CastCallbacks {
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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>
|
@ -30,8 +30,6 @@ public class ClientConfig {
|
||||
|
||||
public static DownloadServiceCallbacks downloadServiceCallbacks;
|
||||
|
||||
public static CastCallbacks castCallbacks;
|
||||
|
||||
private static boolean initialized = false;
|
||||
|
||||
public static synchronized void initialize(Context context) {
|
@ -8,8 +8,8 @@ import android.util.Log;
|
||||
import de.danoeh.antennapod.event.PlayerStatusEvent;
|
||||
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.model.playback.MediaType;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL;
|
||||
|
@ -40,6 +40,7 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
|
||||
import de.danoeh.antennapod.core.service.download.HttpDownloader;
|
||||
import de.danoeh.antennapod.core.util.NetworkUtils;
|
||||
import de.danoeh.antennapod.core.util.playback.IPlayer;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
@ -17,8 +17,9 @@ import androidx.media.AudioManagerCompat;
|
||||
import de.danoeh.antennapod.event.PlayerErrorEvent;
|
||||
import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
|
||||
import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.core.util.playback.MediaPlayerError;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import org.antennapod.audio.MediaPlayer;
|
||||
|
||||
import java.io.File;
|
||||
@ -39,7 +40,7 @@ import de.danoeh.antennapod.model.playback.MediaType;
|
||||
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
|
||||
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
|
||||
import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils;
|
||||
import de.danoeh.antennapod.core.util.playback.AudioPlayer;
|
||||
import de.danoeh.antennapod.core.util.playback.IPlayer;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
@ -148,7 +149,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
|
||||
}
|
||||
|
||||
public LocalPSMP(@NonNull Context context,
|
||||
@NonNull PSMPCallback callback) {
|
||||
@NonNull PlaybackServiceMediaPlayer.PSMPCallback callback) {
|
||||
super(context, callback);
|
||||
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
this.playerLock = new PlayerLock();
|
||||
@ -265,9 +266,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
|
||||
LocalPSMP.this.startWhenPrepared.set(startWhenPrepared);
|
||||
setPlayerStatus(PlayerStatus.INITIALIZING, media);
|
||||
try {
|
||||
if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
|
||||
((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
|
||||
}
|
||||
callback.ensureMediaInfoLoaded(media);
|
||||
callback.onMediaChanged(false);
|
||||
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence());
|
||||
if (stream) {
|
||||
@ -1098,7 +1097,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
|
||||
EventBus.getDefault().post(BufferUpdateEvent.ended());
|
||||
return true;
|
||||
default:
|
||||
return callback.onMediaPlayerInfo(what, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1148,4 +1147,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
|
||||
executor.submit(r);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCasting() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
@ -47,6 +48,10 @@ import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
|
||||
import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
|
||||
import de.danoeh.antennapod.event.PlayerErrorEvent;
|
||||
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import de.danoeh.antennapod.playback.cast.CastPsmp;
|
||||
import de.danoeh.antennapod.playback.cast.CastStateListener;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
@ -103,24 +108,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
*/
|
||||
private static final String TAG = "PlaybackService";
|
||||
|
||||
/**
|
||||
* Parcelable of type Playable.
|
||||
*/
|
||||
public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
|
||||
/**
|
||||
* True if cast session should disconnect.
|
||||
*/
|
||||
public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect";
|
||||
/**
|
||||
* True if media should be streamed.
|
||||
*/
|
||||
public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream";
|
||||
public static final String EXTRA_ALLOW_STREAM_THIS_TIME = "extra.de.danoeh.antennapod.core.service.allowStream";
|
||||
public static final String EXTRA_ALLOW_STREAM_ALWAYS = "extra.de.danoeh.antennapod.core.service.allowStreamAlways";
|
||||
/**
|
||||
* True if playback should be started immediately after media has been
|
||||
* prepared.
|
||||
*/
|
||||
public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared";
|
||||
|
||||
public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately";
|
||||
@ -200,10 +191,10 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
|
||||
private PlaybackServiceMediaPlayer mediaPlayer;
|
||||
private PlaybackServiceTaskManager taskManager;
|
||||
private PlaybackServiceFlavorHelper flavorHelper;
|
||||
private PlaybackServiceStateManager stateManager;
|
||||
private Disposable positionEventTimer;
|
||||
private PlaybackServiceNotificationBuilder notificationBuilder;
|
||||
private CastStateListener castStateListener;
|
||||
|
||||
private String autoSkippedFeedMediaId = null;
|
||||
|
||||
@ -280,7 +271,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
EventBus.getDefault().register(this);
|
||||
taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
|
||||
|
||||
flavorHelper = new PlaybackServiceFlavorHelper(PlaybackService.this, flavorHelperCallback);
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.registerOnSharedPreferenceChangeListener(prefListener);
|
||||
|
||||
@ -305,12 +295,36 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
npe.printStackTrace();
|
||||
}
|
||||
|
||||
flavorHelper.initializeMediaPlayer(PlaybackService.this);
|
||||
recreateMediaPlayer();
|
||||
mediaSession.setActive(true);
|
||||
|
||||
castStateListener = new CastStateListener(this) {
|
||||
@Override
|
||||
public void onSessionStartedOrEnded() {
|
||||
recreateMediaPlayer();
|
||||
}
|
||||
};
|
||||
EventBus.getDefault().post(new PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED));
|
||||
}
|
||||
|
||||
void recreateMediaPlayer() {
|
||||
Playable media = null;
|
||||
boolean wasPlaying = false;
|
||||
if (mediaPlayer != null) {
|
||||
media = mediaPlayer.getPlayable();
|
||||
wasPlaying = mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING;
|
||||
mediaPlayer.pause(true, false);
|
||||
mediaPlayer.shutdown();
|
||||
}
|
||||
mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback);
|
||||
if (mediaPlayer == null) {
|
||||
mediaPlayer = new LocalPSMP(this, mediaPlayerCallback); // Cast not supported or not connected
|
||||
}
|
||||
if (media != null) {
|
||||
mediaPlayer.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true);
|
||||
}
|
||||
isCasting = mediaPlayer.isCasting();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
@ -324,6 +338,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
stateManager.stopForeground(!UserPreferences.isPersistNotify());
|
||||
isRunning = false;
|
||||
currentMediaType = MediaType.UNKNOWN;
|
||||
castStateListener.destroy();
|
||||
|
||||
cancelPositionObserver();
|
||||
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener);
|
||||
@ -337,8 +352,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
unregisterReceiver(audioBecomingNoisy);
|
||||
unregisterReceiver(skipCurrentEpisodeReceiver);
|
||||
unregisterReceiver(pausePlayCurrentEpisodeReceiver);
|
||||
flavorHelper.removeCastConsumer();
|
||||
flavorHelper.unregisterWifiBroadcastReceiver();
|
||||
mediaPlayer.shutdown();
|
||||
taskManager.shutdown();
|
||||
}
|
||||
@ -483,9 +496,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
|
||||
final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
|
||||
final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false);
|
||||
final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
|
||||
Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
|
||||
if (keycode == -1 && playable == null && !castDisconnect) {
|
||||
if (keycode == -1 && playable == null) {
|
||||
Log.e(TAG, "PlaybackService was started with no arguments");
|
||||
stateManager.stopService();
|
||||
return Service.START_NOT_STICKY;
|
||||
@ -509,7 +521,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
stateManager.stopService();
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
} else if (!flavorHelper.castDisconnect(castDisconnect) && playable != null) {
|
||||
} else {
|
||||
stateManager.validStartCommandWasReceived();
|
||||
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true);
|
||||
boolean allowStreamThisTime = intent.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false);
|
||||
@ -553,9 +565,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
stateManager.stopService();
|
||||
});
|
||||
return Service.START_NOT_STICKY;
|
||||
} else {
|
||||
Log.d(TAG, "Did not handle intent to PlaybackService: " + intent);
|
||||
Log.d(TAG, "Extras: " + intent.getExtras());
|
||||
}
|
||||
}
|
||||
|
||||
@ -781,8 +790,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
saveCurrentPosition(true, null, PlaybackServiceMediaPlayer.INVALID_TIME);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public WidgetUpdater.WidgetState requestWidgetState() {
|
||||
return new WidgetUpdater.WidgetState(getPlayable(), getStatus(),
|
||||
@ -872,11 +879,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
updateNotificationAndMediaSession(getPlayable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
|
||||
return flavorHelper.onMediaPlayerInfo(PlaybackService.this, code, resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped,
|
||||
boolean playingNext) {
|
||||
@ -916,10 +918,24 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
return PlaybackService.this.getNextInQueue(currentMedia);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Playable findMedia(@NonNull String url) {
|
||||
FeedItem item = DBReader.getFeedItemByGuidOrEpisodeUrl(null, url);
|
||||
return item != null ? item.getMedia() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) {
|
||||
PlaybackService.this.onPlaybackEnded(mediaType, stopPlaying);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ensureMediaInfoLoaded(@NonNull Playable media) {
|
||||
if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
|
||||
((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@ -1248,15 +1264,15 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
// This would give the PIP of videos a play button
|
||||
capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY;
|
||||
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) {
|
||||
flavorHelper.sessionStateAddActionForWear(sessionState,
|
||||
WearMediaSession.sessionStateAddActionForWear(sessionState,
|
||||
CUSTOM_ACTION_REWIND,
|
||||
getString(R.string.rewind_label),
|
||||
android.R.drawable.ic_media_rew);
|
||||
flavorHelper.sessionStateAddActionForWear(sessionState,
|
||||
WearMediaSession.sessionStateAddActionForWear(sessionState,
|
||||
CUSTOM_ACTION_FAST_FORWARD,
|
||||
getString(R.string.fast_forward_label),
|
||||
android.R.drawable.ic_media_ff);
|
||||
flavorHelper.mediaSessionSetExtraForWear(mediaSession);
|
||||
WearMediaSession.mediaSessionSetExtraForWear(mediaSession);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1338,7 +1354,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
notificationBuilder.setPlayable(playable);
|
||||
notificationBuilder.setMediaSessionToken(mediaSession.getSessionToken());
|
||||
notificationBuilder.setPlayerStatus(playerStatus);
|
||||
notificationBuilder.setCasting(isCasting);
|
||||
notificationBuilder.updatePosition(getCurrentPosition(), getCurrentPlaybackSpeed());
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
@ -1901,93 +1916,6 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
(sharedPreferences, key) -> {
|
||||
if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) {
|
||||
updateNotificationAndMediaSession(getPlayable());
|
||||
} else {
|
||||
flavorHelper.onSharedPreference(key);
|
||||
}
|
||||
};
|
||||
|
||||
interface FlavorHelperCallback {
|
||||
PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback();
|
||||
|
||||
void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer);
|
||||
|
||||
PlaybackServiceMediaPlayer getMediaPlayer();
|
||||
|
||||
void setIsCasting(boolean isCasting);
|
||||
|
||||
void sendNotificationBroadcast(int type, int code);
|
||||
|
||||
void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position);
|
||||
|
||||
void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info);
|
||||
|
||||
MediaSessionCompat getMediaSession();
|
||||
|
||||
Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter);
|
||||
|
||||
void unregisterReceiver(BroadcastReceiver receiver);
|
||||
}
|
||||
|
||||
private final FlavorHelperCallback flavorHelperCallback = new FlavorHelperCallback() {
|
||||
@Override
|
||||
public PlaybackServiceMediaPlayer.PSMPCallback getMediaPlayerCallback() {
|
||||
return PlaybackService.this.mediaPlayerCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer) {
|
||||
PlaybackService.this.mediaPlayer = mediaPlayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaybackServiceMediaPlayer getMediaPlayer() {
|
||||
return PlaybackService.this.mediaPlayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIsCasting(boolean isCasting) {
|
||||
PlaybackService.isCasting = isCasting;
|
||||
stateManager.validStartCommandWasReceived();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendNotificationBroadcast(int type, int code) {
|
||||
PlaybackService.this.sendNotificationBroadcast(type, code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveCurrentPosition(boolean fromMediaPlayer, Playable playable, int position) {
|
||||
PlaybackService.this.saveCurrentPosition(fromMediaPlayer, playable, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupNotification(boolean connected, PlaybackServiceMediaPlayer.PSMPInfo info) {
|
||||
if (connected) {
|
||||
PlaybackService.this.updateNotificationAndMediaSession(info.playable);
|
||||
} else {
|
||||
PlayerStatus status = info.playerStatus;
|
||||
if (status == PlayerStatus.PLAYING || status == PlayerStatus.SEEKING
|
||||
|| status == PlayerStatus.PREPARING || UserPreferences.isPersistNotify()) {
|
||||
PlaybackService.this.updateNotificationAndMediaSession(info.playable);
|
||||
} else if (!UserPreferences.isPersistNotify()) {
|
||||
stateManager.stopForeground(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaSessionCompat getMediaSession() {
|
||||
return PlaybackService.this.mediaSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
||||
return PlaybackService.this.registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterReceiver(BroadcastReceiver receiver) {
|
||||
PlaybackService.this.unregisterReceiver(receiver);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -31,17 +31,17 @@ import de.danoeh.antennapod.model.playback.Playable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
public class PlaybackServiceNotificationBuilder {
|
||||
private static final String TAG = "PlaybackSrvNotification";
|
||||
private static Bitmap defaultIcon = null;
|
||||
|
||||
private Context context;
|
||||
private final Context context;
|
||||
private Playable playable;
|
||||
private MediaSessionCompat.Token mediaSessionToken;
|
||||
private PlayerStatus playerStatus;
|
||||
private boolean isCasting;
|
||||
private Bitmap icon;
|
||||
private String position;
|
||||
|
||||
@ -140,7 +140,7 @@ public class PlaybackServiceNotificationBuilder {
|
||||
if (playable != null) {
|
||||
notification.setContentTitle(playable.getFeedTitle());
|
||||
notification.setContentText(playable.getEpisodeTitle());
|
||||
addActions(notification, mediaSessionToken, playerStatus, isCasting);
|
||||
addActions(notification, mediaSessionToken, playerStatus);
|
||||
|
||||
if (icon != null) {
|
||||
notification.setLargeIcon(icon);
|
||||
@ -175,23 +175,10 @@ public class PlaybackServiceNotificationBuilder {
|
||||
}
|
||||
|
||||
private void addActions(NotificationCompat.Builder notification, MediaSessionCompat.Token mediaSessionToken,
|
||||
PlayerStatus playerStatus, boolean isCasting) {
|
||||
PlayerStatus playerStatus) {
|
||||
ArrayList<Integer> compactActionList = new ArrayList<>();
|
||||
|
||||
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
|
||||
|
||||
if (isCasting) {
|
||||
Intent stopCastingIntent = new Intent(context, PlaybackService.class);
|
||||
stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true);
|
||||
PendingIntent stopCastingPendingIntent = PendingIntent.getService(context,
|
||||
numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||
| (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
|
||||
notification.addAction(R.drawable.ic_notification_cast_off,
|
||||
context.getString(R.string.cast_disconnect_label),
|
||||
stopCastingPendingIntent);
|
||||
numActions++;
|
||||
}
|
||||
|
||||
// always let them rewind
|
||||
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
|
||||
KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
|
||||
@ -270,10 +257,6 @@ public class PlaybackServiceNotificationBuilder {
|
||||
this.playerStatus = playerStatus;
|
||||
}
|
||||
|
||||
public void setCasting(boolean casting) {
|
||||
isCasting = casting;
|
||||
}
|
||||
|
||||
public PlayerStatus getPlayerStatus() {
|
||||
return playerStatus;
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.model.feed.FeedPreferences;
|
||||
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
|
||||
class PlaybackVolumeUpdater {
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -22,9 +22,9 @@ import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
|
||||
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackService;
|
||||
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
@ -25,11 +25,11 @@ import de.danoeh.antennapod.model.playback.MediaType;
|
||||
import de.danoeh.antennapod.core.glide.ApGlideSettings;
|
||||
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
|
||||
import de.danoeh.antennapod.core.receiver.PlayerWidget;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.core.util.Converter;
|
||||
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
|
||||
import de.danoeh.antennapod.core.util.TimeSpeedConverter;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
|
||||
import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
|
||||
|
||||
|
@ -6,9 +6,9 @@ import androidx.annotation.NonNull;
|
||||
import androidx.core.app.SafeJobIntentService;
|
||||
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
|
||||
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
|
||||
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.core.util.playback.PlayableUtils;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
|
||||
public class WidgetUpdaterJobService extends SafeJobIntentService {
|
||||
private static final int JOB_ID = -17001;
|
||||
|
@ -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>
|
@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<dimen name="widget_margin">0dp</dimen>
|
||||
<dimen name="external_player_height">64dp</dimen>
|
||||
<dimen name="text_size_micro">12sp</dimen>
|
||||
@ -28,11 +27,5 @@
|
||||
<dimen name="audioplayer_playercontrols_length_big">64dp</dimen>
|
||||
<dimen name="audioplayer_playercontrols_margin">12dp</dimen>
|
||||
|
||||
<dimen name="media_router_controller_playback_control_vertical_padding">16dp</dimen>
|
||||
<dimen name="media_router_controller_playback_control_horizontal_spacing">12dp</dimen>
|
||||
<dimen name="media_router_controller_playback_control_start_padding">24dp</dimen>
|
||||
<dimen name="media_router_controller_bottom_margin">8dp</dimen>
|
||||
|
||||
<dimen name="nav_drawer_max_screen_size">480dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
@ -502,9 +502,6 @@
|
||||
<string name="pref_proxy_title">Proxy</string>
|
||||
<string name="pref_proxy_sum">Set a network proxy</string>
|
||||
<string name="pref_no_browser_found">No web browser found.</string>
|
||||
<string name="pref_cast_title">Chromecast support</string>
|
||||
<string name="pref_cast_message_play_flavor">Enable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV)</string>
|
||||
<string name="pref_cast_message_free_flavor" tools:ignore="UnusedResources">Chromecast requires third party proprietary libraries that are disabled in this version of AntennaPod</string>
|
||||
<string name="pref_enqueue_downloaded_title">Enqueue Downloaded</string>
|
||||
<string name="pref_enqueue_downloaded_summary">Add downloaded episodes to the queue</string>
|
||||
<string name="media_player_builtin">Built-in Android player (deprecated) </string>
|
||||
@ -664,7 +661,6 @@
|
||||
<string name="pref_pausePlaybackForFocusLoss_title">Pause for Interruptions</string>
|
||||
<string name="pref_resumeAfterCall_sum">Resume playback after a phone call completes</string>
|
||||
<string name="pref_resumeAfterCall_title">Resume after Call</string>
|
||||
<string name="pref_restart_required">AntennaPod has to be restarted for this change to take effect.</string>
|
||||
|
||||
<!-- Online feed view -->
|
||||
<string name="subscribe_label">Subscribe</string>
|
||||
@ -808,21 +804,6 @@
|
||||
<!-- Subscriptions fragment -->
|
||||
<string name="subscription_num_columns">Number of columns</string>
|
||||
|
||||
<!-- Casting -->
|
||||
<string name="cast_media_route_menu_title">Play on…</string>
|
||||
<string name="cast_disconnect_label">Disconnect the cast session</string>
|
||||
<string name="cast_not_castable">Media selected is not compatible with cast device</string>
|
||||
<string name="cast_failed_to_play">Failed to start the playback of media</string>
|
||||
<string name="cast_failed_to_stop">Failed to stop the playback of media</string>
|
||||
<string name="cast_failed_to_pause">Failed to pause the playback of media</string>
|
||||
<string name="cast_failed_setting_volume">Failed to set the volume</string>
|
||||
<string name="cast_failed_no_connection">No connection to the cast device is present</string>
|
||||
<string name="cast_failed_no_connection_trans">Connection to the cast device has been lost. Application is trying to re-establish the connection, if possible. Please wait for a few seconds and try again.</string>
|
||||
<string name="cast_failed_status_request">Failed to sync up with the cast device</string>
|
||||
<string name="cast_failed_seek">Failed to seek to the new position on the cast device</string>
|
||||
<string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string>
|
||||
<string name="cast_failed_media_error_skipping">Error playing media. Skipping…</string>
|
||||
|
||||
<!-- Notification channels -->
|
||||
<string name="notification_group_errors">Errors</string>
|
||||
<string name="notification_group_news">News</string>
|
||||
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -6,6 +6,8 @@ import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.model.feed.FeedPreferences;
|
||||
import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
|
3
playback/README.md
Normal file
3
playback/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# :playback
|
||||
|
||||
This folder contains modules that deal with media playback.
|
3
playback/base/README.md
Normal file
3
playback/base/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# :playback:base
|
||||
|
||||
This module provides the basic interfaces for a PlaybackServiceMediaPlayer.
|
10
playback/base/build.gradle
Normal file
10
playback/base/build.gradle
Normal 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'
|
||||
}
|
1
playback/base/src/main/AndroidManifest.xml
Normal file
1
playback/base/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="de.danoeh.antennapod.playback.base" />
|
@ -1,10 +1,9 @@
|
||||
package de.danoeh.antennapod.core.service.playback;
|
||||
package de.danoeh.antennapod.playback.base;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.net.wifi.WifiManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.SurfaceHolder;
|
||||
@ -12,6 +11,7 @@ import android.view.SurfaceHolder;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import de.danoeh.antennapod.model.playback.MediaType;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
|
||||
@ -31,20 +31,20 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
/**
|
||||
* Return value of some PSMP methods if the method call failed.
|
||||
*/
|
||||
static final int INVALID_TIME = -1;
|
||||
public static final int INVALID_TIME = -1;
|
||||
|
||||
private volatile PlayerStatus oldPlayerStatus;
|
||||
volatile PlayerStatus playerStatus;
|
||||
protected volatile PlayerStatus playerStatus;
|
||||
|
||||
/**
|
||||
* A wifi-lock that is acquired if the media file is being streamed.
|
||||
*/
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
|
||||
final PSMPCallback callback;
|
||||
final Context context;
|
||||
protected final PSMPCallback callback;
|
||||
protected final Context context;
|
||||
|
||||
PlaybackServiceMediaPlayer(@NonNull Context context,
|
||||
protected PlaybackServiceMediaPlayer(@NonNull Context context,
|
||||
@NonNull PSMPCallback callback){
|
||||
this.context = context;
|
||||
this.callback = callback;
|
||||
@ -281,7 +281,9 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
*/
|
||||
protected abstract boolean shouldLockWifi();
|
||||
|
||||
final synchronized void acquireWifiLockIfNecessary() {
|
||||
public abstract boolean isCasting();
|
||||
|
||||
protected final synchronized void acquireWifiLockIfNecessary() {
|
||||
if (shouldLockWifi()) {
|
||||
if (wifiLock == null) {
|
||||
wifiLock = ((WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE))
|
||||
@ -292,7 +294,7 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
final synchronized void releaseWifiLockIfNecessary() {
|
||||
protected final synchronized void releaseWifiLockIfNecessary() {
|
||||
if (wifiLock != null && wifiLock.isHeld()) {
|
||||
wifiLock.release();
|
||||
}
|
||||
@ -313,7 +315,8 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
* @param position The position to be set to the current Playable object in case playback started or paused.
|
||||
* Will be ignored if given the value of {@link #INVALID_TIME}.
|
||||
*/
|
||||
final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia, int position) {
|
||||
protected final synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus,
|
||||
Playable newMedia, int position) {
|
||||
Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
|
||||
|
||||
this.oldPlayerStatus = playerStatus;
|
||||
@ -339,7 +342,7 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
/**
|
||||
* @see #setPlayerStatus(PlayerStatus, Playable, int)
|
||||
*/
|
||||
final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
|
||||
protected final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
|
||||
setPlayerStatus(newStatus, newMedia, INVALID_TIME);
|
||||
}
|
||||
|
||||
@ -350,8 +353,6 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
|
||||
void onMediaChanged(boolean reloadUI);
|
||||
|
||||
boolean onMediaPlayerInfo(int code, @StringRes int resourceId);
|
||||
|
||||
void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext);
|
||||
|
||||
void onPlaybackStart(@NonNull Playable playable, int position);
|
||||
@ -360,7 +361,12 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
|
||||
Playable getNextInQueue(Playable currentMedia);
|
||||
|
||||
@Nullable
|
||||
Playable findMedia(@NonNull String url);
|
||||
|
||||
void onPlaybackEnded(MediaType mediaType, boolean stopPlaying);
|
||||
|
||||
void ensureMediaInfoLoaded(@NonNull Playable media);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -371,7 +377,7 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
public PlayerStatus playerStatus;
|
||||
public Playable playable;
|
||||
|
||||
PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
|
||||
public PSMPInfo(PlayerStatus oldPlayerStatus, PlayerStatus playerStatus, Playable playable) {
|
||||
this.oldPlayerStatus = oldPlayerStatus;
|
||||
this.playerStatus = playerStatus;
|
||||
this.playable = playable;
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.danoeh.antennapod.core.util;
|
||||
package de.danoeh.antennapod.playback.base;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -1,4 +1,4 @@
|
||||
package de.danoeh.antennapod.core.util;
|
||||
package de.danoeh.antennapod.playback.base;
|
||||
|
||||
import org.junit.Test;
|
||||
|
3
playback/cast/README.md
Normal file
3
playback/cast/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# :playback:cast
|
||||
|
||||
This module provides Chromecast support for the Google Play version of the app.
|
17
playback/cast/build.gradle
Normal file
17
playback/cast/build.gradle
Normal 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'
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package de.danoeh.antennapod.activity;
|
||||
package de.danoeh.antennapod.playback.cast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
1
playback/cast/src/main/AndroidManifest.xml
Normal file
1
playback/cast/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="de.danoeh.antennapod.playback.cast" />
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,66 +1,77 @@
|
||||
package de.danoeh.antennapod.core.service.playback;
|
||||
package de.danoeh.antennapod.playback.cast;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaPlayer;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
import com.google.android.gms.cast.Cast;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
|
||||
import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
|
||||
import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
|
||||
|
||||
import de.danoeh.antennapod.core.cast.MediaInfoCreator;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import de.danoeh.antennapod.core.R;
|
||||
import de.danoeh.antennapod.core.cast.CastConsumer;
|
||||
import de.danoeh.antennapod.core.cast.CastManager;
|
||||
import de.danoeh.antennapod.core.cast.CastUtils;
|
||||
import de.danoeh.antennapod.core.cast.DefaultCastConsumer;
|
||||
import de.danoeh.antennapod.core.storage.DBReader;
|
||||
import de.danoeh.antennapod.model.playback.RemoteMedia;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.gms.cast.MediaError;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaLoadOptions;
|
||||
import com.google.android.gms.cast.MediaLoadRequestData;
|
||||
import com.google.android.gms.cast.MediaSeekOptions;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.cast.framework.CastState;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import de.danoeh.antennapod.event.PlayerErrorEvent;
|
||||
import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
|
||||
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||
import de.danoeh.antennapod.model.playback.MediaType;
|
||||
import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
|
||||
import de.danoeh.antennapod.model.playback.Playable;
|
||||
import de.danoeh.antennapod.model.playback.RemoteMedia;
|
||||
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import de.danoeh.antennapod.playback.base.RewindAfterPauseUtils;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
/**
|
||||
* Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
|
||||
*/
|
||||
public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
public class CastPsmp extends PlaybackServiceMediaPlayer {
|
||||
|
||||
public static final String TAG = "RemotePSMP";
|
||||
|
||||
public static final int CAST_ERROR = 3001;
|
||||
|
||||
public static final int CAST_ERROR_PRIORITY_HIGH = 3005;
|
||||
|
||||
private final CastManager castMgr;
|
||||
public static final String TAG = "CastPSMP";
|
||||
|
||||
private volatile Playable media;
|
||||
private volatile MediaType mediaType;
|
||||
private volatile MediaInfo remoteMedia;
|
||||
private volatile int remoteState;
|
||||
private final CastContext castContext;
|
||||
private final RemoteMediaClient remoteMediaClient;
|
||||
|
||||
private final AtomicBoolean isBuffering;
|
||||
|
||||
private final AtomicBoolean startWhenPrepared;
|
||||
|
||||
public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) {
|
||||
@Nullable
|
||||
public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
|
||||
@NonNull PSMPCallback callback) {
|
||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
|
||||
return null;
|
||||
}
|
||||
if (CastContext.getSharedInstance(context).getCastState() == CastState.CONNECTED) {
|
||||
return new CastPsmp(context, callback);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public CastPsmp(@NonNull Context context, @NonNull PSMPCallback callback) {
|
||||
super(context, callback);
|
||||
|
||||
castMgr = CastManager.getInstance();
|
||||
castContext = CastContext.getSharedInstance(context);
|
||||
remoteMediaClient = castContext.getSessionManager().getCurrentCastSession().getRemoteMediaClient();
|
||||
remoteMediaClient.registerCallback(remoteMediaClientCallback);
|
||||
media = null;
|
||||
mediaType = null;
|
||||
startWhenPrepared = new AtomicBoolean(false);
|
||||
@ -68,94 +79,48 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
remoteState = MediaStatus.PLAYER_STATE_UNKNOWN;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
try {
|
||||
if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) {
|
||||
private final RemoteMediaClient.Callback remoteMediaClientCallback = new RemoteMediaClient.Callback() {
|
||||
@Override
|
||||
public void onMetadataUpdated() {
|
||||
super.onMetadataUpdated();
|
||||
onRemoteMediaPlayerStatusUpdated();
|
||||
}
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to do initial check for loaded media", e);
|
||||
}
|
||||
|
||||
castMgr.addCastConsumer(castConsumer);
|
||||
}
|
||||
|
||||
private CastConsumer castConsumer = new DefaultCastConsumer() {
|
||||
@Override
|
||||
public void onRemoteMediaPlayerMetadataUpdated() {
|
||||
RemotePSMP.this.onRemoteMediaPlayerStatusUpdated();
|
||||
public void onPreloadStatusUpdated() {
|
||||
super.onPreloadStatusUpdated();
|
||||
onRemoteMediaPlayerStatusUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteMediaPlayerStatusUpdated() {
|
||||
RemotePSMP.this.onRemoteMediaPlayerStatusUpdated();
|
||||
public void onStatusUpdated() {
|
||||
super.onStatusUpdated();
|
||||
onRemoteMediaPlayerStatusUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaLoadResult(int statusCode) {
|
||||
if (playerStatus == PlayerStatus.PREPARING) {
|
||||
if (statusCode == CastStatusCodes.SUCCESS) {
|
||||
setPlayerStatus(PlayerStatus.PREPARED, media);
|
||||
if (media.getDuration() == 0) {
|
||||
Log.d(TAG, "Setting duration of media");
|
||||
try {
|
||||
media.setDuration((int) castMgr.getMediaDuration());
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to get remote media's duration");
|
||||
}
|
||||
}
|
||||
} else if (statusCode != CastStatusCodes.REPLACED){
|
||||
Log.d(TAG, "Remote media failed to load");
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, media);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "onMediaLoadResult called, but Player Status wasn't in preparing state, so we ignore the result");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationStatusChanged(String appStatus) {
|
||||
if (playerStatus != PlayerStatus.PLAYING) {
|
||||
Log.d(TAG, "onApplicationStatusChanged, but no media was playing");
|
||||
return;
|
||||
}
|
||||
boolean playbackEnded = false;
|
||||
try {
|
||||
int standbyState = castMgr.getApplicationStandbyState();
|
||||
Log.d(TAG, "standbyState: " + standbyState);
|
||||
playbackEnded = standbyState == Cast.STANDBY_STATE_YES;
|
||||
} catch (IllegalStateException e) {
|
||||
Log.d(TAG, "unable to get standbyState on onApplicationStatusChanged()");
|
||||
}
|
||||
if (playbackEnded) {
|
||||
// This is an unconventional thing to occur...
|
||||
Log.w(TAG, "Somehow, Chromecast went from playing directly to standby mode");
|
||||
endPlayback(false, false, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailed(int resourceId, int statusCode) {
|
||||
callback.onMediaPlayerInfo(CAST_ERROR, resourceId);
|
||||
public void onMediaError(@NonNull MediaError mediaError) {
|
||||
EventBus.getDefault().post(new PlayerErrorEvent(mediaError.getReason()));
|
||||
}
|
||||
};
|
||||
|
||||
private void setBuffering(boolean buffering) {
|
||||
if (buffering && isBuffering.compareAndSet(false, true)) {
|
||||
callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0);
|
||||
EventBus.getDefault().post(BufferUpdateEvent.started());
|
||||
} else if (!buffering && isBuffering.compareAndSet(true, false)) {
|
||||
callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0);
|
||||
EventBus.getDefault().post(BufferUpdateEvent.ended());
|
||||
}
|
||||
}
|
||||
|
||||
private Playable localVersion(MediaInfo info){
|
||||
if (info == null) {
|
||||
private Playable localVersion(MediaInfo info) {
|
||||
if (info == null || info.getMetadata() == null) {
|
||||
return null;
|
||||
}
|
||||
if (CastUtils.matches(info, media)) {
|
||||
return media;
|
||||
}
|
||||
return CastUtils.getPlayable(info, true);
|
||||
String streamUrl = info.getMetadata().getString(CastUtils.KEY_STREAM_URL);
|
||||
return streamUrl == null ? CastUtils.makeRemoteMedia(info) : callback.findMedia(streamUrl);
|
||||
}
|
||||
|
||||
private MediaInfo remoteVersion(Playable playable) {
|
||||
@ -166,7 +131,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
return remoteMedia;
|
||||
}
|
||||
if (playable instanceof FeedMedia) {
|
||||
return CastUtils.convertFromFeedMedia((FeedMedia) playable);
|
||||
return MediaInfoCreator.from((FeedMedia) playable);
|
||||
}
|
||||
if (playable instanceof RemoteMedia) {
|
||||
return MediaInfoCreator.from((RemoteMedia) playable);
|
||||
@ -175,7 +140,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
}
|
||||
|
||||
private void onRemoteMediaPlayerStatusUpdated() {
|
||||
MediaStatus status = castMgr.getMediaStatus();
|
||||
MediaStatus status = remoteMediaClient.getMediaStatus();
|
||||
if (status == null) {
|
||||
Log.d(TAG, "Received null MediaStatus");
|
||||
return;
|
||||
@ -206,8 +171,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
remoteState = state;
|
||||
}
|
||||
|
||||
if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING &&
|
||||
state != MediaStatus.PLAYER_STATE_IDLE) {
|
||||
if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING
|
||||
&& state != MediaStatus.PLAYER_STATE_IDLE) {
|
||||
callback.onPlaybackPause(null, INVALID_TIME);
|
||||
// We don't want setPlayerStatus to handle the onPlaybackPause callback
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
|
||||
@ -230,9 +195,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position);
|
||||
break;
|
||||
case MediaStatus.PLAYER_STATE_BUFFERING:
|
||||
setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING) ?
|
||||
PlayerStatus.PREPARING : PlayerStatus.SEEKING,
|
||||
currentMedia,
|
||||
setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING)
|
||||
? PlayerStatus.PREPARING : PlayerStatus.SEEKING, currentMedia,
|
||||
currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
|
||||
break;
|
||||
case MediaStatus.PLAYER_STATE_IDLE:
|
||||
@ -271,11 +235,13 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
endPlayback(true, false, true, true);
|
||||
return;
|
||||
case MediaStatus.IDLE_REASON_ERROR:
|
||||
Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode...");
|
||||
callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH,
|
||||
R.string.cast_failed_media_error_skipping);
|
||||
Log.w(TAG, "Got an error status from the Chromecast. "
|
||||
+ "Skipping, if possible, to the next episode...");
|
||||
EventBus.getDefault().post(new PlayerErrorEvent("Chromecast error code 1"));
|
||||
endPlayback(false, false, true, true);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case MediaStatus.PLAYER_STATE_UNKNOWN:
|
||||
@ -284,7 +250,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.wtf(TAG, "Remote media state undetermined!");
|
||||
Log.w(TAG, "Remote media state undetermined!");
|
||||
}
|
||||
if (mediaChanged) {
|
||||
callback.onMediaChanged(true);
|
||||
@ -295,25 +261,29 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
|
||||
public void playMediaObject(@NonNull final Playable playable, final boolean stream,
|
||||
final boolean startWhenPrepared, final boolean prepareImmediately) {
|
||||
Log.d(TAG, "playMediaObject() called");
|
||||
playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
|
||||
* Internal implementation of playMediaObject. This method has an additional parameter that
|
||||
* allows the caller to force a media player reset even if
|
||||
* the given playable parameter is the same object as the currently playing media.
|
||||
*
|
||||
* @see #playMediaObject(Playable, boolean, boolean, boolean)
|
||||
*/
|
||||
private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
|
||||
if (!CastUtils.isCastable(playable)) {
|
||||
private void playMediaObject(@NonNull final Playable playable, final boolean forceReset,
|
||||
final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
|
||||
if (!CastUtils.isCastable(playable, castContext.getSessionManager().getCurrentCastSession())) {
|
||||
Log.d(TAG, "media provided is not compatible with cast device");
|
||||
callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable);
|
||||
EventBus.getDefault().post(new PlayerErrorEvent("Media not compatible with cast device"));
|
||||
Playable nextPlayable = playable;
|
||||
do {
|
||||
nextPlayable = callback.getNextInQueue(nextPlayable);
|
||||
} while (nextPlayable != null && !CastUtils.isCastable(nextPlayable));
|
||||
} while (nextPlayable != null && !CastUtils.isCastable(nextPlayable,
|
||||
castContext.getSessionManager().getCurrentCastSession()));
|
||||
if (nextPlayable != null) {
|
||||
playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately);
|
||||
}
|
||||
@ -328,14 +298,8 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
return;
|
||||
} else {
|
||||
// set temporarily to pause in order to update list with current position
|
||||
boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
|
||||
int position = media.getPosition();
|
||||
try {
|
||||
isPlaying = castMgr.isRemoteMediaPlaying();
|
||||
position = (int) castMgr.getCurrentMediaPosition();
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to determine whether media was playing, falling back to stored player status", e);
|
||||
}
|
||||
boolean isPlaying = remoteMediaClient.isPlaying();
|
||||
int position = (int) remoteMediaClient.getApproximateStreamPosition();
|
||||
if (isPlaying) {
|
||||
callback.onPlaybackPause(media, position);
|
||||
}
|
||||
@ -343,7 +307,6 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
final Playable oldMedia = media;
|
||||
callback.onPostPlayback(oldMedia, false, false, true);
|
||||
}
|
||||
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, null);
|
||||
}
|
||||
}
|
||||
@ -353,9 +316,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
this.mediaType = media.getMediaType();
|
||||
this.startWhenPrepared.set(startWhenPrepared);
|
||||
setPlayerStatus(PlayerStatus.INITIALIZING, media);
|
||||
if (media instanceof FeedMedia && ((FeedMedia) media).getItem() == null) {
|
||||
((FeedMedia) media).setItem(DBReader.getFeedItem(((FeedMedia) media).getItemId()));
|
||||
}
|
||||
callback.ensureMediaInfoLoaded(media);
|
||||
callback.onMediaChanged(true);
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, media);
|
||||
if (prepareImmediately) {
|
||||
@ -365,29 +326,16 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
try {
|
||||
if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
|
||||
int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
|
||||
media.getPosition(),
|
||||
media.getLastPlayedTime());
|
||||
castMgr.play(newPosition);
|
||||
} else {
|
||||
castMgr.play();
|
||||
}
|
||||
} catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to resume remote playback", e);
|
||||
}
|
||||
seekTo(newPosition);
|
||||
remoteMediaClient.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(boolean abandonFocus, boolean reinit) {
|
||||
try {
|
||||
if (castMgr.isRemoteMediaPlaying()) {
|
||||
castMgr.pause();
|
||||
}
|
||||
} catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to pause", e);
|
||||
}
|
||||
remoteMediaClient.pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -395,18 +343,16 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
if (playerStatus == PlayerStatus.INITIALIZED) {
|
||||
Log.d(TAG, "Preparing media player");
|
||||
setPlayerStatus(PlayerStatus.PREPARING, media);
|
||||
try {
|
||||
int position = media.getPosition();
|
||||
if (position > 0) {
|
||||
position = RewindAfterPauseUtils.calculatePositionWithRewind(
|
||||
position,
|
||||
media.getLastPlayedTime());
|
||||
}
|
||||
castMgr.loadMedia(remoteMedia, startWhenPrepared.get(), position);
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Error loading media", e);
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, media);
|
||||
}
|
||||
remoteMediaClient.load(new MediaLoadRequestData.Builder()
|
||||
.setMediaInfo(remoteMedia)
|
||||
.setAutoplay(startWhenPrepared.get())
|
||||
.setCurrentTime(position).build());
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,19 +368,9 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
|
||||
@Override
|
||||
public void seekTo(int t) {
|
||||
//TODO check other seek implementations and see if there's no issue with sending too many seek commands to the remote media player
|
||||
try {
|
||||
if (castMgr.isRemoteMediaLoaded()) {
|
||||
setPlayerStatus(PlayerStatus.SEEKING, media);
|
||||
castMgr.seek(t);
|
||||
} else if (media != null && playerStatus == PlayerStatus.INITIALIZED){
|
||||
media.setPosition(t);
|
||||
startWhenPrepared.set(false);
|
||||
prepare();
|
||||
}
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to seek", e);
|
||||
}
|
||||
new Exception("Seeking to " + t).printStackTrace();
|
||||
remoteMediaClient.seek(new MediaSeekOptions.Builder()
|
||||
.setPosition(t).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -449,49 +385,19 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
|
||||
@Override
|
||||
public int getDuration() {
|
||||
int retVal = INVALID_TIME;
|
||||
boolean prepared;
|
||||
try {
|
||||
prepared = castMgr.isRemoteMediaLoaded();
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to check if remote media is loaded", e);
|
||||
prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
|
||||
}
|
||||
if (prepared) {
|
||||
try {
|
||||
retVal = (int) castMgr.getMediaDuration();
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to determine remote media's duration", e);
|
||||
}
|
||||
}
|
||||
if(retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
|
||||
int retVal = (int) remoteMediaClient.getStreamDuration();
|
||||
if (retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
|
||||
retVal = media.getDuration();
|
||||
}
|
||||
Log.d(TAG, "getDuration() -> " + retVal);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
int retVal = INVALID_TIME;
|
||||
boolean prepared;
|
||||
try {
|
||||
prepared = castMgr.isRemoteMediaLoaded();
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to check if remote media is loaded", e);
|
||||
prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
|
||||
}
|
||||
if (prepared) {
|
||||
try {
|
||||
retVal = (int) castMgr.getCurrentMediaPosition();
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to determine remote media's position", e);
|
||||
}
|
||||
}
|
||||
if(retVal <= 0 && media != null && media.getPosition() >= 0) {
|
||||
int retVal = (int) remoteMediaClient.getApproximateStreamPosition();
|
||||
if (retVal <= 0 && media != null && media.getPosition() >= 0) {
|
||||
retVal = media.getPosition();
|
||||
}
|
||||
Log.d(TAG, "getPosition() -> " + retVal);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@ -507,29 +413,21 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
|
||||
@Override
|
||||
public void setPlaybackParams(float speed, boolean skipSilence) {
|
||||
//Can be safely ignored as neither set speed not skipSilence is supported
|
||||
double playbackRate = (float) Math.max(MediaLoadOptions.PLAYBACK_RATE_MIN,
|
||||
Math.min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed));
|
||||
remoteMediaClient.setPlaybackRate(playbackRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
return 1;
|
||||
MediaStatus status = remoteMediaClient.getMediaStatus();
|
||||
return status != null ? (float) status.getPlaybackRate() : 1.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(float volumeLeft, float volumeRight) {
|
||||
Log.d(TAG, "Setting the Stream volume on Remote Media Player");
|
||||
double volume = (volumeLeft+volumeRight)/2;
|
||||
if (volume > 1.0) {
|
||||
volume = 1.0;
|
||||
}
|
||||
if (volume < 0.0) {
|
||||
volume = 0.0;
|
||||
}
|
||||
try {
|
||||
castMgr.setStreamVolume(volume);
|
||||
} catch (TransientNetworkDisconnectionException | NoConnectionException | CastException e) {
|
||||
Log.e(TAG, "Unable to set the volume", e);
|
||||
}
|
||||
remoteMediaClient.setStreamVolume(volumeLeft);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -554,7 +452,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
castMgr.removeCastConsumer(castConsumer);
|
||||
remoteMediaClient.unregisterCallback(remoteMediaClientCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -626,7 +524,7 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
boolean playNextEpisode = isPlaying && nextMedia != null;
|
||||
if (playNextEpisode) {
|
||||
Log.d(TAG, "Playback of next episode will start immediately.");
|
||||
} else if (nextMedia == null){
|
||||
} else if (nextMedia == null) {
|
||||
Log.d(TAG, "No more episodes available to play");
|
||||
} else {
|
||||
Log.d(TAG, "Loading next episode, but not playing automatically.");
|
||||
@ -636,45 +534,34 @@ public class RemotePSMP extends PlaybackServiceMediaPlayer {
|
||||
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode);
|
||||
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing
|
||||
media = null;
|
||||
playMediaObject(nextMedia, false, true /*TODO for now we always stream*/, playNextEpisode, playNextEpisode);
|
||||
playMediaObject(nextMedia, false, true, playNextEpisode, playNextEpisode);
|
||||
}
|
||||
}
|
||||
if (shouldContinue || toStoppedState) {
|
||||
boolean shouldPostProcess = true;
|
||||
if (nextMedia == null) {
|
||||
try {
|
||||
castMgr.stop();
|
||||
shouldPostProcess = false;
|
||||
} catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
|
||||
Log.e(TAG, "Unable to stop playback", e);
|
||||
callback.onPlaybackEnded(null, true);
|
||||
stop();
|
||||
}
|
||||
}
|
||||
if (shouldPostProcess) {
|
||||
remoteMediaClient.stop();
|
||||
// Otherwise we rely on the chromecast callback to tell us the playback has stopped.
|
||||
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, nextMedia != null);
|
||||
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, false);
|
||||
} else {
|
||||
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, true);
|
||||
}
|
||||
} else if (isPlaying) {
|
||||
callback.onPlaybackPause(currentMedia,
|
||||
currentMedia != null ? currentMedia.getPosition() : INVALID_TIME);
|
||||
}
|
||||
|
||||
FutureTask<?> future = new FutureTask<>(() -> {}, null);
|
||||
FutureTask<?> future = new FutureTask<>(() -> { }, null);
|
||||
future.run();
|
||||
return future;
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
if (playerStatus == PlayerStatus.INDETERMINATE) {
|
||||
setPlayerStatus(PlayerStatus.STOPPED, null);
|
||||
} else {
|
||||
Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldLockWifi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCasting() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
11
playback/cast/src/play/res/menu/cast_button.xml
Normal file
11
playback/cast/src/play/res/menu/cast_button.xml
Normal 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>
|
@ -10,6 +10,9 @@ include ':net:sync:model'
|
||||
include ':parser:feed'
|
||||
include ':parser:media'
|
||||
|
||||
include ':playback:base'
|
||||
include ':playback:cast'
|
||||
|
||||
include ':ui:app-start-intent'
|
||||
include ':ui:common'
|
||||
include ':ui:png-icons'
|
||||
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user