summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java135
-rw-r--r--app/src/main/AndroidManifest.xml11
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java647
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/CastEnabledActivity.java237
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java83
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java26
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java130
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java620
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java12
-rw-r--r--app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java10
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java18
-rw-r--r--app/src/main/res/layout/external_player_fragment.xml24
-rw-r--r--app/src/main/res/layout/mediaplayerinfo_activity.xml (renamed from app/src/main/res/layout/audioplayer_activity.xml)15
-rw-r--r--app/src/main/res/menu/cast_enabled.xml10
-rw-r--r--app/src/main/res/xml/preferences.xml9
-rw-r--r--build.gradle3
-rw-r--r--core/build.gradle5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java1766
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java317
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java347
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java106
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java22
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java17
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java8
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java894
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java551
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java863
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java29
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java587
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java12
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/Supplier.java5
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java2
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java39
-rw-r--r--core/src/main/res/drawable-hdpi/ic_audiotrack_light.pngbin0 -> 417 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_disabled_light.pngbin0 -> 770 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_disconnect_grey600_36dp.pngbin0 -> 1419 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_disconnect_white_36dp.pngbin0 -> 968 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_light.pngbin0 -> 975 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_off_light.pngbin0 -> 867 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_on_0_light.pngbin0 -> 961 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_on_1_light.pngbin0 -> 979 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_on_2_light.pngbin0 -> 976 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_cast_on_light.pngbin0 -> 982 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_close_light.pngbin0 -> 493 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_media_cast_disconnect.pngbin0 -> 2355 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_pause_light.pngbin0 -> 191 bytes
-rw-r--r--core/src/main/res/drawable-hdpi/ic_play_light.pngbin0 -> 562 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_audiotrack_light.pngbin0 -> 333 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_disabled_light.pngbin0 -> 536 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_disconnect_grey600_36dp.pngbin0 -> 1033 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_disconnect_white_36dp.pngbin0 -> 694 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_light.pngbin0 -> 693 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_off_light.pngbin0 -> 635 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_on_0_light.pngbin0 -> 684 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_on_1_light.pngbin0 -> 696 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_on_2_light.pngbin0 -> 690 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_cast_on_light.pngbin0 -> 694 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_close_light.pngbin0 -> 379 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_media_cast_disconnect.pngbin0 -> 1467 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_pause_light.pngbin0 -> 280 bytes
-rw-r--r--core/src/main/res/drawable-mdpi/ic_play_light.pngbin0 -> 447 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_audiotrack_light.pngbin0 -> 447 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_disabled_light.pngbin0 -> 976 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_disconnect_grey600_36dp.pngbin0 -> 1832 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_disconnect_white_36dp.pngbin0 -> 1348 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_light.pngbin0 -> 1328 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_off_light.pngbin0 -> 1161 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_on_0_light.pngbin0 -> 1286 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_on_1_light.pngbin0 -> 1308 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_on_2_light.pngbin0 -> 1309 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_cast_on_light.pngbin0 -> 1331 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_close_light.pngbin0 -> 526 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_media_cast_disconnect.pngbin0 -> 4496 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_pause_light.pngbin0 -> 221 bytes
-rw-r--r--core/src/main/res/drawable-xhdpi/ic_play_light.pngbin0 -> 678 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_audiotrack_light.pngbin0 -> 584 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_disabled_light.pngbin0 -> 1429 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_disconnect_grey600_36dp.pngbin0 -> 2068 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_disconnect_white_36dp.pngbin0 -> 1953 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_light.pngbin0 -> 1952 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_off_light.pngbin0 -> 1679 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_on_0_light.pngbin0 -> 1832 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_on_1_light.pngbin0 -> 1893 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_on_2_light.pngbin0 -> 1910 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_cast_on_light.pngbin0 -> 1944 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_close_light.pngbin0 -> 673 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_media_cast_disconnect.pngbin0 -> 7096 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_pause_light.pngbin0 -> 317 bytes
-rw-r--r--core/src/main/res/drawable-xxhdpi/ic_play_light.pngbin0 -> 955 bytes
-rw-r--r--core/src/main/res/drawable-xxxhdpi/ic_close_light.pngbin0 -> 805 bytes
-rw-r--r--core/src/main/res/drawable-xxxhdpi/ic_pause_light.pngbin0 -> 400 bytes
-rw-r--r--core/src/main/res/drawable-xxxhdpi/ic_play_light.pngbin0 -> 1190 bytes
-rw-r--r--core/src/main/res/values/attrs.xml1
-rw-r--r--core/src/main/res/values/strings.xml18
-rw-r--r--core/src/main/res/values/styles.xml4
105 files changed, 5937 insertions, 1700 deletions
diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
index 7dac89e9b..195dccdda 100644
--- a/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/service/playback/PlaybackServiceMediaPlayerTest.java
@@ -21,13 +21,14 @@ import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer;
+import de.danoeh.antennapod.core.service.playback.LocalPSMP;
import de.danoeh.antennapod.core.service.playback.PlayerStatus;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.test.antennapod.util.service.download.HTTPBin;
/**
- * Test class for PlaybackServiceMediaPlayer
+ * Test class for LocalPSMP
*/
public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
private static final String TAG = "PlaybackServiceMediaPlayerTest";
@@ -85,7 +86,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
assertEquals(0, httpServer.serveFile(dest));
}
- private void checkPSMPInfo(PlaybackServiceMediaPlayer.PSMPInfo info) {
+ private void checkPSMPInfo(LocalPSMP.PSMPInfo info) {
try {
switch (info.playerStatus) {
case PLAYING:
@@ -111,7 +112,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
public void testInit() {
final Context c = getInstrumentation().getTargetContext();
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, defaultCallback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, defaultCallback);
psmp.shutdown();
}
@@ -139,7 +140,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(2);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
@@ -175,7 +176,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -185,7 +186,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -195,12 +196,12 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
psmp.playMediaObject(p, true, false, false);
boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -218,7 +219,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(2);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
@@ -254,7 +255,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -264,7 +265,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -274,11 +275,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
psmp.playMediaObject(p, true, true, false);
@@ -297,7 +298,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(4);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
@@ -336,7 +337,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -346,7 +347,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -356,11 +357,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
psmp.playMediaObject(p, true, false, true);
boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -377,7 +378,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(5);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
@@ -419,7 +420,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -429,7 +430,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -439,12 +440,12 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, null);
psmp.playMediaObject(p, true, true, true);
boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -460,7 +461,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(2);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
@@ -496,7 +497,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -506,7 +507,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -516,12 +517,12 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
psmp.playMediaObject(p, false, false, false);
boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -538,7 +539,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(2);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
@@ -574,7 +575,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -584,7 +585,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -594,11 +595,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
psmp.playMediaObject(p, false, true, false);
boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -615,7 +616,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(4);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
@@ -654,7 +655,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -664,7 +665,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -674,11 +675,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
psmp.playMediaObject(p, false, false, true);
boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -694,7 +695,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(5);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
try {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR)
@@ -737,7 +738,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -747,7 +748,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -757,11 +758,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
psmp.playMediaObject(p, false, true, true);
boolean res = countDownLatch.await(LATCH_TIMEOUT_SECONDS, TimeUnit.SECONDS);
@@ -775,7 +776,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
private final PlaybackServiceMediaPlayer.PSMPCallback defaultCallback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
}
@@ -795,7 +796,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -805,7 +806,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) { return false; }
+ public boolean onMediaPlayerInfo(int code, int resourceId) { return false; }
@Override
public boolean onMediaPlayerError(Object inObj, int what, int extra) {
@@ -813,7 +814,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
@@ -825,7 +826,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null)
@@ -873,7 +874,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -883,7 +884,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -895,11 +896,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
if (initialState == PlayerStatus.PLAYING) {
psmp.playMediaObject(p, stream, true, true);
@@ -956,7 +957,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null)
@@ -988,7 +989,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -998,7 +999,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -1011,11 +1012,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
if (initialState == PlayerStatus.PREPARED || initialState == PlayerStatus.PLAYING || initialState == PlayerStatus.PAUSED) {
boolean startWhenPrepared = (initialState != PlayerStatus.PREPARED);
psmp.playMediaObject(writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL), false, startWhenPrepared, true);
@@ -1049,7 +1050,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null)
@@ -1080,7 +1081,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -1090,7 +1091,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -1102,11 +1103,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
if (initialState == PlayerStatus.INITIALIZED
|| initialState == PlayerStatus.PLAYING
@@ -1154,7 +1155,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
final CountDownLatch countDownLatch = new CountDownLatch(latchCount);
PlaybackServiceMediaPlayer.PSMPCallback callback = new PlaybackServiceMediaPlayer.PSMPCallback() {
@Override
- public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) {
+ public void statusChanged(LocalPSMP.PSMPInfo newInfo) {
checkPSMPInfo(newInfo);
if (newInfo.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null)
@@ -1184,7 +1185,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
+ public void reloadUI() {
}
@@ -1194,7 +1195,7 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, int resourceId) {
return false;
}
@@ -1206,11 +1207,11 @@ public class PlaybackServiceMediaPlayerTest extends InstrumentationTestCase {
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
+ public boolean endPlayback(Playable p, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
return false;
}
};
- PlaybackServiceMediaPlayer psmp = new PlaybackServiceMediaPlayer(c, callback);
+ PlaybackServiceMediaPlayer psmp = new LocalPSMP(c, callback);
Playable p = writeTestPlayable(PLAYABLE_FILE_URL, PLAYABLE_LOCAL_URL);
boolean prepareImmediately = initialState != PlayerStatus.INITIALIZED;
boolean startImmediately = initialState != PlayerStatus.PREPARED;
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 005ba604f..65c9f8ec4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -41,6 +41,10 @@
android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI3a05VToCTlqBymJrbFGaKQMvF-bBAuLsOdavBA"/>
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+
<activity
android:name=".activity.MainActivity"
android:configChanges="keyboardHidden|orientation"
@@ -67,6 +71,13 @@
<data android:mimeType="audio/*"/>
</intent-filter>
</activity>
+ <activity
+ android:name=".activity.CastplayerActivity"
+ android:launchMode="singleTop">
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value="de.danoeh.antennapod.activity.MainActivity"/>
+ </activity>
<activity
android:name=".activity.DownloadAuthenticationActivity"
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java
index 292f3d9b6..ca214de9e 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/AudioplayerActivity.java
@@ -1,166 +1,27 @@
package de.danoeh.antennapod.activity;
-import android.app.AlertDialog;
-import android.content.DialogInterface;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.os.Build;
-import android.support.annotation.Nullable;
-import android.support.design.widget.AppBarLayout;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentStatePagerAdapter;
-import android.support.v4.view.ViewPager;
-import android.support.v4.widget.DrawerLayout;
-import android.support.v7.app.ActionBarDrawerToggle;
-import android.support.v7.widget.Toolbar;
+import android.support.v4.view.ViewCompat;
import android.text.TextUtils;
import android.util.Log;
-import android.util.TypedValue;
-import android.view.ContextMenu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ListView;
-import com.viewpagerindicator.CirclePageIndicator;
-
-import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.adapter.ChaptersListAdapter;
-import de.danoeh.antennapod.adapter.NavListAdapter;
-import de.danoeh.antennapod.core.asynctask.FeedRemover;
-import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
-import de.danoeh.antennapod.core.feed.EventDistributor;
-import de.danoeh.antennapod.core.feed.Feed;
-import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
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.playback.ExternalMedia;
-import de.danoeh.antennapod.core.util.playback.Playable;
-import de.danoeh.antennapod.core.util.playback.PlaybackController;
-import de.danoeh.antennapod.fragment.AddFeedFragment;
-import de.danoeh.antennapod.fragment.ChaptersFragment;
-import de.danoeh.antennapod.fragment.CoverFragment;
-import de.danoeh.antennapod.fragment.DownloadsFragment;
-import de.danoeh.antennapod.fragment.EpisodesFragment;
-import de.danoeh.antennapod.fragment.ItemDescriptionFragment;
-import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
-import de.danoeh.antennapod.fragment.QueueFragment;
-import de.danoeh.antennapod.fragment.SubscriptionFragment;
-import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
-import de.danoeh.antennapod.preferences.PreferenceController;
-import rx.Observable;
-import rx.Subscription;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.schedulers.Schedulers;
+import de.danoeh.antennapod.dialog.VariableSpeedDialog;
/**
* Activity for playing audio files.
*/
-public class AudioplayerActivity extends MediaplayerActivity implements NavDrawerActivity {
-
- private static final int POS_COVER = 0;
- private static final int POS_DESCR = 1;
- private static final int POS_CHAPTERS = 2;
- private static final int NUM_CONTENT_FRAGMENTS = 3;
-
- final String TAG = "AudioplayerActivity";
- private static final String PREFS = "AudioPlayerActivityPreferences";
- private static final String PREF_KEY_SELECTED_FRAGMENT_POSITION = "selectedFragmentPosition";
-
- public static final String[] NAV_DRAWER_TAGS = {
- QueueFragment.TAG,
- EpisodesFragment.TAG,
- SubscriptionFragment.TAG,
- DownloadsFragment.TAG,
- PlaybackHistoryFragment.TAG,
- AddFeedFragment.TAG,
- NavListAdapter.SUBSCRIPTION_LIST_TAG
- };
+public class AudioplayerActivity extends MediaplayerInfoActivity {
+ public static final String TAG = "AudioPlayerActivity";
private AtomicBoolean isSetup = new AtomicBoolean(false);
- private DrawerLayout drawerLayout;
- private NavListAdapter navAdapter;
- private ListView navList;
- private View navDrawer;
- private ActionBarDrawerToggle drawerToggle;
- private int mPosition = -1;
-
- private Playable media;
- private ViewPager pager;
- private AudioplayerPagerAdapter pagerAdapter;
-
- private Subscription subscription;
-
- @Override
- protected void onStop() {
- super.onStop();
- Log.d(TAG, "onStop()");
- if(pagerAdapter != null) {
- pagerAdapter.setController(null);
- }
- if(subscription != null) {
- subscription.unsubscribe();
- }
- EventDistributor.getInstance().unregister(contentUpdate);
- saveCurrentFragment();
- }
-
- @Override
- public void onDestroy() {
- Log.d(TAG, "onDestroy()");
- super.onDestroy();
- // don't risk creating memory leaks
- drawerLayout = null;
- navAdapter = null;
- navList = null;
- navDrawer = null;
- drawerToggle = null;
- pager = null;
- pagerAdapter = null;
- }
-
- @Override
- protected void chooseTheme() {
- setTheme(UserPreferences.getNoTitleTheme());
- }
-
- private void saveCurrentFragment() {
- if(pager == null) {
- return;
- }
- Log.d(TAG, "Saving preferences");
- SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
- prefs.edit()
- .putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, pager.getCurrentItem())
- .commit();
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- if(drawerToggle != null) {
- drawerToggle.onConfigurationChanged(newConfig);
- }
- }
-
- private void loadLastFragment() {
- Log.d(TAG, "Restoring instance state");
- SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
- int lastPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1);
- pager.setCurrentItem(lastPosition);
- }
-
@Override
protected void onResume() {
super.onResume();
@@ -177,456 +38,116 @@ public class AudioplayerActivity extends MediaplayerActivity implements NavDrawe
launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
true);
startService(launchIntent);
+ } else if (PlaybackService.isCasting()) {
+ Intent intent = PlaybackService.getPlayerActivityIntent(this);
+ if (!intent.getComponent().getClassName().equals(AudioplayerActivity.class.getName())) {
+ saveCurrentFragment();
+ finish();
+ startActivity(intent);
+ }
}
- if(pagerAdapter != null && controller != null && controller.getMedia() != media) {
- media = controller.getMedia();
- pagerAdapter.onMediaChanged(media);
- pagerAdapter.setController(controller);
- }
-
- EventDistributor.getInstance().register(contentUpdate);
- loadData();
}
@Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
- setIntent(intent);
- }
-
- @Override
- protected void onAwaitingVideoSurface() {
- Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player");
- startActivity(new Intent(this, VideoplayerActivity.class));
- }
+ protected void onReloadNotification(int notificationCode) {
+ if (notificationCode == PlaybackService.EXTRA_CODE_CAST) {
+ Log.d(TAG, "ReloadNotification received, switching to Castplayer now");
+ saveCurrentFragment();
+ finish();
+ startActivity(new Intent(this, CastplayerActivity.class));
- @Override
- protected void postStatusMsg(int resId) {
- if (resId == R.string.player_preparing_msg
- || resId == R.string.player_seeking_msg
- || resId == R.string.player_buffering_msg) {
- // TODO Show progress bar here
+ } else {
+ super.onReloadNotification(notificationCode);
}
}
@Override
- protected void clearStatusMsg() {
- // TODO Hide progress bar here
- }
-
-
- @Override
- protected void setupGUI() {
- if(isSetup.getAndSet(true)) {
+ protected void updatePlaybackSpeedButton() {
+ if(butPlaybackSpeed == null) {
return;
}
- super.setupGUI();
- Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- getSupportActionBar().setTitle("");
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- findViewById(R.id.shadow).setVisibility(View.GONE);
- AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appBar);
- float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
- appBarLayout.setElevation(px);
+ if (controller == null) {
+ butPlaybackSpeed.setVisibility(View.GONE);
+ return;
}
- drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
- navList = (ListView) findViewById(R.id.nav_list);
- navDrawer = findViewById(R.id.nav_layout);
-
- drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close);
- drawerToggle.setDrawerIndicatorEnabled(false);
- drawerLayout.setDrawerListener(drawerToggle);
-
- navAdapter = new NavListAdapter(itemAccess, this);
- navList.setAdapter(navAdapter);
- navList.setOnItemClickListener((parent, view, position, id) -> {
- int viewType = parent.getAdapter().getItemViewType(position);
- if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) {
- Intent intent = new Intent(AudioplayerActivity.this, MainActivity.class);
- intent.putExtra(MainActivity.EXTRA_NAV_TYPE, viewType);
- intent.putExtra(MainActivity.EXTRA_NAV_INDEX, position);
- startActivity(intent);
- }
- drawerLayout.closeDrawer(navDrawer);
- });
- navList.setOnItemLongClickListener((parent, view, position, id) -> {
- if (position < navAdapter.getTags().size()) {
- showDrawerPreferencesDialog();
- return true;
- } else {
- mPosition = position;
- return false;
- }
- });
- registerForContextMenu(navList);
- drawerToggle.syncState();
-
- findViewById(R.id.nav_settings).setOnClickListener(v -> {
- drawerLayout.closeDrawer(navDrawer);
- startActivity(new Intent(AudioplayerActivity.this, PreferenceController.getPreferenceActivity()));
- });
-
- pager = (ViewPager) findViewById(R.id.pager);
- pagerAdapter = new AudioplayerPagerAdapter(getSupportFragmentManager(), media);
- pagerAdapter.setController(controller);
- pager.setAdapter(pagerAdapter);
- CirclePageIndicator pageIndicator = (CirclePageIndicator) findViewById(R.id.page_indicator);
- pageIndicator.setViewPager(pager);
- loadLastFragment();
- pager.onSaveInstanceState();
- }
-
- @Override
- protected void onPositionObserverUpdate() {
- super.onPositionObserverUpdate();
- notifyMediaPositionChanged();
+ updatePlaybackSpeedButtonText();
+ ViewCompat.setAlpha(butPlaybackSpeed, controller.canSetPlaybackSpeed() ? 1.0f : 0.5f);
+ butPlaybackSpeed.setVisibility(View.VISIBLE);
}
@Override
- protected boolean loadMediaInfo() {
- if (!super.loadMediaInfo()) {
- return false;
- }
- if(controller.getMedia() != media) {
- media = controller.getMedia();
- pagerAdapter.onMediaChanged(media);
+ protected void updatePlaybackSpeedButtonText() {
+ if(butPlaybackSpeed == null) {
+ return;
}
- return true;
- }
-
- public void notifyMediaPositionChanged() {
- if(pagerAdapter == null) {
+ if (controller == null) {
+ butPlaybackSpeed.setVisibility(View.GONE);
return;
}
- ChaptersFragment chaptersFragment = pagerAdapter.getChaptersFragment();
- if(chaptersFragment != null) {
- ChaptersListAdapter adapter = (ChaptersListAdapter) chaptersFragment.getListAdapter();
- if (adapter != null) {
- adapter.notifyDataSetChanged();
+ float speed = 1.0f;
+ if(controller.canSetPlaybackSpeed()) {
+ try {
+ // we can only retrieve the playback speed from the controller/playback service
+ // once mediaplayer has been initialized
+ speed = Float.parseFloat(UserPreferences.getPlaybackSpeed());
+ } catch (NumberFormatException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ UserPreferences.setPlaybackSpeed(String.valueOf(speed));
}
}
+ String speedStr = String.format("%.2fx", speed);
+ butPlaybackSpeed.setText(speedStr);
}
@Override
- protected void onReloadNotification(int notificationCode) {
- if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) {
- Log.d(TAG, "ReloadNotification received, switching to Videoplayer now");
- finish();
- startActivity(new Intent(this, VideoplayerActivity.class));
-
- }
- }
-
- @Override
- protected void onBufferStart() {
- postStatusMsg(R.string.player_buffering_msg);
- }
-
- @Override
- protected void onBufferEnd() {
- clearStatusMsg();
- }
-
- public PlaybackController getPlaybackController() {
- return controller;
- }
-
- @Override
- public boolean isDrawerOpen() {
- return drawerLayout != null && navDrawer != null && drawerLayout.isDrawerOpen(navDrawer);
- }
-
- @Override
- protected int getContentViewResourceId() {
- return R.layout.audioplayer_activity;
- }
-
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- return drawerToggle != null && drawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
- }
-
- @Override
- public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
- super.onCreateContextMenu(menu, v, menuInfo);
- if(v.getId() != R.id.nav_list) {
- return;
- }
- AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
- int position = adapterInfo.position;
- if(position < navAdapter.getSubscriptionOffset()) {
+ protected void setupGUI() {
+ if(isSetup.getAndSet(true)) {
return;
}
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.nav_feed_context, menu);
- Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset());
- menu.setHeaderTitle(feed.getTitle());
- // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones!
- }
-
- @Override
- public boolean onContextItemSelected(MenuItem item) {
- final int position = mPosition;
- mPosition = -1; // reset
- if(position < 0) {
- return false;
- }
- Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset());
- switch(item.getItemId()) {
- case R.id.mark_all_seen_item:
- DBWriter.markFeedSeen(feed.getId());
- return true;
- case R.id.mark_all_read_item:
- DBWriter.markFeedRead(feed.getId());
- return true;
- case R.id.remove_item:
- final FeedRemover remover = new FeedRemover(this, feed) {
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
+ super.setupGUI();
+ if(butCastDisconnect != null) {
+ butCastDisconnect.setVisibility(View.GONE);
+ }
+ if(butPlaybackSpeed != null) {
+ butPlaybackSpeed.setOnClickListener(v -> {
+ if (controller == null) {
+ return;
+ }
+ if (controller.canSetPlaybackSpeed()) {
+ String[] availableSpeeds = UserPreferences.getPlaybackSpeedArray();
+ String currentSpeed = UserPreferences.getPlaybackSpeed();
+
+ // Provide initial value in case the speed list has changed
+ // out from under us
+ // and our current speed isn't in the new list
+ String newSpeed;
+ if (availableSpeeds.length > 0) {
+ newSpeed = availableSpeeds[0];
+ } else {
+ newSpeed = "1.00";
}
- };
- ConfirmationDialog conDialog = new ConfirmationDialog(this,
- R.string.remove_feed_label,
- R.string.feed_delete_confirmation_msg) {
- @Override
- public void onConfirmButtonPressed(
- DialogInterface dialog) {
- dialog.dismiss();
- if (controller != null) {
- Playable playable = controller.getMedia();
- if (playable != null && playable instanceof FeedMedia) {
- FeedMedia media = (FeedMedia) playable;
- if (media.getItem().getFeed().getId() == feed.getId()) {
- Log.d(TAG, "Currently playing episode is about to be deleted, skipping");
- remover.skipOnCompletion = true;
- if(controller.getStatus() == PlayerStatus.PLAYING) {
- sendBroadcast(new Intent(
- PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE));
- }
- }
+
+ for (int i = 0; i < availableSpeeds.length; i++) {
+ if (availableSpeeds[i].equals(currentSpeed)) {
+ if (i == availableSpeeds.length - 1) {
+ newSpeed = availableSpeeds[0];
+ } else {
+ newSpeed = availableSpeeds[i + 1];
}
+ break;
}
- remover.executeAsync();
}
- };
- conDialog.createNewDialog().show();
+ UserPreferences.setPlaybackSpeed(newSpeed);
+ controller.setPlaybackSpeed(Float.parseFloat(newSpeed));
+ } else {
+ VariableSpeedDialog.showGetPluginDialog(this);
+ }
+ });
+ butPlaybackSpeed.setOnLongClickListener(v -> {
+ VariableSpeedDialog.showDialog(this);
return true;
- default:
- return super.onContextItemSelected(item);
+ });
+ butPlaybackSpeed.setVisibility(View.VISIBLE);
}
}
-
- @Override
- public void onBackPressed() {
- if(isDrawerOpen()) {
- drawerLayout.closeDrawer(navDrawer);
- } else if (pager == null || pager.getCurrentItem() == 0) {
- // If the user is currently looking at the first step, allow the system to handle the
- // Back button. This calls finish() on this activity and pops the back stack.
- super.onBackPressed();
- } else {
- // Otherwise, select the previous step.
- pager.setCurrentItem(pager.getCurrentItem() - 1);
- }
- }
-
- public void showDrawerPreferencesDialog() {
- final List<String> hiddenDrawerItems = UserPreferences.getHiddenDrawerItems();
- String[] navLabels = new String[NAV_DRAWER_TAGS.length];
- final boolean[] checked = new boolean[NAV_DRAWER_TAGS.length];
- for (int i = 0; i < NAV_DRAWER_TAGS.length; i++) {
- String tag = NAV_DRAWER_TAGS[i];
- navLabels[i] = navAdapter.getLabel(tag);
- if (!hiddenDrawerItems.contains(tag)) {
- checked[i] = true;
- }
- }
-
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setTitle(R.string.drawer_preferences);
- builder.setMultiChoiceItems(navLabels, checked, (dialog, which, isChecked) -> {
- if (isChecked) {
- hiddenDrawerItems.remove(NAV_DRAWER_TAGS[which]);
- } else {
- hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]);
- }
- });
- builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
- UserPreferences.setHiddenDrawerItems(hiddenDrawerItems);
- });
- builder.setNegativeButton(R.string.cancel_label, null);
- builder.create().show();
- }
-
- private DBReader.NavDrawerData navDrawerData;
-
- private void loadData() {
- subscription = Observable.fromCallable(DBReader::getNavDrawerData)
- .subscribeOn(Schedulers.newThread())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(result -> {
- navDrawerData = result;
- if (navAdapter != null) {
- navAdapter.notifyDataSetChanged();
- }
- }, error -> {
- Log.e(TAG, Log.getStackTraceString(error));
- });
- }
-
-
-
- private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
-
- @Override
- public void update(EventDistributor eventDistributor, Integer arg) {
- if ((EventDistributor.FEED_LIST_UPDATE & arg) != 0) {
- Log.d(TAG, "Received contentUpdate Intent.");
- loadData();
- }
- }
- };
-
- private final NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() {
- @Override
- public int getCount() {
- if (navDrawerData != null) {
- return navDrawerData.feeds.size();
- } else {
- return 0;
- }
- }
-
- @Override
- public Feed getItem(int position) {
- if (navDrawerData != null && 0 <= position && position < navDrawerData.feeds.size()) {
- return navDrawerData.feeds.get(position);
- } else {
- return null;
- }
- }
-
- @Override
- public int getSelectedItemIndex() {
- return -1;
- }
-
- @Override
- public int getQueueSize() {
- return (navDrawerData != null) ? navDrawerData.queueSize : 0;
- }
-
- @Override
- public int getNumberOfNewItems() {
- return (navDrawerData != null) ? navDrawerData.numNewItems : 0;
- }
-
- @Override
- public int getNumberOfDownloadedItems() {
- return (navDrawerData != null) ? navDrawerData.numDownloadedItems : 0;
- }
-
- @Override
- public int getReclaimableItems() {
- return (navDrawerData != null) ? navDrawerData.reclaimableSpace : 0;
- }
-
- @Override
- public int getFeedCounter(long feedId) {
- return navDrawerData != null ? navDrawerData.feedCounters.get(feedId) : 0;
- }
-
- @Override
- public int getFeedCounterSum() {
- if(navDrawerData == null) {
- return 0;
- }
- int sum = 0;
- for(int counter : navDrawerData.feedCounters.values()) {
- sum += counter;
- }
- return sum;
- }
- };
-
- public interface AudioplayerContentFragment {
- void onMediaChanged(Playable media);
- }
-
- private static class AudioplayerPagerAdapter extends FragmentStatePagerAdapter {
-
- private static final String TAG = "AudioplayerPagerAdapter";
-
- private Playable media;
- private PlaybackController controller;
-
- public AudioplayerPagerAdapter(FragmentManager fm, Playable media) {
- super(fm);
- this.media = media;
- }
-
- private CoverFragment coverFragment;
- private ItemDescriptionFragment itemDescriptionFragment;
- private ChaptersFragment chaptersFragment;
-
- public void onMediaChanged(Playable media) {
- this.media = media;
- if(coverFragment != null) {
- coverFragment.onMediaChanged(media);
- }
- if(itemDescriptionFragment != null) {
- itemDescriptionFragment.onMediaChanged(media);
- }
- if(chaptersFragment != null) {
- chaptersFragment.onMediaChanged(media);
- }
- }
-
- public void setController(PlaybackController controller) {
- this.controller = controller;
- if(chaptersFragment != null) {
- chaptersFragment.setController(controller);
- }
- }
-
- @Nullable
- public ChaptersFragment getChaptersFragment() {
- return chaptersFragment;
- }
-
- @Override
- public Fragment getItem(int position) {
- Log.d(TAG, "getItem(" + position + ")");
- switch (position) {
- case POS_COVER:
- if(coverFragment == null) {
- coverFragment = CoverFragment.newInstance(media);
- }
- return coverFragment;
- case POS_DESCR:
- if(itemDescriptionFragment == null) {
- itemDescriptionFragment = ItemDescriptionFragment.newInstance(media, true, true);
- }
- return itemDescriptionFragment;
- case POS_CHAPTERS:
- if(chaptersFragment == null) {
- chaptersFragment = ChaptersFragment.newInstance(media);
- chaptersFragment.setController(controller);
- }
- return chaptersFragment;
- default:
- return null;
- }
- }
-
- @Override
- public int getCount() {
- return NUM_CONTENT_FRAGMENTS;
- }
- }
-
}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/CastEnabledActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
new file mode 100644
index 000000000..b8856c295
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/CastEnabledActivity.java
@@ -0,0 +1,237 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.SharedPreferences;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.CallSuper;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.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.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;
+
+/**
+ * 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";
+
+ protected CastManager castManager;
+ protected SwitchableMediaRouteActionProvider mediaRouteActionProvider;
+ private final CastButtonVisibilityManager castButtonVisibilityManager = new CastButtonVisibilityManager();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).
+ registerOnSharedPreferenceChangeListener(this);
+
+ castManager = CastManager.getInstance();
+ castManager.addCastConsumer(castConsumer);
+ castButtonVisibilityManager.setPrefEnabled(UserPreferences.isCastEnabled());
+ onCastConnectionChanged(castManager.isConnected());
+ }
+
+ @Override
+ protected void onDestroy() {
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
+ .unregisterOnSharedPreferenceChangeListener(this);
+ castManager.removeCastConsumer(castConsumer);
+ super.onDestroy();
+ }
+
+ @Override
+ @CallSuper
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.cast_enabled, menu);
+ castButtonVisibilityManager.setMenu(menu);
+ return true;
+ }
+
+ @Override
+ @CallSuper
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ mediaRouteActionProvider = castManager
+ .addMediaRouterButton(menu.findItem(R.id.media_route_menu_item));
+ mediaRouteActionProvider.setEnabled(castButtonVisibilityManager.shouldEnable());
+ return true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ castButtonVisibilityManager.setResumed(true);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ castButtonVisibilityManager.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);
+ castButtonVisibilityManager.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();
+ }
+ }
+ }
+
+ CastConsumer castConsumer = new DefaultCastConsumer() {
+ @Override
+ public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
+ onCastConnectionChanged(true);
+ }
+
+ @Override
+ public void onDisconnected() {
+ onCastConnectionChanged(false);
+ }
+ };
+
+ private void onCastConnectionChanged(boolean connected) {
+ if (connected) {
+ castButtonVisibilityManager.onConnected();
+ setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE);
+ } else {
+ castButtonVisibilityManager.onDisconnected();
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+ }
+ }
+
+ /**
+ * Should be called by any activity or fragment for which the cast button should be shown.
+ *
+ * @param showAsAction refer to {@link MenuItem#setShowAsAction(int)}
+ */
+ public final void requestCastButton(int showAsAction) {
+ castButtonVisibilityManager.requestCastButton(showAsAction);
+ }
+
+ private class CastButtonVisibilityManager {
+ 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 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;
+ }
+ MenuItemCompat.setShowAsAction(item, connected? MenuItem.SHOW_AS_ACTION_ALWAYS : showAsAction);
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java
new file mode 100644
index 000000000..1ca4d095f
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/CastplayerActivity.java
@@ -0,0 +1,83 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import de.danoeh.antennapod.core.service.playback.PlaybackService;
+
+/**
+ * Activity for controlling the remote playback on a Cast device.
+ */
+public class CastplayerActivity extends MediaplayerInfoActivity {
+ public static final String TAG = "CastPlayerActivity";
+
+ private AtomicBoolean isSetup = new AtomicBoolean(false);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!PlaybackService.isCasting()) {
+ Intent intent = PlaybackService.getPlayerActivityIntent(this);
+ if (!intent.getComponent().getClassName().equals(CastplayerActivity.class.getName())) {
+ finish();
+ startActivity(intent);
+ }
+ }
+ }
+
+ @Override
+ protected void onReloadNotification(int notificationCode) {
+ if (notificationCode == PlaybackService.EXTRA_CODE_AUDIO) {
+ Log.d(TAG, "ReloadNotification received, switching to Audioplayer now");
+ saveCurrentFragment();
+ finish();
+ startActivity(new Intent(this, AudioplayerActivity.class));
+ } else {
+ super.onReloadNotification(notificationCode);
+ }
+ }
+
+ @Override
+ protected void setupGUI() {
+ if(isSetup.getAndSet(true)) {
+ return;
+ }
+ super.setupGUI();
+ if (butPlaybackSpeed != null) {
+ butPlaybackSpeed.setVisibility(View.GONE);
+ }
+ if (butCastDisconnect != null) {
+ butCastDisconnect.setOnClickListener(v -> castManager.disconnect());
+ butCastDisconnect.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ if (!PlaybackService.isCasting()) {
+ Intent intent = PlaybackService.getPlayerActivityIntent(this);
+ if (!intent.getComponent().getClassName().equals(CastplayerActivity.class.getName())) {
+ saveCurrentFragment();
+ finish();
+ startActivity(intent);
+ }
+ }
+ super.onResume();
+ }
+
+ @Override
+ protected void onBufferStart() {
+ //sbPosition.setIndeterminate(true);
+ sbPosition.setEnabled(false);
+ }
+
+ @Override
+ protected void onBufferEnd() {
+ //sbPosition.setIndeterminate(false);
+ sbPosition.setEnabled(true);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
index 8599eb4f4..b7c7d86c7 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -7,7 +7,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.database.DataSetObserver;
-import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -17,11 +16,11 @@ import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AlertDialog;
-import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextMenu;
+import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
@@ -70,7 +69,7 @@ import rx.schedulers.Schedulers;
/**
* The activity that is shown when the user launches the app.
*/
-public class MainActivity extends AppCompatActivity implements NavDrawerActivity {
+public class MainActivity extends CastEnabledActivity implements NavDrawerActivity {
private static final String TAG = "MainActivity";
@@ -123,7 +122,6 @@ public class MainActivity extends AppCompatActivity implements NavDrawerActivity
super.onCreate(savedInstanceState);
StorageUtils.checkStorageAvailability(this);
setContentView(R.layout.main);
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
@@ -507,6 +505,26 @@ public class MainActivity extends AppCompatActivity implements NavDrawerActivity
}
@Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ boolean retVal = super.onCreateOptionsMenu(menu);
+ switch (getLastNavFragment()) {
+ case QueueFragment.TAG:
+ case EpisodesFragment.TAG:
+ requestCastButton(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return retVal;
+ case DownloadsFragment.TAG:
+ case PlaybackHistoryFragment.TAG:
+ case AddFeedFragment.TAG:
+ case SubscriptionFragment.TAG:
+ return retVal;
+ default:
+ requestCastButton(MenuItem.SHOW_AS_ACTION_NEVER);
+ return retVal;
+ }
+
+ }
+
+ @Override
public boolean onOptionsItemSelected(MenuItem item) {
if (drawerToggle.onOptionsItemSelected(item)) {
return true;
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
index 4911f61bc..8ee91c7a3 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerActivity.java
@@ -6,13 +6,10 @@ import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PixelFormat;
-import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import android.support.v4.view.ViewCompat;
import android.support.v7.app.AlertDialog;
-import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
@@ -58,7 +55,7 @@ import rx.schedulers.Schedulers;
* Provides general features which are both needed for playing audio and video
* files.
*/
-public abstract class MediaplayerActivity extends AppCompatActivity implements OnSeekBarChangeListener {
+public abstract class MediaplayerActivity extends CastEnabledActivity implements OnSeekBarChangeListener {
private static final String TAG = "MediaplayerActivity";
private static final String PREFS = "MediaPlayerActivityPreferences";
private static final String PREF_SHOW_TIME_LEFT = "showTimeLeft";
@@ -68,7 +65,6 @@ public abstract class MediaplayerActivity extends AppCompatActivity implements O
protected TextView txtvPosition;
protected TextView txtvLength;
protected SeekBar sbPosition;
- protected Button butPlaybackSpeed;
protected ImageButton butRev;
protected TextView txtvRev;
protected ImageButton butPlay;
@@ -129,8 +125,8 @@ public abstract class MediaplayerActivity extends AppCompatActivity implements O
}
@Override
- public void postStatusMsg(int msg) {
- MediaplayerActivity.this.postStatusMsg(msg);
+ public void postStatusMsg(int msg, boolean showToast) {
+ MediaplayerActivity.this.postStatusMsg(msg, showToast);
}
@Override
@@ -208,7 +204,6 @@ public abstract class MediaplayerActivity extends AppCompatActivity implements O
Log.d(TAG, "onCreate()");
StorageUtils.checkStorageAvailability(this);
- setVolumeControlStream(AudioManager.STREAM_MUSIC);
orientation = getResources().getConfiguration().orientation;
getWindow().setFormat(PixelFormat.TRANSPARENT);
@@ -286,6 +281,7 @@ public abstract class MediaplayerActivity extends AppCompatActivity implements O
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
+ requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.mediaplayer, menu);
return true;
@@ -589,7 +585,7 @@ public abstract class MediaplayerActivity extends AppCompatActivity implements O
*/
protected abstract void onAwaitingVideoSurface();
- protected abstract void postStatusMsg(int resId);
+ protected abstract void postStatusMsg(int resId, boolean showToast);
protected abstract void clearStatusMsg();
@@ -644,40 +640,12 @@ public abstract class MediaplayerActivity extends AppCompatActivity implements O
}
}
- private void updatePlaybackSpeedButton() {
- if(butPlaybackSpeed == null) {
- return;
- }
- if (controller == null) {
- butPlaybackSpeed.setVisibility(View.GONE);
- return;
- }
- updatePlaybackSpeedButtonText();
- ViewCompat.setAlpha(butPlaybackSpeed, controller.canSetPlaybackSpeed() ? 1.0f : 0.5f);
- butPlaybackSpeed.setVisibility(View.VISIBLE);
+ protected void updatePlaybackSpeedButton() {
+ // Only meaningful on AudioplayerActivity, where it is overridden.
}
- private void updatePlaybackSpeedButtonText() {
- if(butPlaybackSpeed == null) {
- return;
- }
- if (controller == null) {
- butPlaybackSpeed.setVisibility(View.GONE);
- return;
- }
- float speed = 1.0f;
- if(controller.canSetPlaybackSpeed()) {
- try {
- // we can only retrieve the playback speed from the controller/playback service
- // once mediaplayer has been initialized
- speed = Float.parseFloat(UserPreferences.getPlaybackSpeed());
- } catch (NumberFormatException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- UserPreferences.setPlaybackSpeed(String.valueOf(speed));
- }
- }
- String speedStr = String.format("%.2fx", speed);
- butPlaybackSpeed.setText(speedStr);
+ protected void updatePlaybackSpeedButtonText() {
+ // Only meaningful on AudioplayerActivity, where it is overridden.
}
@@ -690,28 +658,29 @@ public abstract class MediaplayerActivity extends AppCompatActivity implements O
showTimeLeft = prefs.getBoolean(PREF_SHOW_TIME_LEFT, false);
Log.d("timeleft", showTimeLeft ? "true" : "false");
txtvLength = (TextView) findViewById(R.id.txtvLength);
- txtvLength.setOnClickListener(v -> {
- showTimeLeft = !showTimeLeft;
- Playable media = controller.getMedia();
- if (media == null) {
- return;
- }
+ if (txtvLength != null) {
+ txtvLength.setOnClickListener(v -> {
+ showTimeLeft = !showTimeLeft;
+ Playable media = controller.getMedia();
+ if (media == null) {
+ return;
+ }
- String length;
- if (showTimeLeft) {
- length = "-" + Converter.getDurationStringLong(media.getDuration() - media.getPosition());
- } else {
- length = Converter.getDurationStringLong(media.getDuration());
- }
- txtvLength.setText(length);
+ String length;
+ if (showTimeLeft) {
+ length = "-" + Converter.getDurationStringLong(media.getDuration() - media.getPosition());
+ } else {
+ length = Converter.getDurationStringLong(media.getDuration());
+ }
+ txtvLength.setText(length);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft);
- editor.apply();
- Log.d("timeleft on click", showTimeLeft ? "true" : "false");
- });
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(PREF_SHOW_TIME_LEFT, showTimeLeft);
+ editor.apply();
+ Log.d("timeleft on click", showTimeLeft ? "true" : "false");
+ });
+ }
- butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed);
butRev = (ImageButton) findViewById(R.id.butRev);
txtvRev = (TextView) findViewById(R.id.txtvRev);
if (txtvRev != null) {
@@ -731,47 +700,6 @@ public abstract class MediaplayerActivity extends AppCompatActivity implements O
// BUTTON SETUP
- if(butPlaybackSpeed != null) {
- butPlaybackSpeed.setOnClickListener(v -> {
- if (controller == null) {
- return;
- }
- if (controller.canSetPlaybackSpeed()) {
- String[] availableSpeeds = UserPreferences.getPlaybackSpeedArray();
- String currentSpeed = UserPreferences.getPlaybackSpeed();
-
- // Provide initial value in case the speed list has changed
- // out from under us
- // and our current speed isn't in the new list
- String newSpeed;
- if (availableSpeeds.length > 0) {
- newSpeed = availableSpeeds[0];
- } else {
- newSpeed = "1.00";
- }
-
- for (int i = 0; i < availableSpeeds.length; i++) {
- if (availableSpeeds[i].equals(currentSpeed)) {
- if (i == availableSpeeds.length - 1) {
- newSpeed = availableSpeeds[0];
- } else {
- newSpeed = availableSpeeds[i + 1];
- }
- break;
- }
- }
- UserPreferences.setPlaybackSpeed(newSpeed);
- controller.setPlaybackSpeed(Float.parseFloat(newSpeed));
- } else {
- VariableSpeedDialog.showGetPluginDialog(this);
- }
- });
- butPlaybackSpeed.setOnLongClickListener(v -> {
- VariableSpeedDialog.showDialog(this);
- return true;
- });
- }
-
if (butRev != null) {
butRev.setOnClickListener(v -> onRewind());
butRev.setOnLongClickListener(new View.OnLongClickListener() {
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java
new file mode 100644
index 000000000..e966b8cce
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MediaplayerInfoActivity.java
@@ -0,0 +1,620 @@
+package de.danoeh.antennapod.activity;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.support.design.widget.AppBarLayout;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextMenu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.viewpagerindicator.CirclePageIndicator;
+
+import java.util.List;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.adapter.ChaptersListAdapter;
+import de.danoeh.antennapod.adapter.NavListAdapter;
+import de.danoeh.antennapod.core.asynctask.FeedRemover;
+import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
+import de.danoeh.antennapod.core.feed.EventDistributor;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+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.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.PlaybackController;
+import de.danoeh.antennapod.fragment.AddFeedFragment;
+import de.danoeh.antennapod.fragment.ChaptersFragment;
+import de.danoeh.antennapod.fragment.CoverFragment;
+import de.danoeh.antennapod.fragment.DownloadsFragment;
+import de.danoeh.antennapod.fragment.EpisodesFragment;
+import de.danoeh.antennapod.fragment.ItemDescriptionFragment;
+import de.danoeh.antennapod.fragment.PlaybackHistoryFragment;
+import de.danoeh.antennapod.fragment.QueueFragment;
+import de.danoeh.antennapod.fragment.SubscriptionFragment;
+import de.danoeh.antennapod.menuhandler.NavDrawerActivity;
+import de.danoeh.antennapod.preferences.PreferenceController;
+import rx.Observable;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.schedulers.Schedulers;
+
+/**
+ * Activity for playing files that do not require a video surface.
+ */
+public abstract class MediaplayerInfoActivity extends MediaplayerActivity implements NavDrawerActivity {
+
+ private static final int POS_COVER = 0;
+ private static final int POS_DESCR = 1;
+ private static final int POS_CHAPTERS = 2;
+ private static final int NUM_CONTENT_FRAGMENTS = 3;
+
+ final String TAG = "MediaplayerInfoActivity";
+ private static final String PREFS = "AudioPlayerActivityPreferences";
+ private static final String PREF_KEY_SELECTED_FRAGMENT_POSITION = "selectedFragmentPosition";
+
+ public static final String[] NAV_DRAWER_TAGS = {
+ QueueFragment.TAG,
+ EpisodesFragment.TAG,
+ SubscriptionFragment.TAG,
+ DownloadsFragment.TAG,
+ PlaybackHistoryFragment.TAG,
+ AddFeedFragment.TAG,
+ NavListAdapter.SUBSCRIPTION_LIST_TAG
+ };
+
+ protected Button butPlaybackSpeed;
+ protected ImageButton butCastDisconnect;
+ private DrawerLayout drawerLayout;
+ private NavListAdapter navAdapter;
+ private ListView navList;
+ private View navDrawer;
+ private ActionBarDrawerToggle drawerToggle;
+ private int mPosition = -1;
+
+ private Playable media;
+ private ViewPager pager;
+ private MediaplayerInfoPagerAdapter pagerAdapter;
+
+ private Subscription subscription;
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.d(TAG, "onStop()");
+ if(pagerAdapter != null) {
+ pagerAdapter.setController(null);
+ }
+ if(subscription != null) {
+ subscription.unsubscribe();
+ }
+ EventDistributor.getInstance().unregister(contentUpdate);
+ saveCurrentFragment();
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "onDestroy()");
+ super.onDestroy();
+ // don't risk creating memory leaks
+ drawerLayout = null;
+ navAdapter = null;
+ navList = null;
+ navDrawer = null;
+ drawerToggle = null;
+ pager = null;
+ pagerAdapter = null;
+ }
+
+ @Override
+ protected void chooseTheme() {
+ setTheme(UserPreferences.getNoTitleTheme());
+ }
+
+ protected void saveCurrentFragment() {
+ if(pager == null) {
+ return;
+ }
+ Log.d(TAG, "Saving preferences");
+ SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
+ prefs.edit()
+ .putInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, pager.getCurrentItem())
+ .commit();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if(drawerToggle != null) {
+ drawerToggle.onConfigurationChanged(newConfig);
+ }
+ }
+
+ private void loadLastFragment() {
+ Log.d(TAG, "Restoring instance state");
+ SharedPreferences prefs = getSharedPreferences(PREFS, MODE_PRIVATE);
+ int lastPosition = prefs.getInt(PREF_KEY_SELECTED_FRAGMENT_POSITION, -1);
+ pager.setCurrentItem(lastPosition);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if(pagerAdapter != null && controller != null && controller.getMedia() != media) {
+ media = controller.getMedia();
+ pagerAdapter.onMediaChanged(media);
+ pagerAdapter.setController(controller);
+ }
+
+ EventDistributor.getInstance().register(contentUpdate);
+ loadData();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
+
+ @Override
+ protected void onAwaitingVideoSurface() {
+ Log.d(TAG, "onAwaitingVideoSurface was called in audio player -> switching to video player");
+ startActivity(new Intent(this, VideoplayerActivity.class));
+ }
+
+ @Override
+ protected void postStatusMsg(int resId, boolean showToast) {
+ if (resId == R.string.player_preparing_msg
+ || resId == R.string.player_seeking_msg
+ || resId == R.string.player_buffering_msg) {
+ // TODO Show progress bar here
+ }
+ if (showToast) {
+ Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ protected void clearStatusMsg() {
+ // TODO Hide progress bar here
+ }
+
+
+ @Override
+ protected void setupGUI() {
+ super.setupGUI();
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setTitle("");
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ findViewById(R.id.shadow).setVisibility(View.GONE);
+ AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appBar);
+ float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
+ appBarLayout.setElevation(px);
+ }
+ drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ navList = (ListView) findViewById(R.id.nav_list);
+ navDrawer = findViewById(R.id.nav_layout);
+
+ drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close);
+ drawerToggle.setDrawerIndicatorEnabled(false);
+ drawerLayout.setDrawerListener(drawerToggle);
+
+ navAdapter = new NavListAdapter(itemAccess, this);
+ navList.setAdapter(navAdapter);
+ navList.setOnItemClickListener((parent, view, position, id) -> {
+ int viewType = parent.getAdapter().getItemViewType(position);
+ if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) {
+ Intent intent = new Intent(MediaplayerInfoActivity.this, MainActivity.class);
+ intent.putExtra(MainActivity.EXTRA_NAV_TYPE, viewType);
+ intent.putExtra(MainActivity.EXTRA_NAV_INDEX, position);
+ startActivity(intent);
+ }
+ drawerLayout.closeDrawer(navDrawer);
+ });
+ navList.setOnItemLongClickListener((parent, view, position, id) -> {
+ if (position < navAdapter.getTags().size()) {
+ showDrawerPreferencesDialog();
+ return true;
+ } else {
+ mPosition = position;
+ return false;
+ }
+ });
+ registerForContextMenu(navList);
+ drawerToggle.syncState();
+
+ findViewById(R.id.nav_settings).setOnClickListener(v -> {
+ drawerLayout.closeDrawer(navDrawer);
+ startActivity(new Intent(MediaplayerInfoActivity.this, PreferenceController.getPreferenceActivity()));
+ });
+
+ butPlaybackSpeed = (Button) findViewById(R.id.butPlaybackSpeed);
+ butCastDisconnect = (ImageButton) findViewById(R.id.butCastDisconnect);
+
+ pager = (ViewPager) findViewById(R.id.pager);
+ pagerAdapter = new MediaplayerInfoPagerAdapter(getSupportFragmentManager(), media);
+ pagerAdapter.setController(controller);
+ pager.setAdapter(pagerAdapter);
+ CirclePageIndicator pageIndicator = (CirclePageIndicator) findViewById(R.id.page_indicator);
+ pageIndicator.setViewPager(pager);
+ loadLastFragment();
+ pager.onSaveInstanceState();
+ }
+
+ @Override
+ protected void onPositionObserverUpdate() {
+ super.onPositionObserverUpdate();
+ notifyMediaPositionChanged();
+ }
+
+ @Override
+ protected boolean loadMediaInfo() {
+ if (!super.loadMediaInfo()) {
+ return false;
+ }
+ if(controller.getMedia() != media) {
+ media = controller.getMedia();
+ pagerAdapter.onMediaChanged(media);
+ }
+ return true;
+ }
+
+ public void notifyMediaPositionChanged() {
+ if(pagerAdapter == null) {
+ return;
+ }
+ ChaptersFragment chaptersFragment = pagerAdapter.getChaptersFragment();
+ if(chaptersFragment != null) {
+ ChaptersListAdapter adapter = (ChaptersListAdapter) chaptersFragment.getListAdapter();
+ if (adapter != null) {
+ adapter.notifyDataSetChanged();
+ }
+ }
+ }
+
+ @Override
+ protected void onReloadNotification(int notificationCode) {
+ if (notificationCode == PlaybackService.EXTRA_CODE_VIDEO) {
+ Log.d(TAG, "ReloadNotification received, switching to Videoplayer now");
+ finish();
+ startActivity(new Intent(this, VideoplayerActivity.class));
+
+ }
+ }
+
+ @Override
+ protected void onBufferStart() {
+ postStatusMsg(R.string.player_buffering_msg, false);
+ }
+
+ @Override
+ protected void onBufferEnd() {
+ clearStatusMsg();
+ }
+
+ public PlaybackController getPlaybackController() {
+ return controller;
+ }
+
+ @Override
+ public boolean isDrawerOpen() {
+ return drawerLayout != null && navDrawer != null && drawerLayout.isDrawerOpen(navDrawer);
+ }
+
+ @Override
+ protected int getContentViewResourceId() {
+ return R.layout.mediaplayerinfo_activity;
+ }
+
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return drawerToggle != null && drawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ if(v.getId() != R.id.nav_list) {
+ return;
+ }
+ AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
+ int position = adapterInfo.position;
+ if(position < navAdapter.getSubscriptionOffset()) {
+ return;
+ }
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.nav_feed_context, menu);
+ Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset());
+ menu.setHeaderTitle(feed.getTitle());
+ // episodes are not loaded, so we cannot check if the podcast has new or unplayed ones!
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ final int position = mPosition;
+ mPosition = -1; // reset
+ if(position < 0) {
+ return false;
+ }
+ Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset());
+ switch(item.getItemId()) {
+ case R.id.mark_all_seen_item:
+ DBWriter.markFeedSeen(feed.getId());
+ return true;
+ case R.id.mark_all_read_item:
+ DBWriter.markFeedRead(feed.getId());
+ return true;
+ case R.id.remove_item:
+ final FeedRemover remover = new FeedRemover(this, feed) {
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ }
+ };
+ ConfirmationDialog conDialog = new ConfirmationDialog(this,
+ R.string.remove_feed_label,
+ R.string.feed_delete_confirmation_msg) {
+ @Override
+ public void onConfirmButtonPressed(
+ DialogInterface dialog) {
+ dialog.dismiss();
+ if (controller != null) {
+ Playable playable = controller.getMedia();
+ if (playable != null && playable instanceof FeedMedia) {
+ FeedMedia media = (FeedMedia) playable;
+ if (media.getItem().getFeed().getId() == feed.getId()) {
+ Log.d(TAG, "Currently playing episode is about to be deleted, skipping");
+ remover.skipOnCompletion = true;
+ if(controller.getStatus() == PlayerStatus.PLAYING) {
+ sendBroadcast(new Intent(
+ PlaybackService.ACTION_PAUSE_PLAY_CURRENT_EPISODE));
+ }
+ }
+ }
+ }
+ remover.executeAsync();
+ }
+ };
+ conDialog.createNewDialog().show();
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(isDrawerOpen()) {
+ drawerLayout.closeDrawer(navDrawer);
+ } else if (pager == null || pager.getCurrentItem() == 0) {
+ // If the user is currently looking at the first step, allow the system to handle the
+ // Back button. This calls finish() on this activity and pops the back stack.
+ super.onBackPressed();
+ } else {
+ // Otherwise, select the previous step.
+ pager.setCurrentItem(pager.getCurrentItem() - 1);
+ }
+ }
+
+ public void showDrawerPreferencesDialog() {
+ final List<String> hiddenDrawerItems = UserPreferences.getHiddenDrawerItems();
+ String[] navLabels = new String[NAV_DRAWER_TAGS.length];
+ final boolean[] checked = new boolean[NAV_DRAWER_TAGS.length];
+ for (int i = 0; i < NAV_DRAWER_TAGS.length; i++) {
+ String tag = NAV_DRAWER_TAGS[i];
+ navLabels[i] = navAdapter.getLabel(tag);
+ if (!hiddenDrawerItems.contains(tag)) {
+ checked[i] = true;
+ }
+ }
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.drawer_preferences);
+ builder.setMultiChoiceItems(navLabels, checked, (dialog, which, isChecked) -> {
+ if (isChecked) {
+ hiddenDrawerItems.remove(NAV_DRAWER_TAGS[which]);
+ } else {
+ hiddenDrawerItems.add(NAV_DRAWER_TAGS[which]);
+ }
+ });
+ builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
+ UserPreferences.setHiddenDrawerItems(hiddenDrawerItems);
+ });
+ builder.setNegativeButton(R.string.cancel_label, null);
+ builder.create().show();
+ }
+
+ private DBReader.NavDrawerData navDrawerData;
+
+ private void loadData() {
+ subscription = Observable.fromCallable(DBReader::getNavDrawerData)
+ .subscribeOn(Schedulers.newThread())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ navDrawerData = result;
+ if (navAdapter != null) {
+ navAdapter.notifyDataSetChanged();
+ }
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ });
+ }
+
+
+
+ private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() {
+
+ @Override
+ public void update(EventDistributor eventDistributor, Integer arg) {
+ if ((EventDistributor.FEED_LIST_UPDATE & arg) != 0) {
+ Log.d(TAG, "Received contentUpdate Intent.");
+ loadData();
+ }
+ }
+ };
+
+ private final NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() {
+ @Override
+ public int getCount() {
+ if (navDrawerData != null) {
+ return navDrawerData.feeds.size();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public Feed getItem(int position) {
+ if (navDrawerData != null && 0 <= position && position < navDrawerData.feeds.size()) {
+ return navDrawerData.feeds.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getSelectedItemIndex() {
+ return -1;
+ }
+
+ @Override
+ public int getQueueSize() {
+ return (navDrawerData != null) ? navDrawerData.queueSize : 0;
+ }
+
+ @Override
+ public int getNumberOfNewItems() {
+ return (navDrawerData != null) ? navDrawerData.numNewItems : 0;
+ }
+
+ @Override
+ public int getNumberOfDownloadedItems() {
+ return (navDrawerData != null) ? navDrawerData.numDownloadedItems : 0;
+ }
+
+ @Override
+ public int getReclaimableItems() {
+ return (navDrawerData != null) ? navDrawerData.reclaimableSpace : 0;
+ }
+
+ @Override
+ public int getFeedCounter(long feedId) {
+ return navDrawerData != null ? navDrawerData.feedCounters.get(feedId) : 0;
+ }
+
+ @Override
+ public int getFeedCounterSum() {
+ if(navDrawerData == null) {
+ return 0;
+ }
+ int sum = 0;
+ for(int counter : navDrawerData.feedCounters.values()) {
+ sum += counter;
+ }
+ return sum;
+ }
+ };
+
+ public interface MediaplayerInfoContentFragment {
+ void onMediaChanged(Playable media);
+ }
+
+ private static class MediaplayerInfoPagerAdapter extends FragmentStatePagerAdapter {
+
+ private static final String TAG = "MPInfoPagerAdapter";
+
+ private Playable media;
+ private PlaybackController controller;
+
+ public MediaplayerInfoPagerAdapter(FragmentManager fm, Playable media) {
+ super(fm);
+ this.media = media;
+ }
+
+ private CoverFragment coverFragment;
+ private ItemDescriptionFragment itemDescriptionFragment;
+ private ChaptersFragment chaptersFragment;
+
+ public void onMediaChanged(Playable media) {
+ Log.d(TAG, "media changing to " + media.getEpisodeTitle());
+ this.media = media;
+ if(coverFragment != null) {
+ coverFragment.onMediaChanged(media);
+ }
+ if(itemDescriptionFragment != null) {
+ itemDescriptionFragment.onMediaChanged(media);
+ }
+ if(chaptersFragment != null) {
+ chaptersFragment.onMediaChanged(media);
+ }
+ }
+
+ public void setController(PlaybackController controller) {
+ this.controller = controller;
+ if(chaptersFragment != null) {
+ chaptersFragment.setController(controller);
+ }
+ }
+
+ @Nullable
+ public ChaptersFragment getChaptersFragment() {
+ return chaptersFragment;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ Log.d(TAG, "getItem(" + position + ")");
+ switch (position) {
+ case POS_COVER:
+ if(coverFragment == null) {
+ coverFragment = CoverFragment.newInstance(media);
+ }
+ return coverFragment;
+ case POS_DESCR:
+ if(itemDescriptionFragment == null) {
+ itemDescriptionFragment = ItemDescriptionFragment.newInstance(media, true, true);
+ }
+ return itemDescriptionFragment;
+ case POS_CHAPTERS:
+ if(chaptersFragment == null) {
+ chaptersFragment = ChaptersFragment.newInstance(media);
+ chaptersFragment.setController(controller);
+ }
+ return chaptersFragment;
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return NUM_CONTENT_FRAGMENTS;
+ }
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
index 42c9edd99..a52382dea 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java
@@ -83,6 +83,12 @@ public class VideoplayerActivity extends MediaplayerActivity {
launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
true);
startService(launchIntent);
+ } else if (PlaybackService.isCasting()) {
+ Intent intent = PlaybackService.getPlayerActivityIntent(this);
+ if (!intent.getComponent().getClassName().equals(VideoplayerActivity.class.getName())) {
+ finish();
+ startActivity(intent);
+ }
}
}
@@ -159,7 +165,7 @@ public class VideoplayerActivity extends MediaplayerActivity {
}
@Override
- protected void postStatusMsg(int resId) {
+ protected void postStatusMsg(int resId, boolean showToast) {
if (resId == R.string.player_preparing_msg) {
progressIndicator.setVisibility(View.VISIBLE);
} else {
@@ -257,6 +263,10 @@ public class VideoplayerActivity extends MediaplayerActivity {
Log.d(TAG, "ReloadNotification received, switching to Audioplayer now");
finish();
startActivity(new Intent(this, AudioplayerActivity.class));
+ } else if (notificationCode == PlaybackService.EXTRA_CODE_CAST) {
+ Log.d(TAG, "ReloadNotification received, switching to Castplayer now");
+ finish();
+ startActivity(new Intent(this, CastplayerActivity.class));
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java
index 997befe99..1ab60ef61 100644
--- a/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java
+++ b/app/src/main/java/de/danoeh/antennapod/config/PlaybackServiceCallbacksImpl.java
@@ -5,6 +5,7 @@ import android.content.Intent;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.AudioplayerActivity;
+import de.danoeh.antennapod.activity.CastplayerActivity;
import de.danoeh.antennapod.activity.VideoplayerActivity;
import de.danoeh.antennapod.core.PlaybackServiceCallbacks;
import de.danoeh.antennapod.core.feed.MediaType;
@@ -12,7 +13,10 @@ import de.danoeh.antennapod.core.feed.MediaType;
public class PlaybackServiceCallbacksImpl implements PlaybackServiceCallbacks {
@Override
- public Intent getPlayerActivityIntent(Context context, MediaType mediaType) {
+ public Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback) {
+ if (remotePlayback) {
+ return new Intent(context, CastplayerActivity.class);
+ }
if (mediaType == MediaType.VIDEO) {
return new Intent(context, VideoplayerActivity.class);
} else {
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
index aea911f79..77e66f3b0 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ChaptersFragment.java
@@ -7,14 +7,14 @@ import android.view.View;
import android.widget.ListView;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.AudioplayerActivity.AudioplayerContentFragment;
+import de.danoeh.antennapod.activity.MediaplayerInfoActivity.MediaplayerInfoContentFragment;
import de.danoeh.antennapod.adapter.ChaptersListAdapter;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.util.playback.Playable;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
-public class ChaptersFragment extends ListFragment implements AudioplayerContentFragment {
+public class ChaptersFragment extends ListFragment implements MediaplayerInfoContentFragment {
private static final String TAG = "ChaptersFragment";
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
index d3b97f9df..943ddeec7 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/CoverFragment.java
@@ -12,14 +12,14 @@ import android.widget.TextView;
import com.bumptech.glide.Glide;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.AudioplayerActivity.AudioplayerContentFragment;
+import de.danoeh.antennapod.activity.MediaplayerInfoActivity.MediaplayerInfoContentFragment;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.util.playback.Playable;
/**
* Displays the cover and the title of a FeedItem.
*/
-public class CoverFragment extends Fragment implements AudioplayerContentFragment {
+public class CoverFragment extends Fragment implements MediaplayerInfoContentFragment {
private static final String TAG = "CoverFragment";
private static final String ARG_PLAYABLE = "arg.playable";
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
index ca60e7bf2..758f8095d 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java
@@ -172,7 +172,7 @@ public class ExternalPlayerFragment extends Fragment {
.into(imgvCover);
fragmentLayout.setVisibility(View.VISIBLE);
- if (controller.isPlayingVideo()) {
+ if (controller.isPlayingVideoLocally()) {
butPlay.setVisibility(View.GONE);
} else {
butPlay.setVisibility(View.VISIBLE);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
index 5b301333e..55d28cadb 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
@@ -27,8 +27,8 @@ import android.webkit.WebViewClient;
import android.widget.Toast;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.AudioplayerActivity;
-import de.danoeh.antennapod.activity.AudioplayerActivity.AudioplayerContentFragment;
+import de.danoeh.antennapod.activity.MediaplayerInfoActivity;
+import de.danoeh.antennapod.activity.MediaplayerInfoActivity.MediaplayerInfoContentFragment;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
@@ -47,7 +47,7 @@ import rx.schedulers.Schedulers;
/**
* Displays the description of a Playable object in a Webview.
*/
-public class ItemDescriptionFragment extends Fragment implements AudioplayerContentFragment {
+public class ItemDescriptionFragment extends Fragment implements MediaplayerInfoContentFragment {
private static final String TAG = "ItemDescriptionFragment";
@@ -371,8 +371,8 @@ public class ItemDescriptionFragment extends Fragment implements AudioplayerCont
private void onTimecodeLinkSelected(String link) {
int time = Timeline.getTimecodeLinkTime(link);
- if (getActivity() != null && getActivity() instanceof AudioplayerActivity) {
- PlaybackController pc = ((AudioplayerActivity) getActivity()).getPlaybackController();
+ if (getActivity() != null && getActivity() instanceof MediaplayerInfoActivity) {
+ PlaybackController pc = ((MediaplayerInfoActivity) getActivity()).getPlaybackController();
if (pc != null) {
pc.seekTo(time);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
index 9740c9af9..e721af47d 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/ItemFragment.java
@@ -39,6 +39,7 @@ import org.apache.commons.lang3.ArrayUtils;
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.adapter.DefaultActionButtonCallback;
import de.danoeh.antennapod.core.event.DownloadEvent;
@@ -311,6 +312,7 @@ public class ItemFragment extends Fragment implements OnSwipeGesture {
if(!isAdded() || item == null) {
return;
}
+ ((CastEnabledActivity) getActivity()).requestCastButton(MenuItem.SHOW_AS_ACTION_ALWAYS);
inflater.inflate(R.menu.feeditem_options, menu);
popupMenu = menu;
if (item.hasMedia()) {
diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java
index 3c1332e33..6c03dc7ae 100644
--- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java
+++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceController.java
@@ -34,6 +34,8 @@ import android.widget.Toast;
import android.widget.ListView;
import com.afollestad.materialdialogs.MaterialDialog;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
import org.apache.commons.lang3.ArrayUtils;
@@ -410,6 +412,22 @@ public class PreferenceController implements SharedPreferences.OnSharedPreferenc
ui.getActivity().startActivity(Intent.createChooser(emailIntent, intentTitle));
return true;
});
+ //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) {
+ return true;
+ } else {
+ GoogleApiAvailability.getInstance()
+ .getErrorDialog(ui.getActivity(), googlePlayServicesCheck, 0)
+ .show();
+ return false;
+ }
+ }
+ return true;
+ });
buildEpisodeCleanupPreference();
buildSmartMarkAsPlayedPreference();
buildAutodownloadSelectedNetworsPreference();
diff --git a/app/src/main/res/layout/external_player_fragment.xml b/app/src/main/res/layout/external_player_fragment.xml
index e6fd21241..fb7abde55 100644
--- a/app/src/main/res/layout/external_player_fragment.xml
+++ b/app/src/main/res/layout/external_player_fragment.xml
@@ -37,6 +37,18 @@
android:indeterminate="false"
tools:progress="100"/>
+ <ImageButton
+ android:id="@+id/butPlay"
+ android:layout_width="52dp"
+ android:layout_height="52dp"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/episodeProgress"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/pause_label"
+ android:background="?attr/selectableItemBackground"
+ tools:src="@drawable/ic_play_arrow_white_36dp"/>
+
<TextView
android:id="@+id/txtvTitle"
android:layout_width="wrap_content"
@@ -72,18 +84,6 @@
android:maxLines="1"
tools:text="Episode author that is too long and will cause the text to wrap"/>
- <ImageButton
- android:id="@+id/butPlay"
- android:layout_width="52dp"
- android:layout_height="52dp"
- android:layout_alignParentRight="true"
- android:layout_alignParentEnd="true"
- android:layout_below="@id/episodeProgress"
- android:layout_centerVertical="true"
- android:contentDescription="@string/pause_label"
- android:background="?attr/selectableItemBackground"
- tools:src="@drawable/ic_play_arrow_white_36dp"/>
-
</RelativeLayout>
</LinearLayout>
diff --git a/app/src/main/res/layout/audioplayer_activity.xml b/app/src/main/res/layout/mediaplayerinfo_activity.xml
index fb4f995a2..0f68b503e 100644
--- a/app/src/main/res/layout/audioplayer_activity.xml
+++ b/app/src/main/res/layout/mediaplayerinfo_activity.xml
@@ -152,6 +152,21 @@
android:src="?attr/av_fast_forward"
android:textSize="@dimen/text_size_medium"
android:textAllCaps="false"
+ tools:visibility="gone"
+ tools:background="@android:color/holo_green_dark" />
+
+ <ImageButton
+ android:id="@+id/butCastDisconnect"
+ android:layout_width="@dimen/audioplayer_playercontrols_length"
+ android:layout_height="@dimen/audioplayer_playercontrols_length"
+ android:layout_toLeftOf="@id/butRev"
+ android:background="?attr/selectableItemBackground"
+ android:contentDescription="@string/cast_disconnect_label"
+ android:src="?attr/ic_cast_disconnect"
+ android:scaleType="fitCenter"
+ android:visibility="gone"
+ tools:visibility="visible"
+ tools:src="@drawable/ic_cast_disconnect_white_36dp"
tools:background="@android:color/holo_green_dark" />
<ImageButton
diff --git a/app/src/main/res/menu/cast_enabled.xml b/app/src/main/res/menu/cast_enabled.xml
new file mode 100644
index 000000000..d6e85c311
--- /dev/null
+++ b/app/src/main/res/menu/cast_enabled.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:custom="http://schemas.android.com/apk/res-auto">
+
+ <item
+ android:id="@+id/media_route_menu_item"
+ android:title="@string/cast_media_route_menu_title"
+ custom:actionProviderClass="de.danoeh.antennapod.core.cast.SwitchableMediaRouteActionProvider"
+ custom:showAsAction="ifRoom"/>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index ecdcd3517..57829e3e1 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -293,5 +293,14 @@
android:key="prefAbout"
android:title="@string/about_pref"/>
</PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/experimental_pref">
+ <de.danoeh.antennapod.preferences.SwitchCompatPreference
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefCast"
+ android:summary="@string/pref_cast_message"
+ android:title="@string/pref_cast_title"/>
+ </PreferenceCategory>
</PreferenceScreen>
diff --git a/build.gradle b/build.gradle
index f692e201a..3eace8f8a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -61,6 +61,9 @@ project.ext {
triangleLabelViewVersion = "1.1.0"
audioPlayerVersion = "v1.0.16"
+
+ castCompanionLibVer = "2.8.3"
+ playServicesVersion = "8.4.0"
}
task wrapper(type: Wrapper) {
diff --git a/core/build.gradle b/core/build.gradle
index f79ea8fc8..5264e435b 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -57,4 +57,9 @@ dependencies {
compile "io.reactivex:rxandroid:$rxAndroidVersion"
compile "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion"
+
+ // Add casting features
+ compile "com.google.android.libraries.cast.companionlibrary:ccl:$castCompanionLibVer"
+ compile "com.android.support:mediarouter-v7:$supportVersion"
+ compile "com.google.android.gms:play-services-cast:$playServicesVersion"
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
index a96affb23..9bbccbb82 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java
@@ -2,6 +2,7 @@ package de.danoeh.antennapod.core;
import android.content.Context;
+import de.danoeh.antennapod.core.cast.CastManager;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
@@ -41,6 +42,7 @@ public class ClientConfig {
UpdateManager.init(context);
PlaybackPreferences.init(context);
NetworkUtils.init(context);
+ CastManager.init(context);
initialized = true;
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java
index 08ccb6d71..13a32ab8a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java
@@ -15,9 +15,10 @@ public interface PlaybackServiceCallbacks {
* type of media that is being played.
*
* @param mediaType The type of media that is being played.
+ * @param remotePlayback true if the media is played on a remote device.
* @return A non-null activity intent.
*/
- Intent getPlayerActivityIntent(Context context, MediaType mediaType);
+ Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback);
/**
* Returns true if the PlaybackService should load new episodes from the queue when playback ends
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java b/core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java
new file mode 100644
index 000000000..213dd1875
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java
@@ -0,0 +1,11 @@
+package de.danoeh.antennapod.core.cast;
+
+import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumer;
+
+public interface CastConsumer extends VideoCastConsumer{
+
+ /**
+ * Called when the stream's volume is changed.
+ */
+ void onStreamVolumeChanged(double value, boolean isMute);
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java
new file mode 100644
index 000000000..5b1fdab61
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java
@@ -0,0 +1,1766 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * ------------------------------------------------------------------------
+ *
+ * Changes made by Domingos Lopes <domingos86lopes@gmail.com>
+ *
+ * original can be found at http://www.github.com/googlecast/CastCompanionLibrary-android
+ */
+
+package de.danoeh.antennapod.core.cast;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+
+import com.google.android.gms.cast.ApplicationMetadata;
+import com.google.android.gms.cast.Cast;
+import com.google.android.gms.cast.CastDevice;
+import com.google.android.gms.cast.CastMediaControlIntent;
+import com.google.android.gms.cast.CastStatusCodes;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaMetadata;
+import com.google.android.gms.cast.MediaQueueItem;
+import com.google.android.gms.cast.MediaStatus;
+import com.google.android.gms.cast.RemoteMediaPlayer;
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager;
+import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration;
+import com.google.android.libraries.cast.companionlibrary.cast.MediaQueue;
+import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException;
+import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException;
+import com.google.android.libraries.cast.companionlibrary.cast.exceptions.OnFailedListener;
+import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException;
+
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.TimeUnit;
+
+import de.danoeh.antennapod.core.R;
+
+import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_PLAY;
+import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_UNCHANGED;
+
+/**
+ * A subclass of {@link BaseCastManager} that is suitable for casting video contents (it
+ * also provides a single custom data channel/namespace if an out-of-band communication is
+ * needed).
+ * <p>
+ * Clients need to initialize this class by calling
+ * {@link #init(android.content.Context)} in the Application's
+ * {@code onCreate()} method. To access the (singleton) instance of this class, clients
+ * need to call {@link #getInstance()}.
+ * <p>This
+ * class manages various states of the remote cast device. Client applications, however, can
+ * complement the default behavior of this class by hooking into various callbacks that it provides
+ * (see {@link CastConsumer}).
+ * Since the number of these callbacks is usually much larger than what a single application might
+ * be interested in, there is a no-op implementation of this interface (see
+ * {@link DefaultCastConsumer}) that applications can subclass to override only those methods that
+ * they are interested in. Since this library depends on the cast functionalities provided by the
+ * Google Play services, the library checks to ensure that the right version of that service is
+ * installed. It also provides a simple static method {@code checkGooglePlayServices()} that clients
+ * can call at an early stage of their applications to provide a dialog for users if they need to
+ * update/activate their Google Play Services library.
+ *
+ * @see CastConfiguration
+ */
+public class CastManager extends BaseCastManager implements OnFailedListener {
+ public static final String TAG = "CastManager";
+
+ public static final String CAST_APP_ID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID;
+
+ public static final double DEFAULT_VOLUME_STEP = 0.05;
+ public static final long DEFAULT_LIVE_STREAM_DURATION_MS = TimeUnit.HOURS.toMillis(2);
+ private double volumeStep = DEFAULT_VOLUME_STEP;
+ private MediaQueue mediaQueue;
+ private MediaStatus mediaStatus;
+
+ private static CastManager INSTANCE;
+ private RemoteMediaPlayer remoteMediaPlayer;
+ private int state = MediaStatus.PLAYER_STATE_IDLE;
+ private int idleReason;
+ private final Set<CastConsumer> castConsumers = new CopyOnWriteArraySet<>();
+ private long liveStreamDuration = DEFAULT_LIVE_STREAM_DURATION_MS;
+ private MediaQueueItem preLoadingItem;
+
+ public static final int QUEUE_OPERATION_LOAD = 1;
+ public static final int QUEUE_OPERATION_INSERT_ITEMS = 2;
+ public static final int QUEUE_OPERATION_UPDATE_ITEMS = 3;
+ public static final int QUEUE_OPERATION_JUMP = 4;
+ public static final int QUEUE_OPERATION_REMOVE_ITEM = 5;
+ public static final int QUEUE_OPERATION_REMOVE_ITEMS = 6;
+ public static final int QUEUE_OPERATION_REORDER = 7;
+ public static final int QUEUE_OPERATION_MOVE = 8;
+ public static final int QUEUE_OPERATION_APPEND = 9;
+ public static final int QUEUE_OPERATION_NEXT = 10;
+ public static final int QUEUE_OPERATION_PREV = 11;
+ public static final int QUEUE_OPERATION_SET_REPEAT = 12;
+
+ private CastManager(Context context, CastConfiguration castConfiguration) {
+ super(context, castConfiguration);
+ Log.d(TAG, "CastManager is instantiated");
+ }
+
+ public static synchronized CastManager init(Context context) {
+ if (INSTANCE == null) {
+ //TODO also setup dialog factory if necessary
+ CastConfiguration castConfiguration = new CastConfiguration.Builder(CAST_APP_ID)
+ .enableDebug()
+ .enableAutoReconnect()
+ .enableWifiReconnection()
+ .setLaunchOptions(true, Locale.getDefault())
+ .build();
+ Log.d(TAG, "New instance of CastManager is created");
+ if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance()
+ .isGooglePlayServicesAvailable(context)) {
+ Log.e(TAG, "Couldn't find the appropriate version of Google Play Services");
+ //TODO check whether creating an instance without google play services installed actually gives an exception
+ }
+ INSTANCE = new CastManager(context, castConfiguration);
+ }
+ return INSTANCE;
+ }
+
+ /**
+ * Returns a (singleton) instance of this class. Clients should call this method in order to
+ * get a hold of this singleton instance, only after it is initialized. If it is not initialized
+ * yet, an {@link IllegalStateException} will be thrown.
+ *
+ */
+ public static CastManager getInstance() {
+ if (INSTANCE == null) {
+ String msg = "No CastManager instance was found, did you forget to initialize it?";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+ return INSTANCE;
+ }
+
+ /**
+ * Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media
+ * control APIs that this library do not provide a wrapper for, client applications can call
+ * those methods directly after obtaining an instance of the active {@link RemoteMediaPlayer}.
+ */
+ public final RemoteMediaPlayer getRemoteMediaPlayer() {
+ return remoteMediaPlayer;
+ }
+
+ /**
+ * Determines if the media that is loaded remotely is a live stream or not.
+ *
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public final boolean isRemoteStreamLive() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ MediaInfo info = getRemoteMediaInformation();
+ return (info != null) && (info.getStreamType() == MediaInfo.STREAM_TYPE_LIVE);
+ }
+
+ /*
+ * A simple check to make sure remoteMediaPlayer is not null
+ */
+ private void checkRemoteMediaPlayerAvailable() throws NoConnectionException {
+ if (remoteMediaPlayer == null) {
+ throw new NoConnectionException();
+ }
+ }
+
+ /**
+ * Returns the url for the media that is currently playing on the remote device. If there is no
+ * connection, this will return <code>null</code>.
+ *
+ * @throws NoConnectionException If no connectivity to the device exists
+ * @throws TransientNetworkDisconnectionException If framework is still trying to recover from
+ * a possibly transient loss of network
+ */
+ public String getRemoteMediaUrl() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ if (remoteMediaPlayer != null && remoteMediaPlayer.getMediaInfo() != null) {
+ MediaInfo info = remoteMediaPlayer.getMediaInfo();
+ remoteMediaPlayer.getMediaStatus().getPlayerState();
+ return info.getContentId();
+ }
+ throw new NoConnectionException();
+ }
+
+ /**
+ * Indicates if the remote media is currently playing (or buffering).
+ *
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public boolean isRemoteMediaPlaying() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ return state == MediaStatus.PLAYER_STATE_BUFFERING
+ || state == MediaStatus.PLAYER_STATE_PLAYING;
+ }
+
+ /**
+ * Returns <code>true</code> if the remote connected device is playing a movie.
+ *
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public boolean isRemoteMediaPaused() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ return state == MediaStatus.PLAYER_STATE_PAUSED;
+ }
+
+ /**
+ * Returns <code>true</code> only if there is a media on the remote being played, paused or
+ * buffered.
+ *
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public boolean isRemoteMediaLoaded() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ return isRemoteMediaPaused() || isRemoteMediaPlaying();
+ }
+
+ /**
+ * Returns the {@link MediaInfo} for the current media
+ *
+ * @throws NoConnectionException If no connectivity to the device exists
+ * @throws TransientNetworkDisconnectionException If framework is still trying to recover from
+ * a possibly transient loss of network
+ */
+ public MediaInfo getRemoteMediaInformation() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ checkRemoteMediaPlayerAvailable();
+ return remoteMediaPlayer.getMediaInfo();
+ }
+
+ /**
+ * Gets the remote's system volume. It internally detects what type of volume is used.
+ *
+ * @throws NoConnectionException If no connectivity to the device exists
+ * @throws TransientNetworkDisconnectionException If framework is still trying to recover from
+ * a possibly transient loss of network
+ */
+ public double getStreamVolume() throws TransientNetworkDisconnectionException, NoConnectionException {
+ checkConnectivity();
+ checkRemoteMediaPlayerAvailable();
+ return remoteMediaPlayer.getMediaStatus().getStreamVolume();
+ }
+
+ /**
+ * Sets the stream volume.
+ *
+ * @param volume Should be a value between 0 and 1, inclusive.
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ * @throws CastException If setting system volume fails
+ */
+ public void setStreamVolume(double volume) throws CastException,
+ TransientNetworkDisconnectionException, NoConnectionException {
+ checkConnectivity();
+ if (volume > 1.0) {
+ volume = 1.0;
+ } else if (volume < 0) {
+ volume = 0.0;
+ }
+
+ RemoteMediaPlayer mediaPlayer = getRemoteMediaPlayer();
+ if (mediaPlayer == null) {
+ throw new NoConnectionException();
+ }
+ mediaPlayer.setStreamVolume(mApiClient, volume).setResultCallback(
+ (result) -> {
+ if (!result.getStatus().isSuccess()) {
+ onFailed(R.string.cast_failed_setting_volume,
+ result.getStatus().getStatusCode());
+ } else {
+ CastManager.this.onStreamVolumeChanged();
+ }
+ });
+ }
+
+ /**
+ * Returns <code>true</code> if remote Stream is muted.
+ *
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public boolean isStreamMute() throws TransientNetworkDisconnectionException, NoConnectionException {
+ checkConnectivity();
+ checkRemoteMediaPlayerAvailable();
+ return remoteMediaPlayer.getMediaStatus().isMute();
+ }
+
+ /**
+ * Returns <code>true</code> if remote device is muted.
+ *
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public boolean isMute() throws TransientNetworkDisconnectionException, NoConnectionException {
+ return isStreamMute() || isDeviceMute();
+ }
+
+ /**
+ * Mutes or un-mutes the stream volume.
+ *
+ * @throws CastException
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void setStreamMute(boolean mute) throws CastException, TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ checkRemoteMediaPlayerAvailable();
+ remoteMediaPlayer.setStreamMute(mApiClient, mute);
+ }
+
+ /**
+ * Returns the duration of the media that is loaded, in milliseconds.
+ *
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public long getMediaDuration() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ checkRemoteMediaPlayerAvailable();
+ return remoteMediaPlayer.getStreamDuration();
+ }
+
+ /**
+ * Returns the time left (in milliseconds) of the current media. If there is no
+ * {@code RemoteMediaPlayer}, it returns -1.
+ *
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public long getMediaTimeRemaining()
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ return -1;
+ }
+ return isRemoteStreamLive() ? liveStreamDuration : remoteMediaPlayer.getStreamDuration()
+ - remoteMediaPlayer.getApproximateStreamPosition();
+ }
+
+ /**
+ * Returns the current (approximate) position of the current media, in milliseconds.
+ *
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public long getCurrentMediaPosition() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ checkRemoteMediaPlayerAvailable();
+ return remoteMediaPlayer.getApproximateStreamPosition();
+ }
+
+ public int getApplicationStandbyState() throws IllegalStateException {
+ Log.d(TAG, "getApplicationStandbyState()");
+ return Cast.CastApi.getStandbyState(mApiClient);
+ }
+
+ private void onApplicationDisconnected(int errorCode) {
+ Log.d(TAG, "onApplicationDisconnected() reached with error code: " + errorCode);
+ mApplicationErrorCode = errorCode;
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onApplicationDisconnected(errorCode);
+ }
+ if (mMediaRouter != null) {
+ Log.d(TAG, "onApplicationDisconnected(): Cached RouteInfo: " + getRouteInfo());
+ Log.d(TAG, "onApplicationDisconnected(): Selected RouteInfo: "
+ + mMediaRouter.getSelectedRoute());
+ if (getRouteInfo() == null || mMediaRouter.getSelectedRoute().equals(getRouteInfo())) {
+ Log.d(TAG, "onApplicationDisconnected(): Setting route to default");
+ mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
+ }
+ }
+ onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
+ }
+
+ private void onApplicationStatusChanged() {
+ if (!isConnected()) {
+ return;
+ }
+ try {
+ String appStatus = Cast.CastApi.getApplicationStatus(mApiClient);
+ Log.d(TAG, "onApplicationStatusChanged() reached: " + appStatus);
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onApplicationStatusChanged(appStatus);
+ }
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "onApplicationStatusChanged()", e);
+ }
+ }
+
+ private void onDeviceVolumeChanged() {
+ Log.d(TAG, "onDeviceVolumeChanged() reached");
+ double volume;
+ try {
+ volume = getDeviceVolume();
+ boolean isMute = isDeviceMute();
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onVolumeChanged(volume, isMute);
+ }
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Failed to get volume", e);
+ }
+
+ }
+
+ private void onStreamVolumeChanged() {
+ Log.d(TAG, "onStreamVolumeChanged() reached");
+ double volume;
+ try {
+ volume = getStreamVolume();
+ boolean isMute = isStreamMute();
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onStreamVolumeChanged(volume, isMute);
+ }
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Failed to get volume", e);
+ }
+ }
+
+ @Override
+ protected void onApplicationConnected(ApplicationMetadata appMetadata,
+ String applicationStatus, String sessionId, boolean wasLaunched) {
+ Log.d(TAG, "onApplicationConnected() reached with sessionId: " + sessionId
+ + ", and mReconnectionStatus=" + mReconnectionStatus);
+ mApplicationErrorCode = NO_APPLICATION_ERROR;
+ if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) {
+ // we have tried to reconnect and successfully launched the app, so
+ // it is time to select the route and make the cast icon happy :-)
+ List<MediaRouter.RouteInfo> routes = mMediaRouter.getRoutes();
+ if (routes != null) {
+ String routeId = mPreferenceAccessor.getStringFromPreference(PREFS_KEY_ROUTE_ID);
+ for (MediaRouter.RouteInfo routeInfo : routes) {
+ if (routeId.equals(routeInfo.getId())) {
+ // found the right route
+ Log.d(TAG, "Found the correct route during reconnection attempt");
+ mReconnectionStatus = RECONNECTION_STATUS_FINALIZED;
+ mMediaRouter.selectRoute(routeInfo);
+ break;
+ }
+ }
+ }
+ }
+ try {
+ //attachDataChannel();
+ attachMediaChannel();
+ mSessionId = sessionId;
+ // saving device for future retrieval; we only save the last session info
+ mPreferenceAccessor.saveStringToPreference(PREFS_KEY_SESSION_ID, mSessionId);
+ remoteMediaPlayer.requestStatus(mApiClient).setResultCallback(result -> {
+ if (!result.getStatus().isSuccess()) {
+ onFailed(R.string.cast_failed_status_request,
+ result.getStatus().getStatusCode());
+ }
+ });
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onApplicationConnected(appMetadata, mSessionId, wasLaunched);
+ }
+ } catch (TransientNetworkDisconnectionException e) {
+ Log.e(TAG, "Failed to attach media/data channel due to network issues", e);
+ onFailed(R.string.cast_failed_no_connection_trans, NO_STATUS_CODE);
+ } catch (NoConnectionException e) {
+ Log.e(TAG, "Failed to attach media/data channel due to network issues", e);
+ onFailed(R.string.cast_failed_no_connection, NO_STATUS_CODE);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager
+ * #onConnectivityRecovered()
+ */
+ @Override
+ public void onConnectivityRecovered() {
+ reattachMediaChannel();
+ //reattachDataChannel();
+ super.onConnectivityRecovered();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.google.android.gms.cast.CastClient.Listener#onApplicationStopFailed (int)
+ */
+ @Override
+ public void onApplicationStopFailed(int errorCode) {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onApplicationStopFailed(errorCode);
+ }
+ }
+
+ @Override
+ public void onApplicationConnectionFailed(int errorCode) {
+ Log.d(TAG, "onApplicationConnectionFailed() reached with errorCode: " + errorCode);
+ mApplicationErrorCode = errorCode;
+ if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) {
+ if (errorCode == CastStatusCodes.APPLICATION_NOT_RUNNING) {
+ // while trying to re-establish session, we found out that the app is not running
+ // so we need to disconnect
+ mReconnectionStatus = RECONNECTION_STATUS_INACTIVE;
+ onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
+ }
+ } else {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onApplicationConnectionFailed(errorCode);
+ }
+ onDeviceSelected(null /* CastDevice */, null /* RouteInfo */);
+ if (mMediaRouter != null) {
+ Log.d(TAG, "onApplicationConnectionFailed(): Setting route to default");
+ mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute());
+ }
+ }
+ }
+
+ /**
+ * Loads a media. For this to succeed, you need to have successfully launched the application.
+ *
+ * @param media The media to be loaded
+ * @param autoPlay If <code>true</code>, playback starts after load
+ * @param position Where to start the playback (only used if autoPlay is <code>true</code>.
+ * Units is milliseconds.
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void loadMedia(MediaInfo media, boolean autoPlay, int position)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ loadMedia(media, autoPlay, position, null);
+ }
+
+ /**
+ * Loads a media. For this to succeed, you need to have successfully launched the application.
+ *
+ * @param media The media to be loaded
+ * @param autoPlay If <code>true</code>, playback starts after load
+ * @param position Where to start the playback (only used if autoPlay is <code>true</code>).
+ * Units is milliseconds.
+ * @param customData Optional {@link JSONObject} data to be passed to the cast device
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void loadMedia(MediaInfo media, boolean autoPlay, int position, JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ loadMedia(media, null, autoPlay, position, customData);
+ }
+
+ /**
+ * Loads a media. For this to succeed, you need to have successfully launched the application.
+ *
+ * @param media The media to be loaded
+ * @param activeTracks An array containing the list of track IDs to be set active for this
+ * media upon a successful load
+ * @param autoPlay If <code>true</code>, playback starts after load
+ * @param position Where to start the playback (only used if autoPlay is <code>true</code>).
+ * Units is milliseconds.
+ * @param customData Optional {@link JSONObject} data to be passed to the cast device
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void loadMedia(MediaInfo media, final long[] activeTracks, boolean autoPlay,
+ int position, JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "loadMedia");
+ checkConnectivity();
+ if (media == null) {
+ return;
+ }
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to load a video with no active media session");
+ throw new NoConnectionException();
+ }
+
+ Log.d(TAG, "remoteMediaPlayer.load() with media=" + media.getMetadata().getString(MediaMetadata.KEY_TITLE)
+ + ", position=" + position + ", autoplay=" + autoPlay);
+ remoteMediaPlayer.load(mApiClient, media, autoPlay, position, activeTracks, customData)
+ .setResultCallback(result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaLoadResult(result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Loads and optionally starts playback of a new queue of media items.
+ *
+ * @param items Array of items to load, in the order that they should be played. Must not be
+ * {@code null} or empty.
+ * @param startIndex The array index of the item in the {@code items} array that should be
+ * played first (i.e., it will become the currentItem).If {@code repeatMode}
+ * is {@link MediaStatus#REPEAT_MODE_REPEAT_OFF} playback will end when the
+ * last item in the array is played.
+ * <p>
+ * This may be useful for continuation scenarios where the user was already
+ * using the sender application and in the middle decides to cast. This lets
+ * the sender application avoid mapping between the local and remote queue
+ * positions and/or avoid issuing an extra request to update the queue.
+ * <p>
+ * This value must be less than the length of {@code items}.
+ * @param repeatMode The repeat playback mode for the queue. One of
+ * {@link MediaStatus#REPEAT_MODE_REPEAT_OFF},
+ * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL},
+ * {@link MediaStatus#REPEAT_MODE_REPEAT_SINGLE} and
+ * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE}.
+ * @param customData Custom application-specific data to pass along with the request, may be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void queueLoad(final MediaQueueItem[] items, final int startIndex, final int repeatMode,
+ final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "queueLoad");
+ checkConnectivity();
+ if (items == null || items.length == 0) {
+ return;
+ }
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to queue one or more videos with no active media session");
+ throw new NoConnectionException();
+ }
+ Log.d(TAG, "remoteMediaPlayer.queueLoad() with " + items.length + "items, starting at "
+ + startIndex);
+ remoteMediaPlayer
+ .queueLoad(mApiClient, items, startIndex, repeatMode, customData)
+ .setResultCallback(result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_LOAD,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Inserts a list of new media items into the queue.
+ *
+ * @param itemsToInsert List of items to insert into the queue, in the order that they should be
+ * played. The itemId field of the items should be unassigned or the
+ * request will fail with an INVALID_PARAMS error. Must not be {@code null}
+ * or empty.
+ * @param insertBeforeItemId ID of the item that will be located immediately after the inserted
+ * list. If the value is {@link MediaQueueItem#INVALID_ITEM_ID} or
+ * invalid, the inserted list will be appended to the end of the
+ * queue.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ * @throws IllegalArgumentException
+ */
+ public void queueInsertItems(final MediaQueueItem[] itemsToInsert, final int insertBeforeItemId,
+ final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "queueInsertItems");
+ checkConnectivity();
+ if (itemsToInsert == null || itemsToInsert.length == 0) {
+ throw new IllegalArgumentException("items cannot be empty or null");
+ }
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to insert into queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueInsertItems(mApiClient, itemsToInsert, insertBeforeItemId, customData)
+ .setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(
+ QUEUE_OPERATION_INSERT_ITEMS,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Updates properties of a subset of the existing items in the media queue.
+ *
+ * @param itemsToUpdate List of queue items to be updated. The items will retain the existing
+ * order and will be fully replaced with the ones provided, including the
+ * media information. Any other items currently in the queue will remain
+ * unchanged. The tracks information can not change once the item is loaded
+ * (if the item is the currentItem). If any of the items does not exist it
+ * will be ignored.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void queueUpdateItems(final MediaQueueItem[] itemsToUpdate, final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to update the queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueUpdateItems(mApiClient, itemsToUpdate, customData).setResultCallback(
+ result -> {
+ Log.d(TAG, "queueUpdateItems() " + result.getStatus() + result.getStatus()
+ .isSuccess());
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_UPDATE_ITEMS,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Plays the item with {@code itemId} in the queue.
+ * <p>
+ * If {@code itemId} is not found in the queue, this method will report success without sending
+ * a request to the receiver.
+ *
+ * @param itemId The ID of the item to which to jump.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ * @throws IllegalArgumentException
+ */
+ public void queueJumpToItem(int itemId, final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException,
+ IllegalArgumentException {
+ checkConnectivity();
+ if (itemId == MediaQueueItem.INVALID_ITEM_ID) {
+ throw new IllegalArgumentException("itemId is not valid");
+ }
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to jump in a queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueJumpToItem(mApiClient, itemId, customData).setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_JUMP,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Removes a list of items from the queue. If the remaining queue is empty, the media session
+ * will be terminated.
+ *
+ * @param itemIdsToRemove The list of media item IDs to remove. Must not be {@code null} or
+ * empty.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ * @throws IllegalArgumentException
+ */
+ public void queueRemoveItems(final int[] itemIdsToRemove, final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException,
+ IllegalArgumentException {
+ Log.d(TAG, "queueRemoveItems");
+ checkConnectivity();
+ if (itemIdsToRemove == null || itemIdsToRemove.length == 0) {
+ throw new IllegalArgumentException("itemIds cannot be empty or null");
+ }
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to remove items from queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueRemoveItems(mApiClient, itemIdsToRemove, customData).setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REMOVE_ITEMS,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Removes the item with {@code itemId} from the queue.
+ * <p>
+ * If {@code itemId} is not found in the queue, this method will silently return without sending
+ * a request to the receiver. A {@code itemId} may not be in the queue because it wasn't
+ * originally in the queue, or it was removed by another sender.
+ *
+ * @param itemId The ID of the item to be removed.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ * @throws IllegalArgumentException
+ */
+ public void queueRemoveItem(final int itemId, final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException,
+ IllegalArgumentException {
+ Log.d(TAG, "queueRemoveItem");
+ checkConnectivity();
+ if (itemId == MediaQueueItem.INVALID_ITEM_ID) {
+ throw new IllegalArgumentException("itemId is invalid");
+ }
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to remove an item from queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueRemoveItem(mApiClient, itemId, customData).setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REMOVE_ITEM,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Reorder a list of media items in the queue.
+ *
+ * @param itemIdsToReorder The list of media item IDs to reorder, in the new order. Any other
+ * items currently in the queue will maintain their existing order. The
+ * list will be inserted just before the item specified by
+ * {@code insertBeforeItemId}, or at the end of the queue if
+ * {@code insertBeforeItemId} is {@link MediaQueueItem#INVALID_ITEM_ID}.
+ * <p>
+ * For example:
+ * <p>
+ * If insertBeforeItemId is not specified <br>
+ * Existing queue: "A","D","G","H","B","E" <br>
+ * itemIds: "D","H","B" <br>
+ * New Order: "A","G","E","D","H","B" <br>
+ * <p>
+ * If insertBeforeItemId is "A" <br>
+ * Existing queue: "A","D","G","H","B" <br>
+ * itemIds: "D","H","B" <br>
+ * New Order: "D","H","B","A","G","E" <br>
+ * <p>
+ * If insertBeforeItemId is "G" <br>
+ * Existing queue: "A","D","G","H","B" <br>
+ * itemIds: "D","H","B" <br>
+ * New Order: "A","D","H","B","G","E" <br>
+ * <p>
+ * If any of the items does not exist it will be ignored.
+ * Must not be {@code null} or empty.
+ * @param insertBeforeItemId ID of the item that will be located immediately after the reordered
+ * list. If set to {@link MediaQueueItem#INVALID_ITEM_ID}, the
+ * reordered list will be appended at the end of the queue.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void queueReorderItems(final int[] itemIdsToReorder, final int insertBeforeItemId,
+ final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException,
+ IllegalArgumentException {
+ Log.d(TAG, "queueReorderItems");
+ checkConnectivity();
+ if (itemIdsToReorder == null || itemIdsToReorder.length == 0) {
+ throw new IllegalArgumentException("itemIdsToReorder cannot be empty or null");
+ }
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to reorder items in a queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueReorderItems(mApiClient, itemIdsToReorder, insertBeforeItemId, customData)
+ .setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REORDER,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Moves the item with {@code itemId} to a new position in the queue.
+ * <p>
+ * If {@code itemId} is not found in the queue, either because it wasn't there originally or it
+ * was removed by another sender before calling this function, this function will silently
+ * return without sending a request to the receiver.
+ *
+ * @param itemId The ID of the item to be moved.
+ * @param newIndex The new index of the item. If the value is negative, an error will be
+ * returned. If the value is out of bounds, or becomes out of bounds because the
+ * queue was shortened by another sender while this request is in progress, the
+ * item will be moved to the end of the queue.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void queueMoveItemToNewIndex(int itemId, int newIndex, final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "queueMoveItemToNewIndex");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to mote item to new index with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueMoveItemToNewIndex(mApiClient, itemId, newIndex, customData)
+ .setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_MOVE,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Appends a new media item to the end of the queue.
+ *
+ * @param item The item to append. Must not be {@code null}.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void queueAppendItem(MediaQueueItem item, final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "queueAppendItem");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to append item with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueAppendItem(mApiClient, item, customData)
+ .setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_APPEND,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Jumps to the next item in the queue.
+ *
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void queueNext(final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "queueNext");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to update the queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueNext(mApiClient, customData).setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_NEXT,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Jumps to the previous item in the queue.
+ *
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void queuePrev(final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "queuePrev");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to update the queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queuePrev(mApiClient, customData).setResultCallback(
+ result -> {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_PREV,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Inserts an item in the queue and starts the playback of that newly inserted item. It is
+ * assumed that we are inserting before the "current item"
+ *
+ * @param item The item to be inserted
+ * @param insertBeforeItemId ID of the item that will be located immediately after the inserted
+ * and is assumed to be the "current item"
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ * @throws IllegalArgumentException
+ */
+ public void queueInsertBeforeCurrentAndPlay(MediaQueueItem item, int insertBeforeItemId,
+ final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "queueInsertBeforeCurrentAndPlay");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to insert into queue with no active media session");
+ throw new NoConnectionException();
+ }
+ if (item == null || insertBeforeItemId == MediaQueueItem.INVALID_ITEM_ID) {
+ throw new IllegalArgumentException(
+ "item cannot be empty or insertBeforeItemId cannot be invalid");
+ }
+ remoteMediaPlayer.queueInsertItems(mApiClient, new MediaQueueItem[]{item},
+ insertBeforeItemId, customData).setResultCallback(
+ result -> {
+ if (result.getStatus().isSuccess()) {
+
+ try {
+ queuePrev(customData);
+ } catch (TransientNetworkDisconnectionException |
+ NoConnectionException e) {
+ Log.e(TAG, "queuePrev() Failed to skip to previous", e);
+ }
+ }
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_INSERT_ITEMS,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Sets the repeat mode of the queue.
+ *
+ * @param repeatMode The repeat playback mode for the queue.
+ * @param customData Custom application-specific data to pass along with the request. May be
+ * {@code null}.
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void queueSetRepeatMode(final int repeatMode, final JSONObject customData)
+ throws TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "queueSetRepeatMode");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to update the queue with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer
+ .queueSetRepeatMode(mApiClient, repeatMode, customData).setResultCallback(
+ result -> {
+ if (!result.getStatus().isSuccess()) {
+ Log.d(TAG, "Failed with status: " + result.getStatus());
+ }
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueOperationResult(QUEUE_OPERATION_SET_REPEAT,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Plays the loaded media.
+ *
+ * @param position Where to start the playback. Units is milliseconds.
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void play(int position) throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ Log.d(TAG, "attempting to play media at position " + position + " seconds");
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to play a video with no active media session");
+ throw new NoConnectionException();
+ }
+ seekAndPlay(position);
+ }
+
+ /**
+ * Resumes the playback from where it was left (can be the beginning).
+ *
+ * @param customData Optional {@link JSONObject} data to be passed to the cast device
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void play(JSONObject customData) throws
+ TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "play(customData)");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to play a video with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer.play(mApiClient, customData)
+ .setResultCallback(result -> {
+ if (!result.getStatus().isSuccess()) {
+ onFailed(R.string.cast_failed_to_play,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Resumes the playback from where it was left (can be the beginning).
+ *
+ * @throws CastException
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void play() throws CastException, TransientNetworkDisconnectionException,
+ NoConnectionException {
+ play(null);
+ }
+
+ /**
+ * Stops the playback of media/stream
+ *
+ * @param customData Optional {@link JSONObject}
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void stop(JSONObject customData) throws
+ TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "stop()");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to stop a stream with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer.stop(mApiClient, customData).setResultCallback(
+ result -> {
+ if (!result.getStatus().isSuccess()) {
+ onFailed(R.string.cast_failed_to_stop,
+ result.getStatus().getStatusCode());
+ }
+ }
+ );
+ }
+
+ /**
+ * Stops the playback of media/stream
+ *
+ * @throws CastException
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void stop() throws CastException,
+ TransientNetworkDisconnectionException, NoConnectionException {
+ stop(null);
+ }
+
+ /**
+ * Pauses the playback.
+ *
+ * @throws CastException
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void pause() throws CastException, TransientNetworkDisconnectionException,
+ NoConnectionException {
+ pause(null);
+ }
+
+ /**
+ * Pauses the playback.
+ *
+ * @param customData Optional {@link JSONObject} data to be passed to the cast device
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void pause(JSONObject customData) throws
+ TransientNetworkDisconnectionException, NoConnectionException {
+ Log.d(TAG, "attempting to pause media");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to pause a video with no active media session");
+ throw new NoConnectionException();
+ }
+ remoteMediaPlayer.pause(mApiClient, customData)
+ .setResultCallback(result -> {
+ if (!result.getStatus().isSuccess()) {
+ onFailed(R.string.cast_failed_to_pause,
+ result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Seeks to the given point without changing the state of the player, i.e. after seek is
+ * completed, it resumes what it was doing before the start of seek.
+ *
+ * @param position in milliseconds
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void seek(int position) throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ Log.d(TAG, "attempting to seek media");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to seek a video with no active media session");
+ throw new NoConnectionException();
+ }
+ Log.d(TAG, "remoteMediaPlayer.seek() to position " + position);
+ remoteMediaPlayer.seek(mApiClient,
+ position,
+ RESUME_STATE_UNCHANGED).
+ setResultCallback(result -> {
+ if (!result.getStatus().isSuccess()) {
+ onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Fast forwards the media by the given amount. If {@code lengthInMillis} is negative, it
+ * rewinds the media.
+ *
+ * @param lengthInMillis The amount to fast forward the media, given in milliseconds
+ * @throws TransientNetworkDisconnectionException
+ * @throws NoConnectionException
+ */
+ public void forward(int lengthInMillis) throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ Log.d(TAG, "forward(): attempting to forward media by " + lengthInMillis);
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to seek a video with no active media session");
+ throw new NoConnectionException();
+ }
+ long position = remoteMediaPlayer.getApproximateStreamPosition() + lengthInMillis;
+ seek((int) position);
+ }
+
+ /**
+ * Seeks to the given point and starts playback regardless of the starting state.
+ *
+ * @param position in milliseconds
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void seekAndPlay(int position) throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ Log.d(TAG, "attempting to seek media");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ Log.e(TAG, "Trying to seekAndPlay a video with no active media session");
+ throw new NoConnectionException();
+ }
+ Log.d(TAG, "remoteMediaPlayer.seek() to position " + position + "and play");
+ remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_PLAY)
+ .setResultCallback(result -> {
+ if (!result.getStatus().isSuccess()) {
+ onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode());
+ }
+ });
+ }
+
+ /**
+ * Toggles the playback of the media.
+ *
+ * @throws CastException
+ * @throws NoConnectionException
+ * @throws TransientNetworkDisconnectionException
+ */
+ public void togglePlayback() throws CastException, TransientNetworkDisconnectionException,
+ NoConnectionException {
+ checkConnectivity();
+ boolean isPlaying = isRemoteMediaPlaying();
+ if (isPlaying) {
+ pause();
+ } else {
+ if (state == MediaStatus.PLAYER_STATE_IDLE
+ && idleReason == MediaStatus.IDLE_REASON_FINISHED) {
+ loadMedia(getRemoteMediaInformation(), true, 0);
+ } else {
+ play();
+ }
+ }
+ }
+
+ private void attachMediaChannel() throws TransientNetworkDisconnectionException,
+ NoConnectionException {
+ Log.d(TAG, "attachMediaChannel()");
+ checkConnectivity();
+ if (remoteMediaPlayer == null) {
+ remoteMediaPlayer = new RemoteMediaPlayer();
+
+ remoteMediaPlayer.setOnStatusUpdatedListener(
+ () -> {
+ Log.d(TAG, "RemoteMediaPlayer::onStatusUpdated() is reached");
+ CastManager.this.onRemoteMediaPlayerStatusUpdated();
+ }
+ );
+
+ remoteMediaPlayer.setOnPreloadStatusUpdatedListener(
+ () -> {
+ Log.d(TAG, "RemoteMediaPlayer::onPreloadStatusUpdated() is reached");
+ CastManager.this.onRemoteMediaPreloadStatusUpdated();
+ });
+
+
+ remoteMediaPlayer.setOnMetadataUpdatedListener(
+ () -> {
+ Log.d(TAG, "RemoteMediaPlayer::onMetadataUpdated() is reached");
+ CastManager.this.onRemoteMediaPlayerMetadataUpdated();
+ }
+ );
+
+ remoteMediaPlayer.setOnQueueStatusUpdatedListener(
+ () -> {
+ Log.d(TAG, "RemoteMediaPlayer::onQueueStatusUpdated() is reached");
+ mediaStatus = remoteMediaPlayer.getMediaStatus();
+ if (mediaStatus != null
+ && mediaStatus.getQueueItems() != null) {
+ List<MediaQueueItem> queueItems = mediaStatus
+ .getQueueItems();
+ int itemId = mediaStatus.getCurrentItemId();
+ MediaQueueItem item = mediaStatus
+ .getQueueItemById(itemId);
+ int repeatMode = mediaStatus.getQueueRepeatMode();
+ onQueueUpdated(queueItems, item, repeatMode, false);
+ } else {
+ onQueueUpdated(null, null,
+ MediaStatus.REPEAT_MODE_REPEAT_OFF, false);
+ }
+ });
+
+ }
+ try {
+ Log.d(TAG, "Registering MediaChannel namespace");
+ Cast.CastApi.setMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace(),
+ remoteMediaPlayer);
+ } catch (IOException | IllegalStateException e) {
+ Log.e(TAG, "attachMediaChannel()", e);
+ }
+ }
+
+ private void reattachMediaChannel() {
+ if (remoteMediaPlayer != null && mApiClient != null) {
+ try {
+ Log.d(TAG, "Registering MediaChannel namespace");
+ Cast.CastApi.setMessageReceivedCallbacks(mApiClient,
+ remoteMediaPlayer.getNamespace(), remoteMediaPlayer);
+ } catch (IOException | IllegalStateException e) {
+ Log.e(TAG, "reattachMediaChannel()", e);
+ }
+ }
+ }
+
+ private void detachMediaChannel() {
+ Log.d(TAG, "trying to detach media channel");
+ if (remoteMediaPlayer != null) {
+ try {
+ Cast.CastApi.removeMessageReceivedCallbacks(mApiClient,
+ remoteMediaPlayer.getNamespace());
+ } catch (IOException | IllegalStateException e) {
+ Log.e(TAG, "detachMediaChannel()", e);
+ }
+ remoteMediaPlayer = null;
+ }
+ }
+
+ /**
+ * Returns the playback status of the remote device.
+ *
+ * @return Returns one of the values
+ * <ul>
+ * <li> <code>MediaStatus.PLAYER_STATE_UNKNOWN</code></li>
+ * <li> <code>MediaStatus.PLAYER_STATE_IDLE</code></li>
+ * <li> <code>MediaStatus.PLAYER_STATE_PLAYING</code></li>
+ * <li> <code>MediaStatus.PLAYER_STATE_PAUSED</code></li>
+ * <li> <code>MediaStatus.PLAYER_STATE_BUFFERING</code></li>
+ * </ul>
+ */
+ public int getPlaybackStatus() {
+ return state;
+ }
+
+ /**
+ * Returns the latest retrieved value for the {@link MediaStatus}. This value is updated
+ * whenever the onStatusUpdated callback is called.
+ */
+ public final MediaStatus getMediaStatus() {
+ return mediaStatus;
+ }
+
+ /**
+ * Returns the Idle reason, defined in <code>MediaStatus.IDLE_*</code>. Note that the returned
+ * value is only meaningful if the status is truly <code>MediaStatus.PLAYER_STATE_IDLE
+ * </code>
+ *
+ * <p>Possible values are:
+ * <ul>
+ * <li>IDLE_REASON_NONE</li>
+ * <li>IDLE_REASON_FINISHED</li>
+ * <li>IDLE_REASON_CANCELED</li>
+ * <li>IDLE_REASON_INTERRUPTED</li>
+ * <li>IDLE_REASON_ERROR</li>
+ * </ul>
+ */
+ public int getIdleReason() {
+ return idleReason;
+ }
+
+ private void onMessageSendFailed(int errorCode) {
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onDataMessageSendFailed(errorCode);
+ }
+ }
+
+ /*
+ * This is called by onStatusUpdated() of the RemoteMediaPlayer
+ */
+ private void onRemoteMediaPlayerStatusUpdated() {
+ Log.d(TAG, "onRemoteMediaPlayerStatusUpdated() reached");
+ if (mApiClient == null || remoteMediaPlayer == null) {
+ Log.d(TAG, "mApiClient or remoteMediaPlayer is null, so will not proceed");
+ return;
+ }
+ mediaStatus = remoteMediaPlayer.getMediaStatus();
+ if (mediaStatus == null) {
+ Log.d(TAG, "MediaStatus is null, so will not proceed");
+ return;
+ } else {
+ List<MediaQueueItem> queueItems = mediaStatus.getQueueItems();
+ if (queueItems != null) {
+ int itemId = mediaStatus.getCurrentItemId();
+ MediaQueueItem item = mediaStatus.getQueueItemById(itemId);
+ int repeatMode = mediaStatus.getQueueRepeatMode();
+ onQueueUpdated(queueItems, item, repeatMode, false);
+ } else {
+ onQueueUpdated(null, null, MediaStatus.REPEAT_MODE_REPEAT_OFF, false);
+ }
+ state = mediaStatus.getPlayerState();
+ idleReason = mediaStatus.getIdleReason();
+
+ if (state == MediaStatus.PLAYER_STATE_PLAYING) {
+ Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = playing");
+ } else if (state == MediaStatus.PLAYER_STATE_PAUSED) {
+ Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = paused");
+ } else if (state == MediaStatus.PLAYER_STATE_IDLE) {
+ Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = IDLE with reason: "
+ + idleReason);
+ if (idleReason == MediaStatus.IDLE_REASON_ERROR) {
+ // something bad happened on the cast device
+ Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): IDLE reason = ERROR");
+ onFailed(R.string.cast_failed_receiver_player_error, NO_STATUS_CODE);
+ }
+ } else if (state == MediaStatus.PLAYER_STATE_BUFFERING) {
+ Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = buffering");
+ } else {
+ Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = unknown");
+ }
+ }
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onRemoteMediaPlayerStatusUpdated();
+ }
+ if (mediaStatus != null) {
+ double volume = mediaStatus.getStreamVolume();
+ boolean isMute = mediaStatus.isMute();
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onStreamVolumeChanged(volume, isMute);
+ }
+ }
+ }
+
+ private void onRemoteMediaPreloadStatusUpdated() {
+ MediaQueueItem item = null;
+ mediaStatus = remoteMediaPlayer.getMediaStatus();
+ if (mediaStatus != null) {
+ item = mediaStatus.getQueueItemById(mediaStatus.getPreloadedItemId());
+ }
+ preLoadingItem = item;
+ Log.d(TAG, "onRemoteMediaPreloadStatusUpdated() " + item);
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onRemoteMediaPreloadStatusUpdated(item);
+ }
+ }
+
+ public MediaQueueItem getPreLoadingItem() {
+ return preLoadingItem;
+ }
+
+ /*
+ * This is called by onQueueStatusUpdated() of RemoteMediaPlayer
+ */
+ private void onQueueUpdated(List<MediaQueueItem> queueItems, MediaQueueItem item,
+ int repeatMode, boolean shuffle) {
+ Log.d(TAG, "onQueueUpdated() reached");
+ Log.d(TAG, String.format("Queue Items size: %d, Item: %s, Repeat Mode: %d, Shuffle: %s",
+ queueItems == null ? 0 : queueItems.size(), item, repeatMode, shuffle));
+ if (queueItems != null) {
+ mediaQueue = new MediaQueue(new CopyOnWriteArrayList<>(queueItems), item, shuffle,
+ repeatMode);
+ } else {
+ mediaQueue = new MediaQueue(new CopyOnWriteArrayList<>(), null, false,
+ MediaStatus.REPEAT_MODE_REPEAT_OFF);
+ }
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onMediaQueueUpdated(queueItems, item, repeatMode, shuffle);
+ }
+ }
+
+ /*
+ * This is called by onMetadataUpdated() of RemoteMediaPlayer
+ */
+ public void onRemoteMediaPlayerMetadataUpdated() {
+ Log.d(TAG, "onRemoteMediaPlayerMetadataUpdated() reached");
+ for (CastConsumer consumer : castConsumers) {
+ consumer.onRemoteMediaPlayerMetadataUpdated();
+ }
+ }
+
+ /**
+ * Registers a {@link CastConsumer} interface with this class.
+ * Registered listeners will be notified of changes to a variety of
+ * lifecycle and media status changes through the callbacks that the interface provides.
+ *
+ * @see DefaultCastConsumer
+ */
+ public synchronized void addCastConsumer(CastConsumer listener) {
+ if (listener != null) {
+ addBaseCastConsumer(listener);
+ castConsumers.add(listener);
+ Log.d(TAG, "Successfully added the new CastConsumer listener " + listener);
+ }
+ }
+
+ /**
+ * Unregisters a {@link CastConsumer}.
+ */
+ public synchronized void removeCastConsumer(CastConsumer listener) {
+ if (listener != null) {
+ removeBaseCastConsumer(listener);
+ castConsumers.remove(listener);
+ }
+ }
+
+ @Override
+ protected void onDeviceUnselected() {
+ detachMediaChannel();
+ //removeDataChannel();
+ state = MediaStatus.PLAYER_STATE_IDLE;
+ mediaStatus = null;
+ }
+
+ @Override
+ protected Cast.CastOptions.Builder getCastOptionBuilder(CastDevice device) {
+ Cast.CastOptions.Builder builder = new Cast.CastOptions.Builder(mSelectedCastDevice, new CastListener());
+ if (isFeatureEnabled(CastConfiguration.FEATURE_DEBUGGING)) {
+ builder.setVerboseLoggingEnabled(true);
+ }
+ return builder;
+ }
+
+ @Override
+ public void onConnectionFailed(ConnectionResult result) {
+ super.onConnectionFailed(result);
+ state = MediaStatus.PLAYER_STATE_IDLE;
+ mediaStatus = null;
+ }
+
+ @Override
+ public void onDisconnected(boolean stopAppOnExit, boolean clearPersistedConnectionData,
+ boolean setDefaultRoute) {
+ super.onDisconnected(stopAppOnExit, clearPersistedConnectionData, setDefaultRoute);
+ state = MediaStatus.PLAYER_STATE_IDLE;
+ mediaStatus = null;
+ mediaQueue = null;
+ }
+
+ class CastListener extends Cast.Listener {
+
+ /*
+ * (non-Javadoc)
+ * @see com.google.android.gms.cast.Cast.Listener#onApplicationDisconnected (int)
+ */
+ @Override
+ public void onApplicationDisconnected(int statusCode) {
+ CastManager.this.onApplicationDisconnected(statusCode);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.google.android.gms.cast.Cast.Listener#onApplicationStatusChanged ()
+ */
+ @Override
+ public void onApplicationStatusChanged() {
+ CastManager.this.onApplicationStatusChanged();
+ }
+
+ @Override
+ public void onVolumeChanged() {
+ CastManager.this.onDeviceVolumeChanged();
+ }
+ }
+
+ @Override
+ public void onFailed(int resourceId, int statusCode) {
+ Log.d(TAG, "onFailed: " + mContext.getString(resourceId) + ", code: " + statusCode);
+ super.onFailed(resourceId, statusCode);
+ }
+
+ /**
+ * Clients can call this method to delegate handling of the volume. Clients should override
+ * {@code dispatchEvent} and call this method:
+ * <pre>
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (mCastManager.onDispatchVolumeKeyEvent(event, VOLUME_DELTA)) {
+ return true;
+ }
+ return super.dispatchKeyEvent(event);
+ }
+ * </pre>
+ * @param event The dispatched event.
+ * @param volumeDelta The amount by which volume should be increased or decreased in each step
+ * @return <code>true</code> if volume is handled by the library, <code>false</code> otherwise.
+ */
+ public boolean onDispatchVolumeKeyEvent(KeyEvent event, double volumeDelta) {
+ if (isConnected()) {
+ boolean isKeyDown = event.getAction() == KeyEvent.ACTION_DOWN;
+ switch (event.getKeyCode()) {
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ return changeVolume(volumeDelta, isKeyDown);
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ return changeVolume(-volumeDelta, isKeyDown);
+ }
+ }
+ return false;
+ }
+
+ private boolean changeVolume(double volumeIncrement, boolean isKeyDown) {
+ if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ && getPlaybackStatus() == MediaStatus.PLAYER_STATE_PLAYING
+ && isFeatureEnabled(CastConfiguration.FEATURE_LOCKSCREEN)) {
+ return false;
+ }
+
+ if (isKeyDown) {
+ try {
+ adjustDeviceVolume(volumeIncrement);
+ } catch (CastException | TransientNetworkDisconnectionException |
+ NoConnectionException e) {
+ Log.e(TAG, "Failed to change volume", e);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sets the volume step, i.e. the fraction by which volume will increase or decrease each time
+ * user presses the hard volume buttons on the device.
+ *
+ * @param volumeStep Should be a double between 0 and 1, inclusive.
+ */
+ public CastManager setVolumeStep(double volumeStep) {
+ if ((volumeStep > 1) || (volumeStep < 0)) {
+ throw new IllegalArgumentException("Volume Step should be between 0 and 1, inclusive");
+ }
+ this.volumeStep = volumeStep;
+ return this;
+ }
+
+ /**
+ * Returns the volume step. The default value is {@code DEFAULT_VOLUME_STEP}.
+ */
+ public double getVolumeStep() {
+ return volumeStep;
+ }
+
+ public final MediaQueue getMediaQueue() {
+ return mediaQueue;
+ }
+
+ /**
+ * Checks whether the selected Cast Device has the specified audio or video capabilities.
+ *
+ * @param capability capability from:
+ * <ul>
+ * <li>{@link CastDevice#CAPABILITY_AUDIO_IN}</li>
+ * <li>{@link CastDevice#CAPABILITY_AUDIO_OUT}</li>
+ * <li>{@link CastDevice#CAPABILITY_VIDEO_IN}</li>
+ * <li>{@link CastDevice#CAPABILITY_VIDEO_OUT}</li>
+ * </ul>
+ * @param defaultVal value to return whenever there's no device selected.
+ * @return {@code true} if the selected device has the specified capability,
+ * {@code false} otherwise.
+ */
+ public boolean hasCapability(final int capability, final boolean defaultVal) {
+ if (mSelectedCastDevice != null) {
+ return mSelectedCastDevice.hasCapability(capability);
+ } else {
+ return defaultVal;
+ }
+ }
+
+ /**
+ * Adds and wires up the Switchable Media Router cast button. It returns a reference to the
+ * {@link SwitchableMediaRouteActionProvider} associated with the button if the caller needs
+ * such reference. It is assumed that the enclosing
+ * {@link android.app.Activity} inherits (directly or indirectly) from
+ * {@link android.support.v7.app.AppCompatActivity}.
+ *
+ * @param menuItem MenuItem of the Media Router cast button.
+ */
+ public final SwitchableMediaRouteActionProvider addMediaRouterButton(MenuItem menuItem) {
+ SwitchableMediaRouteActionProvider mediaRouteActionProvider = (SwitchableMediaRouteActionProvider)
+ MenuItemCompat.getActionProvider(menuItem);
+ mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector);
+ if (mCastConfiguration.getMediaRouteDialogFactory() != null) {
+ mediaRouteActionProvider.setDialogFactory(mCastConfiguration.getMediaRouteDialogFactory());
+ }
+ return mediaRouteActionProvider;
+ }
+
+ /* (non-Javadoc)
+ * These methods startReconnectionService and stopReconnectionService simply override the ones
+ * from BaseCastManager with empty implementations because we handle the service ourselves, but
+ * need to allow BaseCastManager to save current network information.
+ */
+ @Override
+ protected void startReconnectionService(long mediaDurationLeft) {
+ // Do nothing
+ }
+
+ @Override
+ protected void stopReconnectionService() {
+ // Do nothing
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java
new file mode 100644
index 000000000..f0a7214c9
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java
@@ -0,0 +1,317 @@
+package de.danoeh.antennapod.core.cast;
+
+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.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedImage;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.playback.ExternalMedia;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+/**
+ * Helper functions for Cast support.
+ */
+public class 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";
+ public static final int EPISODE_NOTES_MAX_LENGTH = Integer.MAX_VALUE;
+
+ /**
+ * 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 || media instanceof ExternalMedia) {
+ return false;
+ }
+ if (media instanceof FeedMedia || media instanceof RemoteMedia){
+ String url = media.getStreamUrl();
+ if(url == null || url.isEmpty()){
+ return false;
+ }
+ 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}.
+ *
+ * Unless media.{@link FeedMedia#loadMetadata() loadMetadata()} has already been called,
+ * 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);
+ try{
+ media.loadMetadata();
+ } catch (Playable.PlayableException e) {
+ Log.e(TAG, "Unable to load FeedMedia metadata", e);
+ }
+ 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);
+ }
+ FeedImage image = feedItem.getImage();
+ if (image != null && !TextUtils.isEmpty(image.getDownload_url())) {
+ metadata.addImage(new WebImage(Uri.parse(image.getDownload_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(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());
+ }
+ }
+ String notes = null;
+ try {
+ notes = media.loadShownotes().call();
+ } catch (Exception e) {
+ Log.e(TAG, "Unable to load FeedMedia notes", e);
+ }
+ if (notes != null) {
+ if (notes.length() > EPISODE_NOTES_MAX_LENGTH) {
+ notes = notes.substring(0, EPISODE_NOTES_MAX_LENGTH);
+ }
+ metadata.putString(KEY_EPISODE_NOTES, notes);
+ }
+ // 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) {
+ try {
+ fMedia.loadMetadata();
+ 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);
+ }
+ } catch (Playable.PlayableException e) {
+ Log.e(TAG, "Unable to load FeedMedia metadata to compare with MediaInfo", e);
+ }
+ } else {
+ Log.d(TAG, "Unable to find in database a FeedMedia with id=" + mediaId);
+ }
+ }
+ if (result == null) {
+ FeedItem feedItem = DBReader.getFeedItem(metadata.getString(KEY_FEED_URL),
+ 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();
+ }
+ 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());
+ String notes = metadata.getString(KEY_EPISODE_NOTES);
+ if (!TextUtils.isEmpty(notes)) {
+ ((RemoteMedia) result).setNotes(notes);
+ }
+ Log.d(TAG, "Converted MediaInfo into RemoteMedia");
+ }
+ if (result.getDuration() == 0 && media.getStreamDuration() > 0) {
+ result.setDuration((int) media.getStreamDuration());
+ }
+ return result;
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they
+ * represent the same podcast episode.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link FeedMedia} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, FeedMedia media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
+ return false;
+ }
+ MediaMetadata metadata = info.getMetadata();
+ FeedItem fi = media.getItem();
+ if (fi == null || metadata == null ||
+ !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) {
+ return false;
+ }
+ Feed feed = fi.getFeed();
+ return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url());
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they
+ * represent the same podcast episode.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link RemoteMedia} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, RemoteMedia media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
+ return false;
+ }
+ MediaMetadata metadata = info.getMetadata();
+ return metadata != null &&
+ TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier()) &&
+ TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl());
+ }
+
+ /**
+ * Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they
+ * represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
+ * and want to avoid unnecessary conversions.
+ *
+ * @param info the {@link MediaInfo} object to be compared.
+ * @param media the {@link Playable} object to be compared.
+ * @return <true>true</true> if there's a match, <code>false</code> otherwise.
+ *
+ * @see RemoteMedia#equals(Object)
+ */
+ public static boolean matches(MediaInfo info, Playable media) {
+ if (info == null || media == null) {
+ return false;
+ }
+ if (media instanceof RemoteMedia) {
+ return matches(info, (RemoteMedia) media);
+ }
+ return media instanceof FeedMedia && matches(info, (FeedMedia) media);
+ }
+
+
+ //TODO Queue handling perhaps
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java b/core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
new file mode 100644
index 000000000..fe4183d54
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java
@@ -0,0 +1,10 @@
+package de.danoeh.antennapod.core.cast;
+
+import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumerImpl;
+
+public class DefaultCastConsumer extends VideoCastConsumerImpl implements CastConsumer {
+ @Override
+ public void onStreamVolumeChanged(double value, boolean isMute) {
+ // no-op
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java b/core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java
new file mode 100644
index 000000000..99f7b9496
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java
@@ -0,0 +1,347 @@
+package de.danoeh.antennapod.core.cast;
+
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+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 java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import de.danoeh.antennapod.core.feed.Chapter;
+import de.danoeh.antennapod.core.feed.Feed;
+import de.danoeh.antennapod.core.feed.FeedItem;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.util.ChapterUtils;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+/**
+ * Playable implementation for media on a Cast Device for which a local version of
+ * {@link de.danoeh.antennapod.core.feed.FeedMedia} hasn't been found.
+ */
+public class RemoteMedia implements Playable {
+ public static final String TAG = "RemoteMedia";
+
+ public static final int PLAYABLE_TYPE_REMOTE_MEDIA = 3;
+
+ private String downloadUrl;
+ private String itemIdentifier;
+ private String feedUrl;
+ private String feedTitle;
+ private String episodeTitle;
+ private String episodeLink;
+ private String feedAuthor;
+ private String imageUrl;
+ private String feedLink;
+ private String mime_type;
+ private Date pubDate;
+ private String notes;
+ private List<Chapter> chapters;
+ private int duration;
+ private int position;
+ private long lastPlayedTime;
+
+ public RemoteMedia(String downloadUrl, String itemId, String feedUrl, String feedTitle,
+ String episodeTitle, String episodeLink, String feedAuthor,
+ String imageUrl, String feedLink, String mime_type, Date pubDate) {
+ this.downloadUrl = downloadUrl;
+ this.itemIdentifier = itemId;
+ this.feedUrl = feedUrl;
+ this.feedTitle = feedTitle;
+ this.episodeTitle = episodeTitle;
+ this.episodeLink = episodeLink;
+ this.feedAuthor = feedAuthor;
+ this.imageUrl = imageUrl;
+ this.feedLink = feedLink;
+ this.mime_type = mime_type;
+ this.pubDate = pubDate;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+ public MediaInfo extractMediaInfo() {
+ MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
+
+ metadata.putString(MediaMetadata.KEY_TITLE, episodeTitle);
+ metadata.putString(MediaMetadata.KEY_SUBTITLE, feedTitle);
+ if (!TextUtils.isEmpty(imageUrl)) {
+ metadata.addImage(new WebImage(Uri.parse(imageUrl)));
+ }
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(pubDate);
+ metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
+ if (!TextUtils.isEmpty(feedAuthor)) {
+ metadata.putString(MediaMetadata.KEY_ARTIST, feedAuthor);
+ }
+ if (!TextUtils.isEmpty(feedUrl)) {
+ metadata.putString(CastUtils.KEY_FEED_URL, feedUrl);
+ }
+ if (!TextUtils.isEmpty(feedLink)) {
+ metadata.putString(CastUtils.KEY_FEED_WEBSITE, feedLink);
+ }
+ if (!TextUtils.isEmpty(itemIdentifier)) {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, itemIdentifier);
+ } else {
+ metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, downloadUrl);
+ }
+ if (!TextUtils.isEmpty(episodeLink)) {
+ metadata.putString(CastUtils.KEY_EPISODE_LINK, episodeLink);
+ }
+ String notes = this.notes;
+ if (notes != null) {
+ if (notes.length() > CastUtils.EPISODE_NOTES_MAX_LENGTH) {
+ notes = notes.substring(0, CastUtils.EPISODE_NOTES_MAX_LENGTH);
+ }
+ 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(downloadUrl)
+ .setContentType(mime_type)
+ .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
+ .setMetadata(metadata);
+ if (duration > 0) {
+ builder.setStreamDuration(duration);
+ }
+ return builder.build();
+ }
+
+ public String getEpisodeIdentifier() {
+ return itemIdentifier;
+ }
+
+ public String getFeedUrl() {
+ return feedUrl;
+ }
+
+ public FeedMedia lookForFeedMedia() {
+ FeedItem feedItem = DBReader.getFeedItem(feedUrl, itemIdentifier);
+ if (feedItem == null) {
+ return null;
+ }
+ return feedItem.getMedia();
+ }
+
+ @Override
+ public void writeToPreferences(SharedPreferences.Editor prefEditor) {
+ //it seems pointless to do it, since the session should be kept by the remote device.
+ }
+
+ @Override
+ public void loadMetadata() throws PlayableException {
+ //Already loaded
+ }
+
+ @Override
+ public void loadChapterMarks() {
+ ChapterUtils.loadChaptersFromStreamUrl(this);
+ }
+
+ @Override
+ public String getEpisodeTitle() {
+ return episodeTitle;
+ }
+
+ @Override
+ public List<Chapter> getChapters() {
+ return chapters;
+ }
+
+ @Override
+ public String getWebsiteLink() {
+ if (episodeLink != null) {
+ return episodeLink;
+ } else {
+ return feedUrl;
+ }
+ }
+
+ @Override
+ public String getPaymentLink() {
+ return null;
+ }
+
+ @Override
+ public String getFeedTitle() {
+ return feedTitle;
+ }
+
+ @Override
+ public Object getIdentifier() {
+ return itemIdentifier + "@" + feedUrl;
+ }
+
+ @Override
+ public int getDuration() {
+ return duration;
+ }
+
+ @Override
+ public int getPosition() {
+ return position;
+ }
+
+ @Override
+ public long getLastPlayedTime() {
+ return lastPlayedTime;
+ }
+
+ @Override
+ public MediaType getMediaType() {
+ return MediaType.fromMimeType(mime_type);
+ }
+
+ @Override
+ public String getLocalMediaUrl() {
+ return null;
+ }
+
+ @Override
+ public String getStreamUrl() {
+ return downloadUrl;
+ }
+
+ @Override
+ public boolean localFileAvailable() {
+ return false;
+ }
+
+ @Override
+ public boolean streamAvailable() {
+ return true;
+ }
+
+ @Override
+ public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp) {
+ //we're not saving playback information for this kind of items on preferences
+ setPosition(newPosition);
+ setLastPlayedTime(timestamp);
+ }
+
+ @Override
+ public void setPosition(int newPosition) {
+ position = newPosition;
+ }
+
+ @Override
+ public void setDuration(int newDuration) {
+ duration = newDuration;
+ }
+
+ @Override
+ public void setLastPlayedTime(long lastPlayedTimestamp) {
+ lastPlayedTime = lastPlayedTimestamp;
+ }
+
+ @Override
+ public void onPlaybackStart() {
+ // no-op
+ }
+
+ @Override
+ public void onPlaybackCompleted() {
+ // no-op
+ }
+
+ @Override
+ public int getPlayableType() {
+ return PLAYABLE_TYPE_REMOTE_MEDIA;
+ }
+
+ @Override
+ public void setChapters(List<Chapter> chapters) {
+ this.chapters = chapters;
+ }
+
+ @Override
+ public Uri getImageUri() {
+ if (imageUrl != null) {
+ return Uri.parse(imageUrl);
+ }
+ return null;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public Callable<String> loadShownotes() {
+ return () -> (notes != null) ? notes : "";
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(downloadUrl);
+ dest.writeString(itemIdentifier);
+ dest.writeString(feedUrl);
+ dest.writeString(feedTitle);
+ dest.writeString(episodeTitle);
+ dest.writeString(episodeLink);
+ dest.writeString(feedAuthor);
+ dest.writeString(imageUrl);
+ dest.writeString(feedLink);
+ dest.writeString(mime_type);
+ dest.writeLong(pubDate.getTime());
+ dest.writeString(notes);
+ dest.writeInt(duration);
+ dest.writeInt(position);
+ dest.writeLong(lastPlayedTime);
+ }
+
+ public static final Parcelable.Creator<RemoteMedia> CREATOR = new Parcelable.Creator<RemoteMedia>() {
+ @Override
+ public RemoteMedia createFromParcel(Parcel in) {
+ RemoteMedia result = new RemoteMedia(in.readString(), in.readString(), in.readString(),
+ in.readString(), in.readString(), in.readString(), in.readString(), in.readString(),
+ in.readString(), in.readString(), new Date(in.readLong()));
+ result.setNotes(in.readString());
+ result.setDuration(in.readInt());
+ result.setPosition(in.readInt());
+ result.setLastPlayedTime(in.readLong());
+ return result;
+ }
+
+ @Override
+ public RemoteMedia[] newArray(int size) {
+ return new RemoteMedia[size];
+ }
+ };
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof RemoteMedia) {
+ RemoteMedia rm = (RemoteMedia) other;
+ return TextUtils.equals(downloadUrl, rm.downloadUrl) &&
+ TextUtils.equals(feedUrl, rm.feedUrl) &&
+ TextUtils.equals(itemIdentifier, rm.itemIdentifier);
+ }
+ if (other instanceof FeedMedia) {
+ FeedMedia fm = (FeedMedia) other;
+ if (!TextUtils.equals(downloadUrl, fm.getStreamUrl())) {
+ return false;
+ }
+ FeedItem fi = fm.getItem();
+ if (fi == null || !TextUtils.equals(itemIdentifier, fi.getItemIdentifier())) {
+ return false;
+ }
+ Feed feed = fi.getFeed();
+ return feed != null && TextUtils.equals(feedUrl, feed.getDownload_url());
+ }
+ return false;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java b/core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
new file mode 100644
index 000000000..f063cf5e3
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java
@@ -0,0 +1,106 @@
+package de.danoeh.antennapod.core.cast;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.app.MediaRouteActionProvider;
+import android.support.v7.app.MediaRouteChooserDialogFragment;
+import android.support.v7.app.MediaRouteControllerDialogFragment;
+import android.support.v7.media.MediaRouter;
+import android.util.Log;
+
+/**
+ * <p>Action Provider that extends {@link MediaRouteActionProvider} and allows the client to
+ * disable completely the button by calling {@link #setEnabled(boolean)}.</p>
+ *
+ * <p>It is disabled by default, so if a client wants to initially have it enabled it must call
+ * <code>setEnabled(true)</code>.</p>
+ */
+public class SwitchableMediaRouteActionProvider extends MediaRouteActionProvider {
+ public static final String TAG = "SwitchblMediaRtActProv";
+
+ private static final String CHOOSER_FRAGMENT_TAG =
+ "android.support.v7.mediarouter:MediaRouteChooserDialogFragment";
+ private static final String CONTROLLER_FRAGMENT_TAG =
+ "android.support.v7.mediarouter:MediaRouteControllerDialogFragment";
+ private boolean enabled;
+
+ public SwitchableMediaRouteActionProvider(Context context) {
+ super(context);
+ enabled = false;
+ }
+
+ /**
+ * <p>Sets whether the Media Router button should be allowed to become visible or not.</p>
+ *
+ * <p>It's invisible by default.</p>
+ */
+ public void setEnabled(boolean newVal) {
+ enabled = newVal;
+ refreshVisibility();
+ }
+
+ @Override
+ public boolean isVisible() {
+ return enabled && super.isVisible();
+ }
+
+ @Override
+ public boolean onPerformDefaultAction() {
+ if (!super.onPerformDefaultAction()) {
+ // there is no button, but we should still show the dialog if it's the case.
+ if (!isVisible()) {
+ return false;
+ }
+ FragmentManager fm = getFragmentManager();
+ if (fm == null) {
+ return false;
+ }
+ MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute();
+ if (route.isDefault() || !route.matchesSelector(getRouteSelector())) {
+ if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
+ Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
+ return false;
+ }
+ MediaRouteChooserDialogFragment f =
+ getDialogFactory().onCreateChooserDialogFragment();
+ f.setRouteSelector(getRouteSelector());
+ f.show(fm, CHOOSER_FRAGMENT_TAG);
+ } else {
+ if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
+ Log.w(TAG, "showDialog(): Route controller dialog already showing!");
+ return false;
+ }
+ MediaRouteControllerDialogFragment f =
+ getDialogFactory().onCreateControllerDialogFragment();
+ f.show(fm, CONTROLLER_FRAGMENT_TAG);
+ }
+ return true;
+
+ } else {
+ return true;
+ }
+ }
+
+ private FragmentManager getFragmentManager() {
+ Activity activity = getActivity();
+ if (activity instanceof FragmentActivity) {
+ return ((FragmentActivity)activity).getSupportFragmentManager();
+ }
+ return null;
+ }
+
+ private Activity getActivity() {
+ // Gross way of unwrapping the Activity so we can get the FragmentManager
+ Context context = getContext();
+ while (context instanceof ContextWrapper) {
+ if (context instanceof Activity) {
+ return (Activity)context;
+ }
+ context = ((ContextWrapper)context).getBaseContext();
+ }
+ return null;
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
index 991499316..7f064fff3 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java
@@ -13,6 +13,7 @@ import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
+import de.danoeh.antennapod.core.cast.RemoteMedia;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
@@ -152,18 +153,7 @@ public class FeedMedia extends FeedFile implements Playable {
* Uses mimetype to determine the type of media.
*/
public MediaType getMediaType() {
- if (mime_type == null || mime_type.isEmpty()) {
- return MediaType.UNKNOWN;
- } else {
- if (mime_type.startsWith("audio")) {
- return MediaType.AUDIO;
- } else if (mime_type.startsWith("video")) {
- return MediaType.VIDEO;
- } else if (mime_type.equals("application/ogg")) {
- return MediaType.AUDIO;
- }
- }
- return MediaType.UNKNOWN;
+ return MediaType.fromMimeType(mime_type);
}
public void updateFromOther(FeedMedia other) {
@@ -579,4 +569,12 @@ public class FeedMedia extends FeedFile implements Playable {
hasEmbeddedPicture = Boolean.FALSE;
}
}
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof RemoteMedia) {
+ return o.equals(this);
+ }
+ return super.equals(o);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java b/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java
index 7b3cb829d..83ac031bf 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java
@@ -1,5 +1,20 @@
package de.danoeh.antennapod.core.feed;
+import android.text.TextUtils;
+
public enum MediaType {
- AUDIO, VIDEO, UNKNOWN
+ AUDIO, VIDEO, UNKNOWN;
+
+ public static MediaType fromMimeType(String mime_type) {
+ if (TextUtils.isEmpty(mime_type)) {
+ return MediaType.UNKNOWN;
+ } else if (mime_type.startsWith("audio")) {
+ return MediaType.AUDIO;
+ } else if (mime_type.startsWith("video")) {
+ return MediaType.VIDEO;
+ } else if (mime_type.equals("application/ogg")) {
+ return MediaType.AUDIO;
+ }
+ return MediaType.UNKNOWN;
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
index 569dfd2c4..b5bbb0350 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java
@@ -111,6 +111,7 @@ public class UserPreferences {
public static final String PREF_SONIC = "prefSonic";
public static final String PREF_STEREO_TO_MONO = "PrefStereoToMono";
public static final String PREF_NORMALIZER = "prefNormalizer";
+ public static final String PREF_CAST_ENABLED = "prefCast"; //Used for enabling Chromecast support
public static final int EPISODE_CLEANUP_QUEUE = -1;
public static final int EPISODE_CLEANUP_NULL = -2;
public static final int EPISODE_CLEANUP_DEFAULT = 0;
@@ -800,4 +801,11 @@ public class UserPreferences {
public static int readEpisodeCacheSize(String valueFromPrefs) {
return readEpisodeCacheSizeInternal(valueFromPrefs);
}
+
+ /**
+ * Evaluates whether Cast support (Chromecast, Audio Cast, etc) is enabled on the preferences.
+ */
+ public static boolean isCastEnabled() {
+ return prefs.getBoolean(PREF_CAST_ENABLED, false);
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
new file mode 100644
index 000000000..be80ea112
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java
@@ -0,0 +1,894 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.PowerManager;
+import android.support.annotation.NonNull;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.util.Pair;
+import android.view.SurfaceHolder;
+
+import org.antennapod.audio.MediaPlayer;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReentrantLock;
+
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
+import de.danoeh.antennapod.core.util.playback.AudioPlayer;
+import de.danoeh.antennapod.core.util.playback.IPlayer;
+import de.danoeh.antennapod.core.util.playback.Playable;
+import de.danoeh.antennapod.core.util.playback.VideoPlayer;
+
+/**
+ * Manages the MediaPlayer object of the PlaybackService.
+ */
+public class LocalPSMP extends PlaybackServiceMediaPlayer {
+ public static final String TAG = "LclPlaybackSvcMPlayer";
+
+ private final AudioManager audioManager;
+
+ private volatile PlayerStatus statusBeforeSeeking;
+ private volatile IPlayer mediaPlayer;
+ private volatile Playable media;
+
+ private volatile boolean stream;
+ private volatile MediaType mediaType;
+ private volatile AtomicBoolean startWhenPrepared;
+ private volatile boolean pausedBecauseOfTransientAudiofocusLoss;
+ private volatile Pair<Integer, Integer> videoSize;
+
+ /**
+ * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads
+ * have to wait until these operations have finished.
+ */
+ private final ReentrantLock playerLock;
+ private CountDownLatch seekLatch;
+
+ private final ThreadPoolExecutor executor;
+
+ public LocalPSMP(@NonNull Context context,
+ @NonNull PSMPCallback callback) {
+ super(context, callback);
+
+ this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ this.playerLock = new ReentrantLock();
+ this.startWhenPrepared = new AtomicBoolean(false);
+ executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<>(),
+ (r, executor) -> Log.d(TAG, "Rejected execution of runnable"));
+
+ mediaPlayer = null;
+ statusBeforeSeeking = null;
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ mediaType = MediaType.UNKNOWN;
+ videoSize = null;
+ }
+
+ /**
+ * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
+ * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
+ * not do anything.
+ * Whether playback starts immediately depends on the given parameters. See below for more details.
+ * <p/>
+ * States:
+ * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
+ * <p/>
+ * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
+ * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
+ * <p/>
+ * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
+ * will enter the ERROR state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ *
+ * @param playable The Playable object that is supposed to be played. This parameter must not be null.
+ * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via
+ * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
+ * the Android MediaPlayer via getStreamUrl.
+ * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the
+ * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared
+ * for playback immediately (see 'prepareImmediately' parameter for more details)
+ * @param prepareImmediately Set to true if the method should also prepare the episode for playback.
+ */
+ @Override
+ public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ Log.d(TAG, "playMediaObject(...)");
+ executor.submit(() -> {
+ playerLock.lock();
+ try {
+ playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ throw e;
+ } finally {
+ playerLock.unlock();
+ }
+ });
+ }
+
+ /**
+ * 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.
+ * <p/>
+ * This method requires the playerLock and is executed on the caller's thread.
+ *
+ * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean)
+ */
+ private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ if (!playerLock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("method requires playerLock");
+ }
+
+
+ if (media != null) {
+ if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())
+ && playerStatus == PlayerStatus.PLAYING) {
+ // episode is already playing -> ignore method call
+ Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
+ return;
+ } else {
+ // stop playback of this episode
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) {
+ mediaPlayer.stop();
+ }
+ // set temporarily to pause in order to update list with current position
+ if (playerStatus == PlayerStatus.PLAYING) {
+ setPlayerStatus(PlayerStatus.PAUSED, media);
+ }
+
+ smartMarkAsPlayed(media);
+
+ setPlayerStatus(PlayerStatus.INDETERMINATE, null);
+ }
+ }
+
+ this.media = playable;
+ this.stream = stream;
+ this.mediaType = media.getMediaType();
+ this.videoSize = null;
+ createMediaPlayer();
+ LocalPSMP.this.startWhenPrepared.set(startWhenPrepared);
+ setPlayerStatus(PlayerStatus.INITIALIZING, media);
+ try {
+ media.loadMetadata();
+ callback.reloadUI();
+ if (stream) {
+ mediaPlayer.setDataSource(media.getStreamUrl());
+ } else {
+ mediaPlayer.setDataSource(media.getLocalMediaUrl());
+ }
+ setPlayerStatus(PlayerStatus.INITIALIZED, media);
+
+ if (prepareImmediately) {
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ mediaPlayer.prepare();
+ onPrepared(startWhenPrepared);
+ }
+
+ } catch (Playable.PlayableException | IOException | IllegalStateException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ }
+ }
+
+ /**
+ * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
+ * nothing will happen.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ @Override
+ public void resume() {
+ executor.submit(() -> {
+ playerLock.lock();
+ resumeSync();
+ playerLock.unlock();
+ });
+ }
+
+ private void resumeSync() {
+ if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
+ int focusGained = audioManager.requestAudioFocus(
+ audioFocusChangeListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+ if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ acquireWifiLockIfNecessary();
+ float speed = 1.0f;
+ try {
+ speed = Float.parseFloat(UserPreferences.getPlaybackSpeed());
+ } catch(NumberFormatException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ UserPreferences.setPlaybackSpeed(String.valueOf(speed));
+ }
+ setSpeed(speed);
+ setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume());
+
+ if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
+ int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
+ media.getPosition(),
+ media.getLastPlayedTime());
+ seekToSync(newPosition);
+ }
+ mediaPlayer.start();
+
+ setPlayerStatus(PlayerStatus.PLAYING, media);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ media.onPlaybackStart();
+
+ } else {
+ Log.e(TAG, "Failed to request audio focus");
+ }
+ } else {
+ Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus);
+ }
+ }
+
+
+ /**
+ * Saves the current position and pauses playback. Note that, if audiofocus
+ * is abandoned, the lockscreen controls will also disapear.
+ * <p/>
+ * This method is executed on an internal executor service.
+ *
+ * @param abandonFocus is true if the service should release audio focus
+ * @param reinit is true if service should reinit after pausing if the media
+ * file is being streamed
+ */
+ @Override
+ public void pause(final boolean abandonFocus, final boolean reinit) {
+ executor.submit(() -> {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
+ if (playerStatus == PlayerStatus.PLAYING) {
+ Log.d(TAG, "Pausing playback.");
+ mediaPlayer.pause();
+ setPlayerStatus(PlayerStatus.PAUSED, media);
+
+ if (abandonFocus) {
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ }
+ if (stream && reinit) {
+ reinit();
+ }
+ } else {
+ Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state");
+ }
+
+ playerLock.unlock();
+ });
+ }
+
+ /**
+ * Prepares media player for playback if the service is in the INITALIZED
+ * state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ @Override
+ public void prepare() {
+ executor.submit(() -> {
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.INITIALIZED) {
+ Log.d(TAG, "Preparing media player");
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ try {
+ mediaPlayer.prepare();
+ onPrepared(startWhenPrepared.get());
+ } catch (IOException e) {
+ e.printStackTrace();
+ setPlayerStatus(PlayerStatus.ERROR, null);
+ }
+ }
+ playerLock.unlock();
+
+ });
+ }
+
+ /**
+ * Called after media player has been prepared. This method is executed on the caller's thread.
+ */
+ void onPrepared(final boolean startWhenPrepared) {
+ playerLock.lock();
+
+ if (playerStatus != PlayerStatus.PREPARING) {
+ playerLock.unlock();
+ throw new IllegalStateException("Player is not in PREPARING state");
+ }
+
+ Log.d(TAG, "Resource prepared");
+
+ if (mediaType == MediaType.VIDEO) {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight());
+ }
+
+ if (media.getPosition() > 0) {
+ seekToSync(media.getPosition());
+ }
+
+ if (media.getDuration() == 0) {
+ Log.d(TAG, "Setting duration of media");
+ media.setDuration(mediaPlayer.getDuration());
+ }
+ setPlayerStatus(PlayerStatus.PREPARED, media);
+
+ if (startWhenPrepared) {
+ resumeSync();
+ }
+
+ playerLock.unlock();
+ }
+
+ /**
+ * Resets the media player and moves it into INITIALIZED state.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ @Override
+ public void reinit() {
+ executor.submit(() -> {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
+ if (media != null) {
+ playMediaObject(media, true, stream, startWhenPrepared.get(), false);
+ } else if (mediaPlayer != null) {
+ mediaPlayer.reset();
+ } else {
+ Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null");
+ }
+ playerLock.unlock();
+ });
+ }
+
+
+ /**
+ * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
+ *
+ * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0
+ * <p/>
+ * This method is executed on the caller's thread.
+ */
+ private void seekToSync(int t) {
+ if (t < 0) {
+ t = 0;
+ }
+ playerLock.lock();
+
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ if (!stream) {
+ statusBeforeSeeking = playerStatus;
+ setPlayerStatus(PlayerStatus.SEEKING, media);
+ }
+ if(seekLatch != null && seekLatch.getCount() > 0) {
+ try {
+ seekLatch.await(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ }
+ }
+ seekLatch = new CountDownLatch(1);
+ mediaPlayer.seekTo(t);
+ try {
+ seekLatch.await(3, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ }
+ } else if (playerStatus == PlayerStatus.INITIALIZED) {
+ media.setPosition(t);
+ startWhenPrepared.set(false);
+ prepare();
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
+ * Invalid time values (< 0) will be ignored.
+ * <p/>
+ * This method is executed on an internal executor service.
+ */
+ @Override
+ public void seekTo(final int t) {
+ executor.submit(() -> seekToSync(t));
+ }
+
+ /**
+ * Seek a specific position from the current position
+ *
+ * @param d offset from current position (positive or negative)
+ */
+ @Override
+ public void seekDelta(final int d) {
+ executor.submit(() -> {
+ playerLock.lock();
+ int currentPosition = getPosition();
+ if (currentPosition != INVALID_TIME) {
+ seekToSync(currentPosition + d);
+ } else {
+ Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
+ }
+
+ playerLock.unlock();
+ });
+ }
+
+ /**
+ * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
+ */
+ @Override
+ public int getDuration() {
+ if (!playerLock.tryLock()) {
+ return INVALID_TIME;
+ }
+
+ int retVal = INVALID_TIME;
+ if (playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) {
+ retVal = mediaPlayer.getDuration();
+ } else if (media != null && media.getDuration() > 0) {
+ retVal = media.getDuration();
+ }
+
+ playerLock.unlock();
+ return retVal;
+ }
+
+ /**
+ * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
+ */
+ @Override
+ public int getPosition() {
+ try {
+ if (!playerLock.tryLock(50, TimeUnit.MILLISECONDS)) {
+ return INVALID_TIME;
+ }
+ } catch (InterruptedException e) {
+ return INVALID_TIME;
+ }
+
+ int retVal = INVALID_TIME;
+ if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) {
+ retVal = mediaPlayer.getCurrentPosition();
+ }
+ if (retVal <= 0 && media != null && media.getPosition() >= 0) {
+ retVal = media.getPosition();
+ }
+
+ playerLock.unlock();
+ Log.d(TAG, "getPosition() -> " + retVal);
+ return retVal;
+ }
+
+ @Override
+ public boolean isStartWhenPrepared() {
+ return startWhenPrepared.get();
+ }
+
+ @Override
+ public void setStartWhenPrepared(boolean startWhenPrepared) {
+ this.startWhenPrepared.set(startWhenPrepared);
+ }
+
+ /**
+ * Returns true if the playback speed can be adjusted.
+ */
+ @Override
+ public boolean canSetSpeed() {
+ boolean retVal = false;
+ if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
+ retVal = (mediaPlayer).canSetSpeed();
+ }
+ return retVal;
+ }
+
+ /**
+ * Sets the playback speed.
+ * This method is executed on the caller's thread.
+ */
+ private void setSpeedSync(float speed) {
+ playerLock.lock();
+ if (media != null && media.getMediaType() == MediaType.AUDIO) {
+ if (mediaPlayer.canSetSpeed()) {
+ mediaPlayer.setPlaybackSpeed(speed);
+ Log.d(TAG, "Playback speed was set to " + speed);
+ callback.playbackSpeedChanged(speed);
+ }
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Sets the playback speed.
+ * This method is executed on an internal executor service.
+ */
+ @Override
+ public void setSpeed(final float speed) {
+ executor.submit(() -> setSpeedSync(speed));
+ }
+
+ /**
+ * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
+ */
+ @Override
+ public float getPlaybackSpeed() {
+ if (!playerLock.tryLock()) {
+ return 1;
+ }
+
+ float retVal = 1;
+ if ((playerStatus == PlayerStatus.PLAYING
+ || playerStatus == PlayerStatus.PAUSED
+ || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) {
+ retVal = mediaPlayer.getCurrentSpeedMultiplier();
+ }
+ playerLock.unlock();
+ return retVal;
+ }
+
+ /**
+ * Sets the playback volume.
+ * This method is executed on an internal executor service.
+ */
+ @Override
+ public void setVolume(final float volumeLeft, float volumeRight) {
+ executor.submit(() -> setVolumeSync(volumeLeft, volumeRight));
+ }
+
+ /**
+ * Sets the playback volume.
+ * This method is executed on the caller's thread.
+ */
+ private void setVolumeSync(float volumeLeft, float volumeRight) {
+ playerLock.lock();
+ if (media != null && media.getMediaType() == MediaType.AUDIO) {
+ mediaPlayer.setVolume(volumeLeft, volumeRight);
+ Log.d(TAG, "Media player volume was set to " + volumeLeft + " " + volumeRight);
+ }
+ playerLock.unlock();
+ }
+
+ /**
+ * Returns true if the mediaplayer can mix stereo down to mono
+ */
+ @Override
+ public boolean canDownmix() {
+ boolean retVal = false;
+ if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
+ retVal = mediaPlayer.canDownmix();
+ }
+ return retVal;
+ }
+
+ @Override
+ public void setDownmix(boolean enable) {
+ playerLock.lock();
+ if (media != null && media.getMediaType() == MediaType.AUDIO) {
+ mediaPlayer.setDownmix(enable);
+ Log.d(TAG, "Media player downmix was set to " + enable);
+ }
+ playerLock.unlock();
+ }
+
+ @Override
+ public MediaType getCurrentMediaType() {
+ return mediaType;
+ }
+
+ @Override
+ public boolean isStreaming() {
+ return stream;
+ }
+
+
+ /**
+ * Releases internally used resources. This method should only be called when the object is not used anymore.
+ */
+ @Override
+ public void shutdown() {
+ executor.shutdown();
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ }
+ releaseWifiLockIfNecessary();
+ }
+
+ /**
+ * Releases internally used resources. This method should only be called when the object is not used anymore.
+ * This method is executed on an internal executor service.
+ */
+ @Override
+ public void shutdownQuietly() {
+ executor.submit(this::shutdown);
+ executor.shutdown();
+ }
+
+ @Override
+ public void setVideoSurface(final SurfaceHolder surface) {
+ executor.submit(() -> {
+ playerLock.lock();
+ if (mediaPlayer != null) {
+ mediaPlayer.setDisplay(surface);
+ }
+ playerLock.unlock();
+ });
+ }
+
+ @Override
+ public void resetVideoSurface() {
+ executor.submit(() -> {
+ playerLock.lock();
+ Log.d(TAG, "Resetting video surface");
+ mediaPlayer.setDisplay(null);
+ reinit();
+ playerLock.unlock();
+ });
+ }
+
+ /**
+ * Return width and height of the currently playing video as a pair.
+ *
+ * @return Width and height as a Pair or null if the video size could not be determined. The method might still
+ * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return
+ * invalid values.
+ */
+ @Override
+ public Pair<Integer, Integer> getVideoSize() {
+ if (!playerLock.tryLock()) {
+ // use cached value if lock can't be aquired
+ return videoSize;
+ }
+ Pair<Integer, Integer> res;
+ if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) {
+ res = null;
+ } else {
+ VideoPlayer vp = (VideoPlayer) mediaPlayer;
+ videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight());
+ res = videoSize;
+ }
+ playerLock.unlock();
+ return res;
+ }
+
+ /**
+ * Returns the current media, if you need the media and the player status together, you should
+ * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition
+ * could result in nonsensical results (like a status of PLAYING, but a null playable)
+ * @return the current media. May be null
+ */
+ @Override
+ public Playable getPlayable() {
+ return media;
+ }
+
+ @Override
+ protected void setPlayable(Playable playable) {
+ media = playable;
+ }
+
+ private IPlayer createMediaPlayer() {
+ if (mediaPlayer != null) {
+ mediaPlayer.release();
+ }
+ if (media == null || media.getMediaType() == MediaType.VIDEO) {
+ mediaPlayer = new VideoPlayer();
+ } else {
+ mediaPlayer = new AudioPlayer(context);
+ }
+ mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+ mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
+ return setMediaPlayerListeners(mediaPlayer);
+ }
+
+ private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
+
+ @Override
+ public void onAudioFocusChange(final int focusChange) {
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ playerLock.lock();
+
+ // If there is an incoming call, playback should be paused permanently
+ TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ final int callState = (tm != null) ? tm.getCallState() : 0;
+ Log.i(TAG, "Call state:" + callState);
+
+ if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
+ (!UserPreferences.shouldResumeAfterCall() && callState != TelephonyManager.CALL_STATE_IDLE)) {
+ Log.d(TAG, "Lost audio focus");
+ pause(true, false);
+ callback.shouldStop();
+ } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
+ Log.d(TAG, "Gained audio focus");
+ if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now
+ resume();
+ } else { // we ducked => raise audio level back
+ setVolumeSync(UserPreferences.getLeftVolume(),
+ UserPreferences.getRightVolume());
+ }
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
+ if (playerStatus == PlayerStatus.PLAYING) {
+ if (!UserPreferences.shouldPauseForFocusLoss()) {
+ Log.d(TAG, "Lost audio focus temporarily. Ducking...");
+ final float DUCK_FACTOR = 0.25f;
+ setVolumeSync(DUCK_FACTOR * UserPreferences.getLeftVolume(),
+ DUCK_FACTOR * UserPreferences.getRightVolume());
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ } else {
+ Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing...");
+ pause(false, false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
+ if (playerStatus == PlayerStatus.PLAYING) {
+ Log.d(TAG, "Lost audio focus temporarily. Pausing...");
+ pause(false, false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+ playerLock.unlock();
+ }
+ });
+ }
+ };
+
+
+ @Override
+ public void endPlayback(final boolean wasSkipped, boolean switchingPlayers) {
+ executor.submit(() -> {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
+
+ boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
+
+ if (playerStatus != PlayerStatus.INDETERMINATE) {
+ setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+ }
+ if (mediaPlayer != null) {
+ mediaPlayer.reset();
+
+ }
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ callback.endPlayback(media, isPlaying, wasSkipped, switchingPlayers);
+
+ playerLock.unlock();
+ });
+ }
+
+ /**
+ * Moves the LocalPSMP into STOPPED state. This call is only valid if the player is currently in
+ * INDETERMINATE state, for example after a call to endPlayback.
+ * This method will only take care of changing the PlayerStatus of this object! Other tasks like
+ * abandoning audio focus have to be done with other methods.
+ */
+ @Override
+ public void stop() {
+ executor.submit(() -> {
+ playerLock.lock();
+ releaseWifiLockIfNecessary();
+
+ if (playerStatus == PlayerStatus.INDETERMINATE) {
+ setPlayerStatus(PlayerStatus.STOPPED, null);
+ } else {
+ Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
+ }
+ playerLock.unlock();
+
+ });
+ }
+
+ @Override
+ protected boolean shouldLockWifi(){
+ return stream;
+ }
+
+ private IPlayer setMediaPlayerListeners(IPlayer mp) {
+ if (mp != null && media != null) {
+ if (media.getMediaType() == MediaType.AUDIO) {
+ ((AudioPlayer) mp)
+ .setOnCompletionListener(audioCompletionListener);
+ ((AudioPlayer) mp)
+ .setOnSeekCompleteListener(audioSeekCompleteListener);
+ ((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
+ ((AudioPlayer) mp)
+ .setOnBufferingUpdateListener(audioBufferingUpdateListener);
+ ((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
+ ((AudioPlayer) mp).setOnSpeedAdjustmentAvailableChangedListener(audioSetSpeedAbilityListener);
+ } else {
+ ((VideoPlayer) mp)
+ .setOnCompletionListener(videoCompletionListener);
+ ((VideoPlayer) mp)
+ .setOnSeekCompleteListener(videoSeekCompleteListener);
+ ((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
+ ((VideoPlayer) mp)
+ .setOnBufferingUpdateListener(videoBufferingUpdateListener);
+ ((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
+ }
+ }
+ return mp;
+ }
+
+ private final MediaPlayer.OnCompletionListener audioCompletionListener =
+ mp -> genericOnCompletion();
+
+ private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener =
+ mp -> genericOnCompletion();
+
+ private void genericOnCompletion() {
+ endPlayback(false, false);
+ }
+
+ private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener =
+ (mp, percent) -> genericOnBufferingUpdate(percent);
+
+ private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener =
+ (mp, percent) -> genericOnBufferingUpdate(percent);
+
+ private void genericOnBufferingUpdate(int percent) {
+ callback.onBufferingUpdate(percent);
+ }
+
+ private final MediaPlayer.OnInfoListener audioInfoListener =
+ (mp, what, extra) -> genericInfoListener(what);
+
+ private final android.media.MediaPlayer.OnInfoListener videoInfoListener =
+ (mp, what, extra) -> genericInfoListener(what);
+
+ private boolean genericInfoListener(int what) {
+ return callback.onMediaPlayerInfo(what, 0);
+ }
+
+ private final MediaPlayer.OnSpeedAdjustmentAvailableChangedListener audioSetSpeedAbilityListener =
+ (arg0, speedAdjustmentAvailable) -> callback.setSpeedAbilityChanged();
+
+
+ private final MediaPlayer.OnErrorListener audioErrorListener =
+ (mp, what, extra) -> {
+ if(mp.canFallback()) {
+ mp.fallback();
+ return true;
+ } else {
+ return genericOnError(mp, what, extra);
+ }
+ };
+
+ private final android.media.MediaPlayer.OnErrorListener videoErrorListener = this::genericOnError;
+
+ private boolean genericOnError(Object inObj, int what, int extra) {
+ return callback.onMediaPlayerError(inObj, what, extra);
+ }
+
+ private final MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener =
+ mp -> genericSeekCompleteListener();
+
+ private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener =
+ mp -> genericSeekCompleteListener();
+
+ private void genericSeekCompleteListener() {
+ Thread t = new Thread(() -> {
+ Log.d(TAG, "genericSeekCompleteListener");
+ if(seekLatch != null) {
+ seekLatch.countDown();
+ }
+ playerLock.lock();
+ if (playerStatus == PlayerStatus.SEEKING) {
+ setPlayerStatus(statusBeforeSeeking, media);
+ }
+ playerLock.unlock();
+ });
+ t.start();
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
index 6ab9859ac..2e4a486e6 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java
@@ -15,15 +15,20 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.MediaPlayer;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.Vibrator;
import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;
+import android.support.v7.media.MediaRouter;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@@ -35,11 +40,17 @@ import android.view.WindowManager;
import android.widget.Toast;
import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.target.Target;
+import com.google.android.gms.cast.ApplicationMetadata;
+import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager;
import java.util.List;
import de.danoeh.antennapod.core.ClientConfig;
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.DefaultCastConsumer;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
@@ -54,14 +65,16 @@ import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.IntList;
+import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.QueueAccess;
import de.danoeh.antennapod.core.util.flattr.FlattrUtils;
+import de.danoeh.antennapod.core.util.playback.ExternalMedia;
import de.danoeh.antennapod.core.util.playback.Playable;
/**
* Controls the MediaPlayer that plays a FeedMedia-file
*/
-public class PlaybackService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener {
+public class PlaybackService extends Service {
public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE";
public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE";
/**
@@ -74,6 +87,10 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
*/
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";
@@ -123,6 +140,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
*/
public static final int EXTRA_CODE_AUDIO = 1;
public static final int EXTRA_CODE_VIDEO = 2;
+ public static final int EXTRA_CODE_CAST = 3;
public static final int NOTIFICATION_TYPE_ERROR = 0;
public static final int NOTIFICATION_TYPE_INFO = 1;
@@ -154,12 +172,23 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
public static final int NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED = 9;
/**
+ * Send a message to the user (with provided String resource id)
+ */
+ public static final int NOTIFICATION_TYPE_SHOW_TOAST = 10;
+
+ /**
* Returned by getPositionSafe() or getDurationSafe() if the playbackService
* is in an invalid state.
*/
public static final int INVALID_TIME = -1;
/**
+ * 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;
+
+ /**
* Is true if service is running.
*/
public static boolean isRunning = false;
@@ -171,11 +200,25 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
* Is true if the service was running, but paused due to headphone disconnect
*/
public static boolean transientPause = false;
+ /**
+ * Is true if a Cast Device is connected to the service.
+ */
+ private static volatile boolean isCasting = false;
+ /**
+ * Stores the state of the cast playback just before it disconnects.
+ */
+ private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection;
+
+ private boolean wifiConnectivity = true;
+ private BroadcastReceiver wifiBroadcastReceiver;
private static final int NOTIFICATION_ID = 1;
private PlaybackServiceMediaPlayer mediaPlayer;
private PlaybackServiceTaskManager taskManager;
+
+ private CastManager castManager;
+ private MediaRouter mediaRouter;
/**
* Only used for Lollipop notifications.
*/
@@ -206,12 +249,12 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
*/
public static Intent getPlayerActivityIntent(Context context) {
if (isRunning) {
- return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType);
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType, isCasting);
} else {
if (PlaybackPreferences.getCurrentEpisodeIsVideo()) {
- return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO);
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO, isCasting);
} else {
- return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO);
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO, isCasting);
}
}
}
@@ -222,7 +265,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
*/
public static Intent getPlayerActivityIntent(Context context, Playable media) {
MediaType mt = media.getMediaType();
- return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt);
+ return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt, isCasting);
}
@Override
@@ -248,7 +291,10 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter(
ACTION_RESUME_PLAY_CURRENT_EPISODE));
taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback);
- mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback);
+
+ mediaRouter = MediaRouter.getInstance(getApplicationContext());
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .registerOnSharedPreferenceChangeListener(prefListener);
ComponentName eventReceiver = new ComponentName(getApplicationContext(),
MediaButtonReceiver.class);
@@ -261,7 +307,6 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
try {
mediaSession.setCallback(sessionCallback);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
- mediaSession.setActive(true);
} catch (NullPointerException npe) {
// on some devices (Huawei) setting active can cause a NullPointerException
// even with correct use of the api.
@@ -270,8 +315,21 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
Log.e(TAG, "NullPointerException while setting up MediaSession");
npe.printStackTrace();
}
- PreferenceManager.getDefaultSharedPreferences(this)
- .registerOnSharedPreferenceChangeListener(this);
+
+ castManager = CastManager.getInstance();
+ castManager.addCastConsumer(castConsumer);
+ isCasting = castManager.isConnected();
+ if (isCasting) {
+ if (UserPreferences.isCastEnabled()) {
+ onCastAppConnected(false);
+ } else {
+ castManager.disconnect();
+ }
+ } else {
+ mediaPlayer = new LocalPSMP(this, mediaPlayerCallback);
+ }
+
+ mediaSession.setActive(true);
}
@Override
@@ -283,7 +341,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
currentMediaType = MediaType.UNKNOWN;
PreferenceManager.getDefaultSharedPreferences(this)
- .unregisterOnSharedPreferenceChangeListener(this);
+ .unregisterOnSharedPreferenceChangeListener(prefListener);
if (mediaSession != null) {
mediaSession.release();
}
@@ -296,6 +354,8 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
unregisterReceiver(skipCurrentEpisodeReceiver);
unregisterReceiver(pausePlayCurrentEpisodeReceiver);
unregisterReceiver(pauseResumeCurrentEpisodeReceiver);
+ castManager.removeCastConsumer(castConsumer);
+ unregisterWifiBroadcastReceiver();
mediaPlayer.shutdown();
taskManager.shutdown();
}
@@ -307,22 +367,17 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
}
@Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- if(key.equals(UserPreferences.PREF_LOCKSCREEN_BACKGROUND)) {
- updateMediaSessionMetadata(getPlayable());
- }
- }
-
- @Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
Log.d(TAG, "OnStartCommand called");
final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
+ final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false);
final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE);
- if (keycode == -1 && playable == null) {
+ if (keycode == -1 && playable == null && !castDisconnect) {
Log.e(TAG, "PlaybackService was started with no arguments");
stopSelf();
+ return Service.START_REDELIVER_INTENT;
}
if ((flags & Service.START_FLAG_REDELIVERY) != 0) {
@@ -334,6 +389,8 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
Log.d(TAG, "Received media button event");
handleKeycode(keycode, intent.getIntExtra(MediaButtonReceiver.EXTRA_SOURCE,
InputDevice.SOURCE_CLASS_NONE));
+ } else if (castDisconnect) {
+ castManager.disconnect();
} else {
started = true;
boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
@@ -341,6 +398,10 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false);
boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false);
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ //If the user asks to play External Media, the casting session, if on, should end.
+ if (playable instanceof ExternalMedia) {
+ castManager.disconnect();
+ }
mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately);
}
}
@@ -359,11 +420,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
if (status == PlayerStatus.PLAYING) {
- if (UserPreferences.isPersistNotify()) {
- mediaPlayer.pause(false, true);
- } else {
- mediaPlayer.pause(true, true);
- }
+ mediaPlayer.pause(!UserPreferences.isPersistNotify(), true);
} else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
mediaPlayer.resume();
} else if (status == PlayerStatus.PREPARING) {
@@ -383,12 +440,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
if (status == PlayerStatus.PLAYING) {
- mediaPlayer.pause(false, true);
- }
- if (UserPreferences.isPersistNotify()) {
- mediaPlayer.pause(false, true);
- } else {
- mediaPlayer.pause(true, true);
+ mediaPlayer.pause(!UserPreferences.isPersistNotify(), true);
}
break;
@@ -397,7 +449,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
UserPreferences.shouldHardwareButtonSkip()) {
// assume the skip command comes from a notification or the lockscreen
// a >| skip button should actually skip
- mediaPlayer.endPlayback(true);
+ mediaPlayer.endPlayback(true, false);
} else {
// assume skip command comes from a (bluetooth) media button
// user actually wants to fast-forward
@@ -509,12 +561,13 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
taskManager.cancelPositionSaver();
saveCurrentPosition(false, 0);
taskManager.cancelWidgetUpdater();
- if (UserPreferences.isPersistNotify() && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ if ((UserPreferences.isPersistNotify() || isCasting) &&
+ android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// do not remove notification on pause based on user pref and whether android version supports expanded notifications
// Change [Play] button to [Pause]
setupNotification(newInfo);
- } else if (!UserPreferences.isPersistNotify()) {
- // remove notifcation on pause
+ } else if (!UserPreferences.isPersistNotify() && !isCasting) {
+ // remove notification on pause
stopForeground(true);
}
writePlayerStatusPlaybackPreferences();
@@ -587,12 +640,14 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
}
@Override
- public void updateMediaSessionMetadata(Playable p) {
- PlaybackService.this.updateMediaSessionMetadata(p);
+ public void reloadUI() {
+ Log.d(TAG, "reloadUI callback reached");
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ PlaybackService.this.updateMediaSessionMetadata(getPlayable());
}
@Override
- public boolean onMediaPlayerInfo(int code) {
+ public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) {
switch (code) {
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0);
@@ -600,6 +655,12 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0);
return true;
+ case RemotePSMP.CAST_ERROR:
+ sendNotificationBroadcast(NOTIFICATION_TYPE_SHOW_TOAST, resourceId);
+ return true;
+ case RemotePSMP.CAST_ERROR_PRIORITY_HIGH:
+ Toast.makeText(PlaybackService.this, resourceId, Toast.LENGTH_SHORT).show();
+ return true;
default:
return false;
}
@@ -619,16 +680,15 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
}
@Override
- public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) {
- PlaybackService.this.endPlayback(playNextEpisode, wasSkipped);
+ public boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
+ PlaybackService.this.endPlayback(media, playNextEpisode, wasSkipped, switchingPlayers);
return true;
}
};
- private void endPlayback(boolean playNextEpisode, boolean wasSkipped) {
- Log.d(TAG, "Playback ended");
+ private void endPlayback(final Playable playable, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) {
+ Log.d(TAG, "Playback ended" + (switchingPlayers ? " from switching players": ""));
- final Playable playable = mediaPlayer.getPlayable();
if (playable == null) {
Log.e(TAG, "Cannot end playback: media was null");
return;
@@ -643,26 +703,35 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
FeedMedia media = (FeedMedia) playable;
FeedItem item = media.getItem();
- try {
- final List<FeedItem> queue = taskManager.getQueue();
- isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId());
- nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue);
- } catch (InterruptedException e) {
- e.printStackTrace();
- // isInQueue remains false
- }
+ if (!switchingPlayers) {
+ try {
+ final List<FeedItem> queue = taskManager.getQueue();
+ isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId());
+ nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ // isInQueue remains false
+ }
- boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode();
+ boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode();
- if (!shouldKeep) {
- // only mark the item as played if we're not keeping it anyways
- DBWriter.markItemPlayed(item, FeedItem.PLAYED, true);
+ if (!shouldKeep) {
+ // only mark the item as played if we're not keeping it anyways
+ DBWriter.markItemPlayed(item, FeedItem.PLAYED, true);
- if (isInQueue) {
- DBWriter.removeQueueItem(PlaybackService.this, item, true);
+ if (isInQueue) {
+ DBWriter.removeQueueItem(PlaybackService.this, item, true);
+ }
+
+ // Delete episode if enabled
+ if (item.getFeed().getPreferences().getCurrentAutoDelete()) {
+ DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId());
+ Log.d(TAG, "Episode Deleted");
+ }
}
}
+
DBWriter.addItemToPlaybackHistory(media);
// auto-flattr if enabled
@@ -670,12 +739,6 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item);
}
- // Delete episode if enabled
- if(item.getFeed().getPreferences().getCurrentAutoDelete() && !shouldKeep ) {
- DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId());
- Log.d(TAG, "Episode Deleted");
- }
-
// gpodder play action
if(GpodnetPreferences.loggedIn()) {
GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY)
@@ -689,46 +752,49 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
}
}
- // Load next episode if previous episode was in the queue and if there
- // is an episode in the queue left.
- // Start playback immediately if continuous playback is enabled
- Playable nextMedia = null;
- boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() &&
- isInQueue &&
- nextItem != null;
-
- playNextEpisode = playNextEpisode &&
- loadNextItem &&
- UserPreferences.isFollowQueue();
-
- if (loadNextItem) {
- Log.d(TAG, "Loading next item in queue");
- nextMedia = nextItem.getMedia();
- }
- final boolean prepareImmediately;
- final boolean startWhenPrepared;
- final boolean stream;
+ if (!switchingPlayers) {
+ // Load next episode if previous episode was in the queue and if there
+ // is an episode in the queue left.
+ // Start playback immediately if continuous playback is enabled
+ Playable nextMedia = null;
+ boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() &&
+ isInQueue &&
+ nextItem != null;
+
+ playNextEpisode = playNextEpisode &&
+ loadNextItem &&
+ UserPreferences.isFollowQueue();
+
+ if (loadNextItem) {
+ Log.d(TAG, "Loading next item in queue");
+ nextMedia = nextItem.getMedia();
+ }
+ final boolean prepareImmediately;
+ final boolean startWhenPrepared;
+ final boolean stream;
- if (playNextEpisode) {
- Log.d(TAG, "Playback of next episode will start immediately.");
- prepareImmediately = startWhenPrepared = true;
- } else {
- Log.d(TAG, "No more episodes available to play");
- prepareImmediately = startWhenPrepared = false;
- stopForeground(true);
- stopWidgetUpdater();
- }
+ if (playNextEpisode) {
+ Log.d(TAG, "Playback of next episode will start immediately.");
+ prepareImmediately = startWhenPrepared = true;
+ } else {
+ Log.d(TAG, "No more episodes available to play");
+ prepareImmediately = startWhenPrepared = false;
+ stopForeground(true);
+ stopWidgetUpdater();
+ }
- writePlaybackPreferencesNoMediaPlaying();
- if (nextMedia != null) {
- stream = !nextMedia.localFileAvailable();
- mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
- sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
- (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
- } else {
- sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
- mediaPlayer.stop();
- //stopSelf();
+ writePlaybackPreferencesNoMediaPlaying();
+ if (nextMedia != null) {
+ stream = !nextMedia.localFileAvailable();
+ mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately);
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+ isCasting ? EXTRA_CODE_CAST :
+ (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO);
+ } else {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
+ mediaPlayer.stop();
+ //stopSelf();
+ }
}
}
@@ -853,11 +919,6 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
}
/**
- * Used by setupNotification to load notification data in another thread.
- */
- private Thread notificationSetupThread;
-
- /**
* Updates the Media Session for the corresponding status.
* @param playerStatus the current {@link PlayerStatus}
*/
@@ -906,38 +967,69 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
mediaSession.setPlaybackState(sessionState.build());
}
- private void updateMediaSessionMetadata(Playable p) {
- if (p == null) {
+ /**
+ * Used by updateMediaSessionMetadata to load notification data in another thread.
+ */
+ private Thread mediaSessionSetupThread;
+
+ private void updateMediaSessionMetadata(final Playable p) {
+ if (p == null || mediaSession == null) {
return;
}
- MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
- builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle());
- builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle());
- builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration());
- builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle());
- builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle());
-
- if (p.getImageUri() != null && UserPreferences.setLockscreenBackground()) {
- builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, p.getImageUri().toString());
- try {
- WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
- Display display = wm.getDefaultDisplay();
- Bitmap art = Glide.with(this)
- .load(p.getImageUri())
- .asBitmap()
- .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
- .centerCrop()
- .into(display.getWidth(), display.getHeight())
- .get();
- builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art);
- } catch (Throwable tr) {
- Log.e(TAG, Log.getStackTraceString(tr));
- }
+ if (mediaSessionSetupThread != null) {
+ mediaSessionSetupThread.interrupt();
}
- mediaSession.setMetadata(builder.build());
+
+ Runnable mediaSessionSetupTask = () -> {
+ MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle());
+ builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle());
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle());
+
+ if (p.getImageUri() != null && UserPreferences.setLockscreenBackground()) {
+ builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, p.getImageUri().toString());
+ try {
+ if (isCasting) {
+ Bitmap art = Glide.with(this)
+ .load(p.getImageUri())
+ .asBitmap()
+ .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
+ .into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
+ .get();
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art);
+ } else {
+ WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ Bitmap art = Glide.with(this)
+ .load(p.getImageUri())
+ .asBitmap()
+ .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
+ .centerCrop()
+ .into(display.getWidth(), display.getHeight())
+ .get();
+ builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art);
+ }
+ } catch (Throwable tr) {
+ Log.e(TAG, Log.getStackTraceString(tr));
+ }
+ }
+ if (!Thread.currentThread().isInterrupted() && started) {
+ mediaSession.setMetadata(builder.build());
+ }
+ };
+
+ mediaSessionSetupThread = new Thread(mediaSessionSetupTask);
+ mediaSessionSetupThread.start();
}
/**
+ * Used by setupNotification to load notification data in another thread.
+ */
+ private Thread notificationSetupThread;
+
+ /**
* Prepares notification and starts the service in the foreground.
*/
private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) {
@@ -966,8 +1058,8 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
.centerCrop()
.into(iconSize, iconSize)
.get();
- } catch(Throwable tr) {
- Log.e(TAG, Log.getStackTraceString(tr));
+ } catch (Throwable tr) {
+ Log.e(TAG, "Error loading the media icon for the notification", tr);
}
}
}
@@ -1002,6 +1094,17 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction
+ if (isCasting) {
+ Intent stopCastingIntent = new Intent(PlaybackService.this, PlaybackService.class);
+ stopCastingIntent.putExtra(EXTRA_CAST_DISCONNECT, true);
+ PendingIntent stopCastingPendingIntent = PendingIntent.getService(PlaybackService.this,
+ numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ notificationBuilder.addAction(R.drawable.ic_media_cast_disconnect,
+ getString(R.string.cast_disconnect_label),
+ stopCastingPendingIntent);
+ numActions++;
+ }
+
// always let them rewind
PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction(
KeyEvent.KEYCODE_MEDIA_REWIND, numActions);
@@ -1066,7 +1169,8 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
if (playerStatus == PlayerStatus.PLAYING ||
playerStatus == PlayerStatus.PREPARING ||
- playerStatus == PlayerStatus.SEEKING) {
+ playerStatus == PlayerStatus.SEEKING ||
+ isCasting) {
startForeground(NOTIFICATION_ID, notification);
} else {
stopForeground(false);
@@ -1118,11 +1222,10 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
DBTasks.flattrItemIfLoggedIn(this, item);
}
}
- playable.saveCurrentPosition(PreferenceManager
- .getDefaultSharedPreferences(getApplicationContext()),
+ playable.saveCurrentPosition(
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext()),
position,
- System.currentTimeMillis()
- );
+ System.currentTimeMillis());
}
}
@@ -1231,11 +1334,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) {
transientPause = true;
}
- if (UserPreferences.isPersistNotify()) {
- mediaPlayer.pause(false, true);
- } else {
- mediaPlayer.pause(true, true);
- }
+ mediaPlayer.pause(!UserPreferences.isPersistNotify(), true);
}
}
@@ -1274,7 +1373,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
public void onReceive(Context context, Intent intent) {
if (TextUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) {
Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent");
- mediaPlayer.endPlayback(true);
+ mediaPlayer.endPlayback(true, false);
}
}
};
@@ -1303,6 +1402,10 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
return currentMediaType;
}
+ public static boolean isCasting() {
+ return isCasting;
+ }
+
public void resume() {
mediaPlayer.resume();
}
@@ -1391,7 +1494,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
}
/**
- * @see de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.core.feed.Chapter)
+ * @see LocalPSMP#seekToChapter(de.danoeh.antennapod.core.feed.Chapter)
*/
public void seekToChapter(Chapter c) {
mediaPlayer.seekToChapter(c);
@@ -1430,6 +1533,67 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
}
}
+ private CastConsumer castConsumer = new DefaultCastConsumer() {
+ @Override
+ public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) {
+ PlaybackService.this.onCastAppConnected(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.
+ if (mediaPlayer != null) {
+ saveCurrentPosition(false, 0);
+ 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()");
+ isCasting = false;
+ PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection;
+ infoBeforeCastDisconnection = null;
+ if (info == null && mediaPlayer != null) {
+ info = mediaPlayer.getPSMPInfo();
+ }
+ if (info == null) {
+ info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null);
+ }
+ switchMediaPlayer(new LocalPSMP(PlaybackService.this, mediaPlayerCallback),
+ info, true);
+ if (info.playable != null) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD,
+ info.playable.getMediaType() == MediaType.AUDIO ? EXTRA_CODE_AUDIO : EXTRA_CODE_VIDEO);
+ } else {
+ Log.d(TAG, "Cast session disconnected, but no current media");
+ sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0);
+ }
+ // hardware volume buttons control the local device volume
+ mediaRouter.setMediaSessionCompat(null);
+ unregisterWifiBroadcastReceiver();
+ PlayerStatus status = info.playerStatus;
+ if ((status == PlayerStatus.PLAYING ||
+ status == PlayerStatus.SEEKING ||
+ status == PlayerStatus.PREPARING ||
+ UserPreferences.isPersistNotify()) &&
+ android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ setupNotification(info);
+ } else if (!UserPreferences.isPersistNotify()){
+ stopForeground(true);
+ }
+ }
+ };
+
private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() {
private static final String TAG = "MediaSessionCompat";
@@ -1487,7 +1651,7 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
public void onSkipToNext() {
Log.d(TAG, "onSkipToNext()");
if(UserPreferences.shouldHardwareButtonSkip()) {
- mediaPlayer.endPlayback(true);
+ mediaPlayer.endPlayback(true, false);
} else {
seekDelta(UserPreferences.getFastFowardSecs() * 1000);
}
@@ -1514,4 +1678,101 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
return false;
}
};
+
+ private void onCastAppConnected(boolean wasLaunched) {
+ Log.d(TAG, "A cast device application was " + (wasLaunched ? "launched" : "joined"));
+ isCasting = true;
+ PlaybackServiceMediaPlayer.PSMPInfo info = null;
+ 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.
+ saveCurrentPosition(false, 0);
+ }
+ }
+ if (info == null) {
+ info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null);
+ }
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, EXTRA_CODE_CAST);
+ switchMediaPlayer(new RemotePSMP(PlaybackService.this, mediaPlayerCallback),
+ info,
+ wasLaunched);
+ // hardware volume buttons control the remote device volume
+ mediaRouter.setMediaSessionCompat(mediaSession);
+ registerWifiBroadcastReceiver();
+ setupNotification(info);
+ }
+
+ private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer,
+ @NonNull PlaybackServiceMediaPlayer.PSMPInfo info,
+ boolean wasLaunched) {
+ if (mediaPlayer != null) {
+ mediaPlayer.endPlayback(true, true);
+ mediaPlayer.shutdownQuietly();
+ }
+ mediaPlayer = newPlayer;
+ 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));
+ }
+ }
+
+ private void registerWifiBroadcastReceiver() {
+ 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;
+ }
+ }
+ }
+ };
+ registerReceiver(wifiBroadcastReceiver,
+ new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION));
+ }
+
+ private void unregisterWifiBroadcastReceiver() {
+ if (wifiBroadcastReceiver != null) {
+ unregisterReceiver(wifiBroadcastReceiver);
+ wifiBroadcastReceiver = null;
+ }
+ }
+
+ private SharedPreferences.OnSharedPreferenceChangeListener prefListener =
+ (sharedPreferences, key) -> {
+ 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();
+ }
+ }
+ } else if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) {
+ updateMediaSessionMetadata(getPlayable());
+ }
+ };
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
index d8b334295..f3a9c1d03 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java
@@ -1,42 +1,31 @@
package de.danoeh.antennapod.core.service.playback;
import android.content.Context;
-import android.media.AudioManager;
import android.net.wifi.WifiManager;
-import android.os.PowerManager;
import android.support.annotation.NonNull;
-import android.telephony.TelephonyManager;
+import android.support.annotation.StringRes;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
-import org.antennapod.audio.MediaPlayer;
-
-import java.io.IOException;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.LinkedBlockingDeque;
-import java.util.concurrent.RejectedExecutionHandler;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.ReentrantLock;
-
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
-import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
-import de.danoeh.antennapod.core.util.playback.AudioPlayer;
-import de.danoeh.antennapod.core.util.playback.IPlayer;
import de.danoeh.antennapod.core.util.playback.Playable;
-import de.danoeh.antennapod.core.util.playback.VideoPlayer;
+
+/*
+ * An inconvenience of an implementation like this is that some members and methods that once were
+ * private are now protected, allowing for access from classes of the same package, namely
+ * PlaybackService. A workaround would be to move this to a dedicated package.
+ */
/**
- * Manages the MediaPlayer object of the PlaybackService.
+ * Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local
+ * and remote (cast devices) playback.
*/
-public class PlaybackServiceMediaPlayer {
+public abstract class PlaybackServiceMediaPlayer {
public static final String TAG = "PlaybackSvcMediaPlayer";
/**
@@ -44,58 +33,22 @@ public class PlaybackServiceMediaPlayer {
*/
public static final int INVALID_TIME = -1;
- private final AudioManager audioManager;
-
- private volatile PlayerStatus playerStatus;
- private volatile PlayerStatus statusBeforeSeeking;
- private volatile IPlayer mediaPlayer;
- private volatile Playable media;
-
- private volatile boolean stream;
- private volatile MediaType mediaType;
- private volatile AtomicBoolean startWhenPrepared;
- private volatile boolean pausedBecauseOfTransientAudiofocusLoss;
- private volatile Pair<Integer, Integer> videoSize;
-
- /**
- * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads
- * have to wait until these operations have finished.
- */
- private final ReentrantLock playerLock;
- private CountDownLatch seekLatch;
-
- private final PSMPCallback callback;
- private final Context context;
-
- private final ThreadPoolExecutor executor;
+ protected volatile PlayerStatus playerStatus;
/**
* A wifi-lock that is acquired if the media file is being streamed.
*/
private WifiManager.WifiLock wifiLock;
+ protected final PSMPCallback callback;
+ protected final Context context;
+
public PlaybackServiceMediaPlayer(@NonNull Context context,
- @NonNull PSMPCallback callback) {
+ @NonNull PSMPCallback callback){
this.context = context;
this.callback = callback;
- this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
- this.playerLock = new ReentrantLock();
- this.startWhenPrepared = new AtomicBoolean(false);
- executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<>(),
- new RejectedExecutionHandler() {
- @Override
- public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
- Log.d(TAG, "Rejected execution of runnable");
- }
- }
- );
- mediaPlayer = null;
- statusBeforeSeeking = null;
- pausedBecauseOfTransientAudiofocusLoss = false;
- mediaType = MediaType.UNKNOWN;
playerStatus = PlayerStatus.STOPPED;
- videoSize = null;
}
/**
@@ -124,99 +77,7 @@ public class PlaybackServiceMediaPlayer {
* for playback immediately (see 'prepareImmediately' parameter for more details)
* @param prepareImmediately Set to true if the method should also prepare the episode for playback.
*/
- public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- Log.d(TAG, "playMediaObject(...)");
- executor.submit(() -> {
- playerLock.lock();
- try {
- playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
- } catch (RuntimeException e) {
- e.printStackTrace();
- throw e;
- } finally {
- playerLock.unlock();
- }
- });
- }
-
- /**
- * 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.
- * <p/>
- * This method requires the playerLock and is executed on the caller's thread.
- *
- * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean)
- */
- private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
- if (!playerLock.isHeldByCurrentThread()) {
- throw new IllegalStateException("method requires playerLock");
- }
-
-
- if (media != null) {
- if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())
- && playerStatus == PlayerStatus.PLAYING) {
- // episode is already playing -> ignore method call
- Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
- return;
- } else {
- // stop playback of this episode
- if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) {
- mediaPlayer.stop();
- }
- // set temporarily to pause in order to update list with current position
- if (playerStatus == PlayerStatus.PLAYING) {
- setPlayerStatus(PlayerStatus.PAUSED, media);
- }
-
- // smart mark as played
- if(media != null && media instanceof FeedMedia) {
- FeedMedia oldMedia = (FeedMedia) media;
- if(oldMedia.hasAlmostEnded()) {
- Log.d(TAG, "smart mark as read");
- FeedItem item = oldMedia.getItem();
- DBWriter.markItemPlayed(item, FeedItem.PLAYED, false);
- DBWriter.removeQueueItem(context, item, false);
- DBWriter.addItemToPlaybackHistory(oldMedia);
- if (item.getFeed().getPreferences().getCurrentAutoDelete()) {
- Log.d(TAG, "Delete " + oldMedia.toString());
- DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId());
- }
- }
- }
-
- setPlayerStatus(PlayerStatus.INDETERMINATE, null);
- }
- }
-
- this.media = playable;
- this.stream = stream;
- this.mediaType = media.getMediaType();
- this.videoSize = null;
- createMediaPlayer();
- PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared);
- setPlayerStatus(PlayerStatus.INITIALIZING, media);
- try {
- media.loadMetadata();
- executor.submit(() -> callback.updateMediaSessionMetadata(media));
- if (stream) {
- mediaPlayer.setDataSource(media.getStreamUrl());
- } else {
- mediaPlayer.setDataSource(media.getLocalMediaUrl());
- }
- setPlayerStatus(PlayerStatus.INITIALIZED, media);
-
- if (prepareImmediately) {
- setPlayerStatus(PlayerStatus.PREPARING, media);
- mediaPlayer.prepare();
- onPrepared(startWhenPrepared);
- }
-
- } catch (Playable.PlayableException | IOException | IllegalStateException e) {
- e.printStackTrace();
- setPlayerStatus(PlayerStatus.ERROR, null);
- }
- }
+ public abstract void playMediaObject(@NonNull Playable playable, boolean stream, boolean startWhenPrepared, boolean prepareImmediately);
/**
* Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
@@ -224,51 +85,7 @@ public class PlaybackServiceMediaPlayer {
* <p/>
* This method is executed on an internal executor service.
*/
- public void resume() {
- executor.submit(() -> {
- playerLock.lock();
- resumeSync();
- playerLock.unlock();
- });
- }
-
- private void resumeSync() {
- if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
- int focusGained = audioManager.requestAudioFocus(
- audioFocusChangeListener, AudioManager.STREAM_MUSIC,
- AudioManager.AUDIOFOCUS_GAIN);
- if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
- acquireWifiLockIfNecessary();
- float speed = 1.0f;
- try {
- speed = Float.parseFloat(UserPreferences.getPlaybackSpeed());
- } catch(NumberFormatException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- UserPreferences.setPlaybackSpeed(String.valueOf(speed));
- }
- setSpeed(speed);
- setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume());
-
- if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
- int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
- media.getPosition(),
- media.getLastPlayedTime());
- seekToSync(newPosition);
- }
- mediaPlayer.start();
-
- setPlayerStatus(PlayerStatus.PLAYING, media);
- pausedBecauseOfTransientAudiofocusLoss = false;
- media.onPlaybackStart();
-
- } else {
- Log.e(TAG, "Failed to request audio focus");
- }
- } else {
- Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus);
- }
- }
-
+ public abstract void resume();
/**
* Saves the current position and pauses playback. Note that, if audiofocus
@@ -280,153 +97,22 @@ public class PlaybackServiceMediaPlayer {
* @param reinit is true if service should reinit after pausing if the media
* file is being streamed
*/
- public void pause(final boolean abandonFocus, final boolean reinit) {
- executor.submit(() -> {
- playerLock.lock();
- releaseWifiLockIfNecessary();
- if (playerStatus == PlayerStatus.PLAYING) {
- Log.d(TAG, "Pausing playback.");
- mediaPlayer.pause();
- setPlayerStatus(PlayerStatus.PAUSED, media);
-
- if (abandonFocus) {
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- pausedBecauseOfTransientAudiofocusLoss = false;
- }
- if (stream && reinit) {
- reinit();
- }
- } else {
- Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state");
- }
-
- playerLock.unlock();
- });
- }
+ public abstract void pause(boolean abandonFocus, boolean reinit);
/**
- * Prepares media player for playback if the service is in the INITALIZED
+ * Prepared media player for playback if the service is in the INITALIZED
* state.
* <p/>
* This method is executed on an internal executor service.
*/
- public void prepare() {
- executor.submit(() -> {
- playerLock.lock();
-
- if (playerStatus == PlayerStatus.INITIALIZED) {
- Log.d(TAG, "Preparing media player");
- setPlayerStatus(PlayerStatus.PREPARING, media);
- try {
- mediaPlayer.prepare();
- onPrepared(startWhenPrepared.get());
- } catch (IOException e) {
- e.printStackTrace();
- setPlayerStatus(PlayerStatus.ERROR, null);
- }
- }
- playerLock.unlock();
-
- });
- }
-
- /**
- * Called after media player has been prepared. This method is executed on the caller's thread.
- */
- void onPrepared(final boolean startWhenPrepared) {
- playerLock.lock();
-
- if (playerStatus != PlayerStatus.PREPARING) {
- playerLock.unlock();
- throw new IllegalStateException("Player is not in PREPARING state");
- }
-
- Log.d(TAG, "Resource prepared");
-
- if (mediaType == MediaType.VIDEO) {
- VideoPlayer vp = (VideoPlayer) mediaPlayer;
- videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight());
- }
-
- if (media.getPosition() > 0) {
- seekToSync(media.getPosition());
- }
-
- if (media.getDuration() == 0) {
- Log.d(TAG, "Setting duration of media");
- media.setDuration(mediaPlayer.getDuration());
- }
- setPlayerStatus(PlayerStatus.PREPARED, media);
-
- if (startWhenPrepared) {
- resumeSync();
- }
-
- playerLock.unlock();
- }
+ public abstract void prepare();
/**
* Resets the media player and moves it into INITIALIZED state.
* <p/>
* This method is executed on an internal executor service.
*/
- public void reinit() {
- executor.submit(() -> {
- playerLock.lock();
- releaseWifiLockIfNecessary();
- if (media != null) {
- playMediaObject(media, true, stream, startWhenPrepared.get(), false);
- } else if (mediaPlayer != null) {
- mediaPlayer.reset();
- } else {
- Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null");
- }
- playerLock.unlock();
- });
- }
-
-
- /**
- * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
- *
- * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0
- * <p/>
- * This method is executed on the caller's thread.
- */
- private void seekToSync(int t) {
- if (t < 0) {
- t = 0;
- }
- playerLock.lock();
-
- if (playerStatus == PlayerStatus.PLAYING
- || playerStatus == PlayerStatus.PAUSED
- || playerStatus == PlayerStatus.PREPARED) {
- if (!stream) {
- statusBeforeSeeking = playerStatus;
- setPlayerStatus(PlayerStatus.SEEKING, media);
- }
- if(seekLatch != null && seekLatch.getCount() > 0) {
- try {
- seekLatch.await(3, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- }
- }
- seekLatch = new CountDownLatch(1);
- mediaPlayer.seekTo(t);
- try {
- seekLatch.await(3, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- }
- } else if (playerStatus == PlayerStatus.INITIALIZED) {
- media.setPosition(t);
- startWhenPrepared.set(false);
- prepare();
- }
- playerLock.unlock();
- }
+ public abstract void reinit();
/**
* Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing.
@@ -434,28 +120,14 @@ public class PlaybackServiceMediaPlayer {
* <p/>
* This method is executed on an internal executor service.
*/
- public void seekTo(final int t) {
- executor.submit(() -> seekToSync(t));
- }
+ public abstract void seekTo(int t);
/**
* Seek a specific position from the current position
*
* @param d offset from current position (positive or negative)
*/
- public void seekDelta(final int d) {
- executor.submit(() -> {
- playerLock.lock();
- int currentPosition = getPosition();
- if (currentPosition != INVALID_TIME) {
- seekToSync(currentPosition + d);
- } else {
- Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
- }
-
- playerLock.unlock();
- });
- }
+ public abstract void seekDelta(int d);
/**
* Seek to the start of the specified chapter.
@@ -467,193 +139,64 @@ public class PlaybackServiceMediaPlayer {
/**
* Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
*/
- public int getDuration() {
- if (!playerLock.tryLock()) {
- return INVALID_TIME;
- }
-
- int retVal = INVALID_TIME;
- if (playerStatus == PlayerStatus.PLAYING
- || playerStatus == PlayerStatus.PAUSED
- || playerStatus == PlayerStatus.PREPARED) {
- retVal = mediaPlayer.getDuration();
- } else if (media != null && media.getDuration() > 0) {
- retVal = media.getDuration();
- }
-
- playerLock.unlock();
- return retVal;
- }
+ public abstract int getDuration();
/**
* Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
*/
- public int getPosition() {
- try {
- if (!playerLock.tryLock(50, TimeUnit.MILLISECONDS)) {
- return INVALID_TIME;
- }
- } catch (InterruptedException e) {
- return INVALID_TIME;
- }
+ public abstract int getPosition();
- int retVal = INVALID_TIME;
- if (playerStatus == PlayerStatus.PLAYING
- || playerStatus == PlayerStatus.PAUSED
- || playerStatus == PlayerStatus.PREPARED
- || playerStatus == PlayerStatus.SEEKING) {
- retVal = mediaPlayer.getCurrentPosition();
- }
- if (retVal <= 0 && media != null && media.getPosition() >= 0) {
- retVal = media.getPosition();
- }
-
- playerLock.unlock();
- Log.d(TAG, "getPosition() -> " + retVal);
- return retVal;
- }
+ public abstract boolean isStartWhenPrepared();
- public boolean isStartWhenPrepared() {
- return startWhenPrepared.get();
- }
-
- public void setStartWhenPrepared(boolean startWhenPrepared) {
- this.startWhenPrepared.set(startWhenPrepared);
- }
+ public abstract void setStartWhenPrepared(boolean startWhenPrepared);
/**
* Returns true if the playback speed can be adjusted.
*/
- public boolean canSetSpeed() {
- boolean retVal = false;
- if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
- retVal = (mediaPlayer).canSetSpeed();
- }
- return retVal;
- }
-
- /**
- * Sets the playback speed.
- * This method is executed on the caller's thread.
- */
- private void setSpeedSync(float speed) {
- playerLock.lock();
- if (media != null && media.getMediaType() == MediaType.AUDIO) {
- if (mediaPlayer.canSetSpeed()) {
- mediaPlayer.setPlaybackSpeed(speed);
- Log.d(TAG, "Playback speed was set to " + speed);
- callback.playbackSpeedChanged(speed);
- }
- }
- playerLock.unlock();
- }
+ public abstract boolean canSetSpeed();
/**
* Sets the playback speed.
* This method is executed on an internal executor service.
*/
- public void setSpeed(final float speed) {
- executor.submit(() -> setSpeedSync(speed));
- }
+ public abstract void setSpeed(float speed);
/**
* Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
*/
- public float getPlaybackSpeed() {
- if (!playerLock.tryLock()) {
- return 1;
- }
-
- float retVal = 1;
- if ((playerStatus == PlayerStatus.PLAYING
- || playerStatus == PlayerStatus.PAUSED
- || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) {
- retVal = mediaPlayer.getCurrentSpeedMultiplier();
- }
- playerLock.unlock();
- return retVal;
- }
+ public abstract float getPlaybackSpeed();
/**
* Sets the playback volume.
* This method is executed on an internal executor service.
*/
- public void setVolume(final float volumeLeft, float volumeRight) {
- executor.submit(() -> setVolumeSync(volumeLeft, volumeRight));
- }
-
- /**
- * Sets the playback volume.
- * This method is executed on the caller's thread.
- */
- private void setVolumeSync(float volumeLeft, float volumeRight) {
- playerLock.lock();
- if (media != null && media.getMediaType() == MediaType.AUDIO) {
- mediaPlayer.setVolume(volumeLeft, volumeRight);
- Log.d(TAG, "Media player volume was set to " + volumeLeft + " " + volumeRight);
- }
- playerLock.unlock();
- }
+ public abstract void setVolume(float volumeLeft, float volumeRight);
/**
* Returns true if the mediaplayer can mix stereo down to mono
*/
- public boolean canDownmix() {
- boolean retVal = false;
- if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) {
- retVal = mediaPlayer.canDownmix();
- }
- return retVal;
- }
+ public abstract boolean canDownmix();
- public void setDownmix(boolean enable) {
- playerLock.lock();
- if (media != null && media.getMediaType() == MediaType.AUDIO) {
- mediaPlayer.setDownmix(enable);
- Log.d(TAG, "Media player downmix was set to " + enable);
- }
- playerLock.unlock();
- }
+ public abstract void setDownmix(boolean enable);
- public MediaType getCurrentMediaType() {
- return mediaType;
- }
+ public abstract MediaType getCurrentMediaType();
- public boolean isStreaming() {
- return stream;
- }
+ public abstract boolean isStreaming();
+ /**
+ * Releases internally used resources. This method should only be called when the object is not used anymore.
+ */
+ public abstract void shutdown();
/**
* Releases internally used resources. This method should only be called when the object is not used anymore.
+ * This method is executed on an internal executor service.
*/
- public void shutdown() {
- executor.shutdown();
- if (mediaPlayer != null) {
- mediaPlayer.release();
- }
- releaseWifiLockIfNecessary();
- }
+ public abstract void shutdownQuietly();
- public void setVideoSurface(final SurfaceHolder surface) {
- executor.submit(() -> {
- playerLock.lock();
- if (mediaPlayer != null) {
- mediaPlayer.setDisplay(surface);
- }
- playerLock.unlock();
- });
- }
+ public abstract void setVideoSurface(SurfaceHolder surface);
- public void resetVideoSurface() {
- executor.submit(() -> {
- playerLock.lock();
- Log.d(TAG, "Resetting video surface");
- mediaPlayer.setDisplay(null);
- reinit();
- playerLock.unlock();
- });
- }
+ public abstract void resetVideoSurface();
/**
* Return width and height of the currently playing video as a pair.
@@ -662,30 +205,15 @@ public class PlaybackServiceMediaPlayer {
* return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return
* invalid values.
*/
- public Pair<Integer, Integer> getVideoSize() {
- if (!playerLock.tryLock()) {
- // use cached value if lock can't be aquired
- return videoSize;
- }
- Pair<Integer, Integer> res;
- if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) {
- res = null;
- } else {
- VideoPlayer vp = (VideoPlayer) mediaPlayer;
- videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight());
- res = videoSize;
- }
- playerLock.unlock();
- return res;
- }
+ public abstract Pair<Integer, Integer> getVideoSize();
/**
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
*
* @return The PSMPInfo object.
*/
- public synchronized PSMPInfo getPSMPInfo() {
- return new PSMPInfo(playerStatus, media);
+ public final synchronized PSMPInfo getPSMPInfo() {
+ return new PSMPInfo(playerStatus, getPlayable());
}
/**
@@ -704,145 +232,27 @@ public class PlaybackServiceMediaPlayer {
* could result in nonsensical results (like a status of PLAYING, but a null playable)
* @return the current media. May be null
*/
- public Playable getPlayable() {
- return media;
- }
-
- /**
- * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
- * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
- * <p/>
- * This method will notify the callback about the change of the player status (even if the new status is the same
- * as the old one).
- *
- * @param newStatus The new PlayerStatus. This must not be null.
- * @param newMedia The new playable object of the PSMP object. This can be null.
- */
- private synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
- Log.d(TAG, "Setting player status to " + newStatus);
-
- this.playerStatus = newStatus;
- this.media = newMedia;
- if (playerStatus != null) {
- Log.d(TAG, "playerStatus: " + playerStatus.toString());
- }
-
- callback.statusChanged(new PSMPInfo(playerStatus, media));
- }
-
- private IPlayer createMediaPlayer() {
- if (mediaPlayer != null) {
- mediaPlayer.release();
- }
- if (media == null || media.getMediaType() == MediaType.VIDEO) {
- mediaPlayer = new VideoPlayer();
- } else {
- mediaPlayer = new AudioPlayer(context);
- }
- mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
- mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
- return setMediaPlayerListeners(mediaPlayer);
- }
-
- private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
-
- @Override
- public void onAudioFocusChange(final int focusChange) {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- playerLock.lock();
-
- // If there is an incoming call, playback should be paused permanently
- TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- final int callState = (tm != null) ? tm.getCallState() : 0;
- Log.i(TAG, "Call state:" + callState);
-
- if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
- (!UserPreferences.shouldResumeAfterCall() && callState != TelephonyManager.CALL_STATE_IDLE)) {
- Log.d(TAG, "Lost audio focus");
- pause(true, false);
- callback.shouldStop();
- } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
- Log.d(TAG, "Gained audio focus");
- if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now
- resume();
- } else { // we ducked => raise audio level back
- setVolumeSync(UserPreferences.getLeftVolume(),
- UserPreferences.getRightVolume());
- }
- } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
- if (playerStatus == PlayerStatus.PLAYING) {
- if (!UserPreferences.shouldPauseForFocusLoss()) {
- Log.d(TAG, "Lost audio focus temporarily. Ducking...");
- final float DUCK_FACTOR = 0.25f;
- setVolumeSync(DUCK_FACTOR * UserPreferences.getLeftVolume(),
- DUCK_FACTOR * UserPreferences.getRightVolume());
- pausedBecauseOfTransientAudiofocusLoss = false;
- } else {
- Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing...");
- pause(false, false);
- pausedBecauseOfTransientAudiofocusLoss = true;
- }
- }
- } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
- if (playerStatus == PlayerStatus.PLAYING) {
- Log.d(TAG, "Lost audio focus temporarily. Pausing...");
- pause(false, false);
- pausedBecauseOfTransientAudiofocusLoss = true;
- }
- }
- playerLock.unlock();
- }
- });
- }
- };
-
+ public abstract Playable getPlayable();
- public void endPlayback(final boolean wasSkipped) {
- executor.submit(() -> {
- playerLock.lock();
- releaseWifiLockIfNecessary();
+ protected abstract void setPlayable(Playable playable);
- boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
-
- if (playerStatus != PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.INDETERMINATE, media);
- }
- if (mediaPlayer != null) {
- mediaPlayer.reset();
-
- }
- audioManager.abandonAudioFocus(audioFocusChangeListener);
- callback.endPlayback(isPlaying, wasSkipped);
-
- playerLock.unlock();
- });
- }
+ public abstract void endPlayback(boolean wasSkipped, boolean switchingPlayers);
/**
- * Moves the PlaybackServiceMediaPlayer into STOPPED state. This call is only valid if the player is currently in
+ * Moves the PSMP into STOPPED state. This call is only valid if the player is currently in
* INDETERMINATE state, for example after a call to endPlayback.
* This method will only take care of changing the PlayerStatus of this object! Other tasks like
* abandoning audio focus have to be done with other methods.
*/
- public void stop() {
- executor.submit(() -> {
- playerLock.lock();
- releaseWifiLockIfNecessary();
-
- if (playerStatus == PlayerStatus.INDETERMINATE) {
- setPlayerStatus(PlayerStatus.STOPPED, null);
- } else {
- Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus);
- }
- playerLock.unlock();
+ public abstract void stop();
- });
- }
+ /**
+ * @return {@code true} if the WifiLock feature should be used, {@code false} otherwise.
+ */
+ protected abstract boolean shouldLockWifi();
- private synchronized void acquireWifiLockIfNecessary() {
- if (stream) {
+ protected final synchronized void acquireWifiLockIfNecessary() {
+ if (shouldLockWifi()) {
if (wifiLock == null) {
wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
@@ -852,22 +262,52 @@ public class PlaybackServiceMediaPlayer {
}
}
- private synchronized void releaseWifiLockIfNecessary() {
+ protected final synchronized void releaseWifiLockIfNecessary() {
if (wifiLock != null && wifiLock.isHeld()) {
wifiLock.release();
}
}
/**
- * Holds information about a PSMP object.
+ * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
+ * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
+ * <p/>
+ * This method will notify the callback about the change of the player status (even if the new status is the same
+ * as the old one).
+ *
+ * @param newStatus The new PlayerStatus. This must not be null.
+ * @param newMedia The new playable object of the PSMP object. This can be null.
*/
- public class PSMPInfo {
- public PlayerStatus playerStatus;
- public Playable playable;
+ protected synchronized final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
+ Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
- public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
- this.playerStatus = playerStatus;
- this.playable = playable;
+ this.playerStatus = newStatus;
+ setPlayable(newMedia);
+
+ if (playerStatus != null) {
+ Log.d(TAG, "playerStatus: " + playerStatus.toString());
+ }
+
+ callback.statusChanged(new PSMPInfo(playerStatus, getPlayable()));
+ }
+
+ protected void smartMarkAsPlayed(Playable media) {
+ if(media != null && media instanceof FeedMedia) {
+ FeedMedia oldMedia = (FeedMedia) media;
+ if(oldMedia.hasAlmostEnded()) {
+ Log.d(TAG, "smart mark as read");
+ FeedItem item = oldMedia.getItem();
+ if (item == null) {
+ return;
+ }
+ DBWriter.markItemPlayed(item, FeedItem.PLAYED, false);
+ DBWriter.removeQueueItem(context, item, false);
+ DBWriter.addItemToPlaybackHistory(oldMedia);
+ if (item.getFeed().getPreferences().getCurrentAutoDelete()) {
+ Log.d(TAG, "Delete " + oldMedia.toString());
+ DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId());
+ }
+ }
}
}
@@ -882,114 +322,25 @@ public class PlaybackServiceMediaPlayer {
void onBufferingUpdate(int percent);
- void updateMediaSessionMetadata(Playable p);
+ void reloadUI();
- boolean onMediaPlayerInfo(int code);
+ boolean onMediaPlayerInfo(int code, @StringRes int resourceId);
boolean onMediaPlayerError(Object inObj, int what, int extra);
- boolean endPlayback(boolean playNextEpisode, boolean wasSkipped);
- }
-
- private IPlayer setMediaPlayerListeners(IPlayer mp) {
- if (mp != null && media != null) {
- if (media.getMediaType() == MediaType.AUDIO) {
- ((AudioPlayer) mp)
- .setOnCompletionListener(audioCompletionListener);
- ((AudioPlayer) mp)
- .setOnSeekCompleteListener(audioSeekCompleteListener);
- ((AudioPlayer) mp).setOnErrorListener(audioErrorListener);
- ((AudioPlayer) mp)
- .setOnBufferingUpdateListener(audioBufferingUpdateListener);
- ((AudioPlayer) mp).setOnInfoListener(audioInfoListener);
- ((AudioPlayer) mp).setOnSpeedAdjustmentAvailableChangedListener(audioSetSpeedAbilityListener);
- } else {
- ((VideoPlayer) mp)
- .setOnCompletionListener(videoCompletionListener);
- ((VideoPlayer) mp)
- .setOnSeekCompleteListener(videoSeekCompleteListener);
- ((VideoPlayer) mp).setOnErrorListener(videoErrorListener);
- ((VideoPlayer) mp)
- .setOnBufferingUpdateListener(videoBufferingUpdateListener);
- ((VideoPlayer) mp).setOnInfoListener(videoInfoListener);
- }
- }
- return mp;
- }
-
- private final MediaPlayer.OnCompletionListener audioCompletionListener =
- mp -> genericOnCompletion();
-
- private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener =
- mp -> genericOnCompletion();
-
- private void genericOnCompletion() {
- endPlayback(false);
- }
-
- private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener =
- (mp, percent) -> genericOnBufferingUpdate(percent);
-
- private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener =
- (mp, percent) -> genericOnBufferingUpdate(percent);
-
- private void genericOnBufferingUpdate(int percent) {
- callback.onBufferingUpdate(percent);
+ boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers);
}
- private final MediaPlayer.OnInfoListener audioInfoListener =
- (mp, what, extra) -> genericInfoListener(what);
-
- private final android.media.MediaPlayer.OnInfoListener videoInfoListener =
- (mp, what, extra) -> genericInfoListener(what);
-
- private boolean genericInfoListener(int what) {
- return callback.onMediaPlayerInfo(what);
- }
+ /**
+ * Holds information about a PSMP object.
+ */
+ public static class PSMPInfo {
+ public PlayerStatus playerStatus;
+ public Playable playable;
- private final MediaPlayer.OnSpeedAdjustmentAvailableChangedListener
- audioSetSpeedAbilityListener = new MediaPlayer.OnSpeedAdjustmentAvailableChangedListener() {
- @Override
- public void onSpeedAdjustmentAvailableChanged(MediaPlayer arg0, boolean speedAdjustmentAvailable) {
- callback.setSpeedAbilityChanged();
+ public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
+ this.playerStatus = playerStatus;
+ this.playable = playable;
}
- };
-
-
- private final MediaPlayer.OnErrorListener audioErrorListener =
- (mp, what, extra) -> {
- if(mp.canFallback()) {
- mp.fallback();
- return true;
- } else {
- return genericOnError(mp, what, extra);
- }
- };
-
- private final android.media.MediaPlayer.OnErrorListener videoErrorListener = this::genericOnError;
-
- private boolean genericOnError(Object inObj, int what, int extra) {
- return callback.onMediaPlayerError(inObj, what, extra);
- }
-
- private final MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener =
- mp -> genericSeekCompleteListener();
-
- private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener =
- mp -> genericSeekCompleteListener();
-
- private void genericSeekCompleteListener() {
- Thread t = new Thread(() -> {
- Log.d(TAG, "genericSeekCompleteListener");
- if(seekLatch != null) {
- seekLatch.countDown();
- }
- playerLock.lock();
- if (playerStatus == PlayerStatus.SEEKING) {
- setPlayerStatus(statusBeforeSeeking, media);
- }
- playerLock.unlock();
- });
- t.start();
}
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
index 7c666abd5..8a222d7ec 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java
@@ -1,24 +1,33 @@
package de.danoeh.antennapod.core.service.playback;
public enum PlayerStatus {
- INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state.
- ERROR,
- PREPARING,
- PAUSED,
- PLAYING,
- STOPPED,
- PREPARED,
- SEEKING,
- INITIALIZING, // playback service is loading the Playable's metadata
- INITIALIZED; // playback service was started, data source of media player was set.
+ 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 int statusValue;
private static final PlayerStatus[] fromOrdinalLookup;
static {
fromOrdinalLookup = PlayerStatus.values();
}
+ PlayerStatus(int val) {
+ statusValue = val;
+ }
+
public static PlayerStatus fromOrdinal(int o) {
return fromOrdinalLookup[o];
}
+
+ public boolean isAtLeast(PlayerStatus other) {
+ return other == null || this.statusValue>=other.statusValue;
+ }
}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
new file mode 100644
index 000000000..b9068447e
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java
@@ -0,0 +1,587 @@
+package de.danoeh.antennapod.core.service.playback;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.support.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 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.cast.RemoteMedia;
+import de.danoeh.antennapod.core.feed.FeedMedia;
+import de.danoeh.antennapod.core.feed.MediaType;
+import de.danoeh.antennapod.core.util.RewindAfterPauseUtils;
+import de.danoeh.antennapod.core.util.playback.Playable;
+
+/**
+ * Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
+ */
+public class RemotePSMP extends PlaybackServiceMediaPlayer {
+
+ public static final String TAG = "RemotePSMP";
+
+ public static final int CAST_ERROR = 3001;
+
+ public static final int CAST_ERROR_PRIORITY_HIGH = 3005;
+
+ private final CastManager castMgr;
+
+ private volatile Playable media;
+ private volatile MediaInfo remoteMedia;
+ private volatile MediaType mediaType;
+
+ private final AtomicBoolean isBuffering;
+
+ private final AtomicBoolean startWhenPrepared;
+
+ public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) {
+ super(context, callback);
+
+ castMgr = CastManager.getInstance();
+ media = null;
+ mediaType = null;
+ startWhenPrepared = new AtomicBoolean(false);
+ isBuffering = new AtomicBoolean(false);
+
+ try {
+ if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) {
+ // updates the state, but does not start playing new media if it was going to
+ onRemoteMediaPlayerStatusUpdated(
+ ((p, playNextEpisode, wasSkipped, switchingPlayers) ->
+ this.callback.endPlayback(p, false, wasSkipped, switchingPlayers)));
+ }
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to do initial check for loaded media", e);
+ }
+
+ castMgr.addCastConsumer(castConsumer);
+ //TODO
+ }
+
+ private CastConsumer castConsumer = new DefaultCastConsumer() {
+ @Override
+ public void onRemoteMediaPlayerMetadataUpdated() {
+ RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(callback::endPlayback);
+ }
+
+ @Override
+ public void onRemoteMediaPlayerStatusUpdated() {
+ RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(callback::endPlayback);
+ }
+
+ @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) {
+ setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+ callback.endPlayback(media, true, false, false);
+ }
+ }
+
+ @Override
+ public void onFailed(int resourceId, int statusCode) {
+ callback.onMediaPlayerInfo(CAST_ERROR, resourceId);
+ }
+ };
+
+ private void setBuffering(boolean buffering) {
+ if (buffering && isBuffering.compareAndSet(false, true)) {
+ callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0);
+ } else if (!buffering && isBuffering.compareAndSet(true, false)) {
+ callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0);
+ }
+ }
+
+ private Playable localVersion(MediaInfo info){
+ if (info == null) {
+ return null;
+ }
+ if (CastUtils.matches(info, media)) {
+ return media;
+ }
+ return CastUtils.getPlayable(info, true);
+ }
+
+ private MediaInfo remoteVersion(Playable playable) {
+ if (playable == null) {
+ return null;
+ }
+ if (CastUtils.matches(remoteMedia, playable)) {
+ return remoteMedia;
+ }
+ if (playable instanceof FeedMedia) {
+ return CastUtils.convertFromFeedMedia((FeedMedia) playable);
+ }
+ if (playable instanceof RemoteMedia) {
+ return ((RemoteMedia) playable).extractMediaInfo();
+ }
+ return null;
+ }
+
+ private void onRemoteMediaPlayerStatusUpdated(@NonNull EndPlaybackCall endPlaybackCall) {
+ MediaStatus status = castMgr.getMediaStatus();
+ if (status == null) {
+ Log.d(TAG, "Received null MediaStatus");
+ //setBuffering(false);
+ //setPlayerStatus(PlayerStatus.INDETERMINATE, null);
+ return;
+ } else {
+ Log.d(TAG, "Received remote status/media update. New state=" + status.getPlayerState());
+ }
+ Playable currentMedia = localVersion(status.getMediaInfo());
+ boolean updateUI = currentMedia != media;
+ if (currentMedia != null) {
+ long position = status.getStreamPosition();
+ if (position > 0 && currentMedia.getPosition() == 0) {
+ currentMedia.setPosition((int) position);
+ }
+ }
+ int state = status.getPlayerState();
+ setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING);
+ switch (state) {
+ case MediaStatus.PLAYER_STATE_PLAYING:
+ setPlayerStatus(PlayerStatus.PLAYING, currentMedia);
+ break;
+ case MediaStatus.PLAYER_STATE_PAUSED:
+ setPlayerStatus(PlayerStatus.PAUSED, currentMedia);
+ break;
+ case MediaStatus.PLAYER_STATE_BUFFERING:
+ setPlayerStatus(playerStatus, currentMedia);
+ break;
+ case MediaStatus.PLAYER_STATE_IDLE:
+ int reason = status.getIdleReason();
+ switch (reason) {
+ case MediaStatus.IDLE_REASON_CANCELED:
+ // check if we're already loading something else
+ if (!updateUI || media == null) {
+ setPlayerStatus(PlayerStatus.STOPPED, currentMedia);
+ } else {
+ updateUI = false;
+ }
+ break;
+ case MediaStatus.IDLE_REASON_INTERRUPTED:
+ // check if we're already loading something else
+ if (!updateUI || media == null) {
+ setPlayerStatus(PlayerStatus.PREPARING, currentMedia);
+ } else {
+ updateUI = false;
+ }
+ break;
+ case MediaStatus.IDLE_REASON_NONE:
+ setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia);
+ break;
+ case MediaStatus.IDLE_REASON_FINISHED:
+ boolean playing = playerStatus == PlayerStatus.PLAYING;
+ setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
+ endPlaybackCall.endPlayback(currentMedia,playing, false, false);
+ // endPlayback already updates the UI, so no need to trigger it ourselves
+ updateUI = false;
+ break;
+ case MediaStatus.IDLE_REASON_ERROR:
+ Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode...");
+ setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
+ callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH,
+ R.string.cast_failed_media_error_skipping);
+ endPlaybackCall.endPlayback(currentMedia, startWhenPrepared.get(), true, false);
+ // endPlayback already updates the UI, so no need to trigger it ourselves
+ updateUI = false;
+ }
+ break;
+ case MediaStatus.PLAYER_STATE_UNKNOWN:
+ //is this right?
+ setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
+ break;
+ default:
+ Log.e(TAG, "Remote media state undetermined!");
+ setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
+ }
+ if (updateUI) {
+ callback.reloadUI();
+ }
+ }
+
+ @Override
+ public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ Log.d(TAG, "playMediaObject() called");
+ playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
+ }
+
+ /**
+ * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if
+ * the given playable parameter is the same object as the currently playing media.
+ *
+ * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean)
+ */
+ private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
+ if (!CastUtils.isCastable(playable)) {
+ Log.d(TAG, "media provided is not compatible with cast device");
+ callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable);
+ callback.endPlayback(playable, startWhenPrepared, true, false);
+ return;
+ }
+
+ if (media != null) {
+ if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())
+ && playerStatus == PlayerStatus.PLAYING) {
+ // episode is already playing -> ignore method call
+ Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.");
+ return;
+ } else {
+ // set temporarily to pause in order to update list with current position
+ try {
+ if (castMgr.isRemoteMediaPlaying()) {
+ setPlayerStatus(PlayerStatus.PAUSED, media);
+ }
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to determine whether media was playing, falling back to stored player status", e);
+ // this might end up just being pointless if we need to query the remote device for the position
+ if (playerStatus == PlayerStatus.PLAYING) {
+ setPlayerStatus(PlayerStatus.PAUSED, media);
+ }
+ }
+ smartMarkAsPlayed(media);
+
+
+ setPlayerStatus(PlayerStatus.INDETERMINATE, null);
+ }
+ }
+
+ this.media = playable;
+ remoteMedia = remoteVersion(playable);
+ //this.stream = stream;
+ this.mediaType = media.getMediaType();
+ this.startWhenPrepared.set(startWhenPrepared);
+ setPlayerStatus(PlayerStatus.INITIALIZING, media);
+ try {
+ media.loadMetadata();
+ callback.reloadUI();
+ setPlayerStatus(PlayerStatus.INITIALIZED, media);
+ if (prepareImmediately) {
+ prepare();
+ }
+ } catch (Playable.PlayableException e) {
+ Log.e(TAG, "Error while loading media metadata", e);
+ setPlayerStatus(PlayerStatus.STOPPED, null);
+ }
+ }
+
+ @Override
+ public void resume() {
+ try {
+ // TODO see comment on prepare()
+ // setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume());
+ if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) {
+ int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
+ media.getPosition(),
+ media.getLastPlayedTime());
+ castMgr.play(newPosition);
+ }
+ castMgr.play();
+ } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to resume remote playback", e);
+ }
+ }
+
+ @Override
+ public void pause(boolean abandonFocus, boolean reinit) {
+ try {
+ if (castMgr.isRemoteMediaPlaying()) {
+ castMgr.pause();
+ }
+ } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to pause", e);
+ }
+ }
+
+ @Override
+ public void prepare() {
+ if (playerStatus == PlayerStatus.INITIALIZED) {
+ Log.d(TAG, "Preparing media player");
+ setPlayerStatus(PlayerStatus.PREPARING, media);
+ try {
+ int position = media.getPosition();
+ if (position > 0) {
+ position = RewindAfterPauseUtils.calculatePositionWithRewind(
+ position,
+ media.getLastPlayedTime());
+ }
+ // TODO We're not supporting user set stream volume yet, as we need to make a UI
+ // that doesn't allow changing playback speed or have different values for left/right
+ //setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume());
+ castMgr.loadMedia(remoteMedia, startWhenPrepared.get(), position);
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Error loading media", e);
+ setPlayerStatus(PlayerStatus.INITIALIZED, media);
+ }
+ }
+ }
+
+ @Override
+ public void reinit() {
+ Log.d(TAG, "reinit() called");
+ if (media != null) {
+ playMediaObject(media, true, false, startWhenPrepared.get(), false);
+ } else {
+ Log.d(TAG, "Call to reinit was ignored: media was null");
+ }
+ }
+
+ @Override
+ public void seekTo(int t) {
+ //TODO check other seek implementations and see if there's no issue with sending too many seek commands to the remote media player
+ try {
+ if (castMgr.isRemoteMediaLoaded()) {
+ setPlayerStatus(PlayerStatus.SEEKING, media);
+ castMgr.seek(t);
+ } else if (media != null && playerStatus == PlayerStatus.INITIALIZED){
+ media.setPosition(t);
+ startWhenPrepared.set(false);
+ prepare();
+ }
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to seek", e);
+ }
+ }
+
+ @Override
+ public void seekDelta(int d) {
+ int position = getPosition();
+ if (position != INVALID_TIME) {
+ seekTo(position + d);
+ } else {
+ Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
+ }
+ }
+
+ @Override
+ public int getDuration() {
+ int retVal = INVALID_TIME;
+ boolean prepared;
+ try {
+ prepared = castMgr.isRemoteMediaLoaded();
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to check if remote media is loaded", e);
+ prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
+ }
+ if (prepared) {
+ try {
+ retVal = (int) castMgr.getMediaDuration();
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to determine remote media's duration", e);
+ }
+ }
+ if(retVal == INVALID_TIME && media != null && media.getDuration() > 0) {
+ retVal = media.getDuration();
+ }
+ Log.d(TAG, "getDuration() -> " + retVal);
+ return retVal;
+ }
+
+ @Override
+ public int getPosition() {
+ int retVal = INVALID_TIME;
+ boolean prepared;
+ try {
+ prepared = castMgr.isRemoteMediaLoaded();
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to check if remote media is loaded", e);
+ prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED);
+ }
+ if (prepared) {
+ try {
+ retVal = (int) castMgr.getCurrentMediaPosition();
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Unable to determine remote media's position", e);
+ }
+ }
+ if(retVal <= 0 && media != null && media.getPosition() >= 0) {
+ retVal = media.getPosition();
+ }
+ Log.d(TAG, "getPosition() -> " + retVal);
+ return retVal;
+ }
+
+ @Override
+ public boolean isStartWhenPrepared() {
+ return startWhenPrepared.get();
+ }
+
+ @Override
+ public void setStartWhenPrepared(boolean startWhenPrepared) {
+ this.startWhenPrepared.set(startWhenPrepared);
+ }
+
+ //TODO I believe some parts of the code make the same decision skipping this check, so that
+ //should be changed as well
+ @Override
+ public boolean canSetSpeed() {
+ return false;
+ }
+
+ @Override
+ public void setSpeed(float speed) {
+ throw new UnsupportedOperationException("Setting playback speed unsupported for Remote Playback");
+ }
+
+ @Override
+ public float getPlaybackSpeed() {
+ return 1;
+ }
+
+ @Override
+ public void setVolume(float volumeLeft, float volumeRight) {
+ Log.d(TAG, "Setting the Stream volume on Remote Media Player");
+ double volume = (volumeLeft+volumeRight)/2;
+ if (volume > 1.0) {
+ volume = 1.0;
+ }
+ if (volume < 0.0) {
+ volume = 0.0;
+ }
+ try {
+ castMgr.setStreamVolume(volume);
+ } catch (TransientNetworkDisconnectionException | NoConnectionException | CastException e) {
+ Log.e(TAG, "Unable to set the volume", e);
+ }
+ }
+
+ @Override
+ public boolean canDownmix() {
+ return false;
+ }
+
+ @Override
+ public void setDownmix(boolean enable) {
+ throw new UnsupportedOperationException("Setting downmix unsupported in Remote Media Player");
+ }
+
+ @Override
+ public MediaType getCurrentMediaType() {
+ return mediaType;
+ }
+
+ @Override
+ public boolean isStreaming() {
+ return true;
+ }
+
+ @Override
+ public void shutdown() {
+ castMgr.removeCastConsumer(castConsumer);
+ }
+
+ @Override
+ public void shutdownQuietly() {
+ shutdown();
+ }
+
+ @Override
+ public void setVideoSurface(SurfaceHolder surface) {
+ throw new UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player");
+ }
+
+ @Override
+ public void resetVideoSurface() {
+ Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player");
+ }
+
+ @Override
+ public Pair<Integer, Integer> getVideoSize() {
+ return null;
+ }
+
+ @Override
+ public Playable getPlayable() {
+ return media;
+ }
+
+ @Override
+ protected void setPlayable(Playable playable) {
+ if (playable != media) {
+ media = playable;
+ remoteMedia = remoteVersion(playable);
+ }
+ }
+
+ @Override
+ public void endPlayback(boolean wasSkipped, boolean switchingPlayers) {
+ Log.d(TAG, "endPlayback() called");
+ boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
+ try {
+ isPlaying = castMgr.isRemoteMediaPlaying();
+ } catch (TransientNetworkDisconnectionException | NoConnectionException e) {
+ Log.e(TAG, "Could not determine if media is playing", e);
+ }
+ // TODO make sure we stop playback whenever there's no next episode.
+ if (playerStatus != PlayerStatus.INDETERMINATE) {
+ setPlayerStatus(PlayerStatus.INDETERMINATE, media);
+ }
+ callback.endPlayback(media, isPlaying, wasSkipped, switchingPlayers);
+ }
+
+ @Override
+ public 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;
+ }
+
+ private interface EndPlaybackCall {
+ boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers);
+ }
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
index 927639e69..55b608dce 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java
@@ -92,6 +92,18 @@ public class NetworkUtils {
return mWifi.isConnected();
}
+ /**
+ * Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi.
+ */
+ public static String getWifiSsid() {
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ if (wifiInfo != null) {
+ return wifiInfo.getSSID();
+ }
+ return null;
+ }
+
public static Observable<Long> getFeedMediaSizeObservable(FeedMedia media) {
return Observable.create(new Observable.OnSubscribe<Long>() {
@Override
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Supplier.java b/core/src/main/java/de/danoeh/antennapod/core/util/Supplier.java
new file mode 100644
index 000000000..796b03154
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/Supplier.java
@@ -0,0 +1,5 @@
+package de.danoeh.antennapod.core.util;
+
+public interface Supplier<T> {
+ T get();
+}
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java
index 0650225f0..5ba7f11d6 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java
@@ -6,7 +6,7 @@ import de.danoeh.antennapod.core.R;
/** Utility class for MediaPlayer errors. */
public class MediaPlayerError {
-
+
/** Get a human-readable string for a specific error code. */
public static String getErrorString(Context context, int code) {
int resId;
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
index 6459d86ed..201efbc81 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java
@@ -8,6 +8,7 @@ import android.util.Log;
import java.util.List;
import de.danoeh.antennapod.core.asynctask.ImageResource;
+import de.danoeh.antennapod.core.cast.RemoteMedia;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
@@ -183,6 +184,9 @@ public interface Playable extends Parcelable,
case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA:
result = createExternalMediaInstance(pref);
break;
+ case RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA:
+ result = createRemoteMediaInstance(pref);
+ break;
}
if (result == null) {
Log.e(TAG, "Could not restore Playable object from preferences");
@@ -211,6 +215,12 @@ public interface Playable extends Parcelable,
}
return result;
}
+
+ private static Playable createRemoteMediaInstance(SharedPreferences pref) {
+ //TODO there's probably no point in restoring RemoteMedia from preferences, because we
+ //only care about it while it's playing on the cast device.
+ return null;
+ }
}
class PlayableException extends Exception {
diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
index 1409ffe09..7870c747e 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java
@@ -176,7 +176,7 @@ public abstract class PlaybackController {
if(serviceBinder != null) {
serviceBinder.unsubscribe();
}
- serviceBinder = Observable.fromCallable(() -> getPlayLastPlayedMediaIntent())
+ serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(intent -> {
@@ -338,6 +338,9 @@ public abstract class PlaybackController {
break;
case PlaybackService.NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED:
onSetSpeedAbilityChanged();
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_SHOW_TOAST:
+ postStatusMsg(code, true);
}
}
@@ -392,7 +395,7 @@ public abstract class PlaybackController {
}
/**
- * Is called whenever the PlaybackService changes it's status. This method
+ * Is called whenever the PlaybackService changes its status. This method
* should be used to update the GUI or start/cancel background threads.
*/
private void handleStatus() {
@@ -401,7 +404,8 @@ public abstract class PlaybackController {
final CharSequence playText = activity.getString(R.string.play_label);
final CharSequence pauseText = activity.getString(R.string.pause_label);
- if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) {
+ if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO ||
+ PlaybackService.isCasting()) {
TypedArray res = activity.obtainStyledAttributes(new int[]{
R.attr.av_play_big, R.attr.av_pause_big});
playResource = res.getResourceId(0, R.drawable.ic_play_arrow_grey600_36dp);
@@ -415,7 +419,7 @@ public abstract class PlaybackController {
Log.d(TAG, "status: " + status.toString());
switch (status) {
case ERROR:
- postStatusMsg(R.string.player_error_msg);
+ postStatusMsg(R.string.player_error_msg, false);
handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN);
break;
case PAUSED:
@@ -424,14 +428,16 @@ public abstract class PlaybackController {
cancelPositionObserver();
onPositionObserverUpdate();
updatePlayButtonAppearance(playResource, playText);
- if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
+ if (!PlaybackService.isCasting() &&
+ PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
setScreenOn(false);
}
break;
case PLAYING:
clearStatusMsg();
checkMediaInfoLoaded();
- if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
+ if (!PlaybackService.isCasting() &&
+ PlaybackService.getCurrentMediaType() == MediaType.VIDEO) {
onAwaitingVideoSurface();
setScreenOn(true);
}
@@ -439,7 +445,7 @@ public abstract class PlaybackController {
updatePlayButtonAppearance(pauseResource, pauseText);
break;
case PREPARING:
- postStatusMsg(R.string.player_preparing_msg);
+ postStatusMsg(R.string.player_preparing_msg, false);
checkMediaInfoLoaded();
if (playbackService != null) {
if (playbackService.isStartWhenPrepared()) {
@@ -450,16 +456,16 @@ public abstract class PlaybackController {
}
break;
case STOPPED:
- postStatusMsg(R.string.player_stopped_msg);
+ postStatusMsg(R.string.player_stopped_msg, false);
break;
case PREPARED:
checkMediaInfoLoaded();
- postStatusMsg(R.string.player_ready_msg);
+ postStatusMsg(R.string.player_ready_msg, false);
updatePlayButtonAppearance(playResource, playText);
break;
case SEEKING:
onPositionObserverUpdate();
- postStatusMsg(R.string.player_seeking_msg);
+ postStatusMsg(R.string.player_seeking_msg, false);
break;
case INITIALIZED:
checkMediaInfoLoaded();
@@ -485,7 +491,7 @@ public abstract class PlaybackController {
return null;
}
- public void postStatusMsg(int msg) {}
+ public void postStatusMsg(int msg, boolean showToast) {}
public void clearStatusMsg() {}
@@ -502,8 +508,9 @@ public abstract class PlaybackController {
private void queryService() {
Log.d(TAG, "Querying service info");
if (playbackService != null) {
- status = playbackService.getStatus();
- media = playbackService.getPlayable();
+ PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo();
+ status = info.playerStatus;
+ media = info.playable;
/*
if (media == null) {
Log.w(TAG,
@@ -712,8 +719,9 @@ public abstract class PlaybackController {
}
}
- public boolean isPlayingVideo() {
- return playbackService != null && PlaybackService.getCurrentMediaType() == MediaType.VIDEO;
+ public boolean isPlayingVideoLocally() {
+ return playbackService != null && PlaybackService.getCurrentMediaType() == MediaType.VIDEO
+ && !PlaybackService.isCasting();
}
public Pair<Integer, Integer> getVideoSize() {
@@ -745,6 +753,7 @@ public abstract class PlaybackController {
public void reinitServiceIfPaused() {
if (playbackService != null
&& playbackService.isStreaming()
+ && !PlaybackService.isCasting()
&& (playbackService.getStatus() == PlayerStatus.PAUSED ||
(playbackService.getStatus() == PlayerStatus.PREPARING &&
!playbackService.isStartWhenPrepared()))) {
diff --git a/core/src/main/res/drawable-hdpi/ic_audiotrack_light.png b/core/src/main/res/drawable-hdpi/ic_audiotrack_light.png
new file mode 100644
index 000000000..04e23e1f6
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_audiotrack_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_disabled_light.png b/core/src/main/res/drawable-hdpi/ic_cast_disabled_light.png
new file mode 100644
index 000000000..c0a55d555
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_disabled_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_disconnect_grey600_36dp.png b/core/src/main/res/drawable-hdpi/ic_cast_disconnect_grey600_36dp.png
new file mode 100644
index 000000000..07da25ec0
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_disconnect_grey600_36dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_disconnect_white_36dp.png b/core/src/main/res/drawable-hdpi/ic_cast_disconnect_white_36dp.png
new file mode 100644
index 000000000..ff9d97fd1
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_disconnect_white_36dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_light.png b/core/src/main/res/drawable-hdpi/ic_cast_light.png
new file mode 100644
index 000000000..b0c581a0e
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_off_light.png b/core/src/main/res/drawable-hdpi/ic_cast_off_light.png
new file mode 100644
index 000000000..5f3c0179c
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_off_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_on_0_light.png b/core/src/main/res/drawable-hdpi/ic_cast_on_0_light.png
new file mode 100644
index 000000000..e872693a4
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_on_0_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_on_1_light.png b/core/src/main/res/drawable-hdpi/ic_cast_on_1_light.png
new file mode 100644
index 000000000..d8be1ebc6
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_on_1_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_on_2_light.png b/core/src/main/res/drawable-hdpi/ic_cast_on_2_light.png
new file mode 100644
index 000000000..27cda9e9d
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_on_2_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_cast_on_light.png b/core/src/main/res/drawable-hdpi/ic_cast_on_light.png
new file mode 100644
index 000000000..4ee525875
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_cast_on_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_close_light.png b/core/src/main/res/drawable-hdpi/ic_close_light.png
new file mode 100644
index 000000000..93187e450
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_close_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_media_cast_disconnect.png b/core/src/main/res/drawable-hdpi/ic_media_cast_disconnect.png
new file mode 100644
index 000000000..700c116e5
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_media_cast_disconnect.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_pause_light.png b/core/src/main/res/drawable-hdpi/ic_pause_light.png
new file mode 100644
index 000000000..0c505d1c8
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_pause_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-hdpi/ic_play_light.png b/core/src/main/res/drawable-hdpi/ic_play_light.png
new file mode 100644
index 000000000..7957dff5b
--- /dev/null
+++ b/core/src/main/res/drawable-hdpi/ic_play_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_audiotrack_light.png b/core/src/main/res/drawable-mdpi/ic_audiotrack_light.png
new file mode 100644
index 000000000..8ce237e38
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_audiotrack_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_disabled_light.png b/core/src/main/res/drawable-mdpi/ic_cast_disabled_light.png
new file mode 100644
index 000000000..7940a0332
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_disabled_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_disconnect_grey600_36dp.png b/core/src/main/res/drawable-mdpi/ic_cast_disconnect_grey600_36dp.png
new file mode 100644
index 000000000..b66b17e44
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_disconnect_grey600_36dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_disconnect_white_36dp.png b/core/src/main/res/drawable-mdpi/ic_cast_disconnect_white_36dp.png
new file mode 100644
index 000000000..4f3c9a9ef
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_disconnect_white_36dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_light.png b/core/src/main/res/drawable-mdpi/ic_cast_light.png
new file mode 100644
index 000000000..1f5bec20b
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_off_light.png b/core/src/main/res/drawable-mdpi/ic_cast_off_light.png
new file mode 100644
index 000000000..963db27d4
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_off_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_on_0_light.png b/core/src/main/res/drawable-mdpi/ic_cast_on_0_light.png
new file mode 100644
index 000000000..a90d9e305
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_on_0_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_on_1_light.png b/core/src/main/res/drawable-mdpi/ic_cast_on_1_light.png
new file mode 100644
index 000000000..bb2cf30bf
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_on_1_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_on_2_light.png b/core/src/main/res/drawable-mdpi/ic_cast_on_2_light.png
new file mode 100644
index 000000000..3ed59e55b
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_on_2_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_cast_on_light.png b/core/src/main/res/drawable-mdpi/ic_cast_on_light.png
new file mode 100644
index 000000000..713427b97
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_cast_on_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_close_light.png b/core/src/main/res/drawable-mdpi/ic_close_light.png
new file mode 100644
index 000000000..2c52c9b0f
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_close_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_media_cast_disconnect.png b/core/src/main/res/drawable-mdpi/ic_media_cast_disconnect.png
new file mode 100644
index 000000000..767f420df
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_media_cast_disconnect.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_pause_light.png b/core/src/main/res/drawable-mdpi/ic_pause_light.png
new file mode 100644
index 000000000..6218a774f
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_pause_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-mdpi/ic_play_light.png b/core/src/main/res/drawable-mdpi/ic_play_light.png
new file mode 100644
index 000000000..1e0ccaf80
--- /dev/null
+++ b/core/src/main/res/drawable-mdpi/ic_play_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_audiotrack_light.png b/core/src/main/res/drawable-xhdpi/ic_audiotrack_light.png
new file mode 100644
index 000000000..247df6f39
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_audiotrack_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_disabled_light.png b/core/src/main/res/drawable-xhdpi/ic_cast_disabled_light.png
new file mode 100644
index 000000000..fbb3e062c
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_disabled_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_disconnect_grey600_36dp.png b/core/src/main/res/drawable-xhdpi/ic_cast_disconnect_grey600_36dp.png
new file mode 100644
index 000000000..d81267f09
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_disconnect_grey600_36dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_disconnect_white_36dp.png b/core/src/main/res/drawable-xhdpi/ic_cast_disconnect_white_36dp.png
new file mode 100644
index 000000000..9aab16855
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_disconnect_white_36dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_light.png b/core/src/main/res/drawable-xhdpi/ic_cast_light.png
new file mode 100644
index 000000000..f2713e20e
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_off_light.png b/core/src/main/res/drawable-xhdpi/ic_cast_off_light.png
new file mode 100644
index 000000000..f4f8aaea8
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_off_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_on_0_light.png b/core/src/main/res/drawable-xhdpi/ic_cast_on_0_light.png
new file mode 100644
index 000000000..247fc95ba
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_on_0_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_on_1_light.png b/core/src/main/res/drawable-xhdpi/ic_cast_on_1_light.png
new file mode 100644
index 000000000..ecf4b4723
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_on_1_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_on_2_light.png b/core/src/main/res/drawable-xhdpi/ic_cast_on_2_light.png
new file mode 100644
index 000000000..60e3afa5d
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_on_2_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_cast_on_light.png b/core/src/main/res/drawable-xhdpi/ic_cast_on_light.png
new file mode 100644
index 000000000..40ce9d4f2
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_cast_on_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_close_light.png b/core/src/main/res/drawable-xhdpi/ic_close_light.png
new file mode 100644
index 000000000..49faa429a
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_close_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_media_cast_disconnect.png b/core/src/main/res/drawable-xhdpi/ic_media_cast_disconnect.png
new file mode 100644
index 000000000..740867129
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_media_cast_disconnect.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_pause_light.png b/core/src/main/res/drawable-xhdpi/ic_pause_light.png
new file mode 100644
index 000000000..40cd79f14
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_pause_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xhdpi/ic_play_light.png b/core/src/main/res/drawable-xhdpi/ic_play_light.png
new file mode 100644
index 000000000..33f6a5919
--- /dev/null
+++ b/core/src/main/res/drawable-xhdpi/ic_play_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_audiotrack_light.png b/core/src/main/res/drawable-xxhdpi/ic_audiotrack_light.png
new file mode 100644
index 000000000..8c83b169d
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_audiotrack_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_disabled_light.png b/core/src/main/res/drawable-xxhdpi/ic_cast_disabled_light.png
new file mode 100644
index 000000000..e94df3889
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_disabled_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_disconnect_grey600_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_cast_disconnect_grey600_36dp.png
new file mode 100644
index 000000000..68f7650a1
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_disconnect_grey600_36dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_disconnect_white_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_cast_disconnect_white_36dp.png
new file mode 100644
index 000000000..828755407
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_disconnect_white_36dp.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_light.png b/core/src/main/res/drawable-xxhdpi/ic_cast_light.png
new file mode 100644
index 000000000..c5722a6eb
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_off_light.png b/core/src/main/res/drawable-xxhdpi/ic_cast_off_light.png
new file mode 100644
index 000000000..92ac67b34
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_off_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_on_0_light.png b/core/src/main/res/drawable-xxhdpi/ic_cast_on_0_light.png
new file mode 100644
index 000000000..2742fcb4a
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_on_0_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_on_1_light.png b/core/src/main/res/drawable-xxhdpi/ic_cast_on_1_light.png
new file mode 100644
index 000000000..405178e64
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_on_1_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_on_2_light.png b/core/src/main/res/drawable-xxhdpi/ic_cast_on_2_light.png
new file mode 100644
index 000000000..dfe52428d
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_on_2_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_cast_on_light.png b/core/src/main/res/drawable-xxhdpi/ic_cast_on_light.png
new file mode 100644
index 000000000..7e69a0864
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_cast_on_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_close_light.png b/core/src/main/res/drawable-xxhdpi/ic_close_light.png
new file mode 100644
index 000000000..be519bfcb
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_close_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_media_cast_disconnect.png b/core/src/main/res/drawable-xxhdpi/ic_media_cast_disconnect.png
new file mode 100644
index 000000000..2d2ec9035
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_media_cast_disconnect.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_pause_light.png b/core/src/main/res/drawable-xxhdpi/ic_pause_light.png
new file mode 100644
index 000000000..a36d4d11e
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_pause_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxhdpi/ic_play_light.png b/core/src/main/res/drawable-xxhdpi/ic_play_light.png
new file mode 100644
index 000000000..b1424874a
--- /dev/null
+++ b/core/src/main/res/drawable-xxhdpi/ic_play_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxxhdpi/ic_close_light.png b/core/src/main/res/drawable-xxxhdpi/ic_close_light.png
new file mode 100644
index 000000000..679c2a4d5
--- /dev/null
+++ b/core/src/main/res/drawable-xxxhdpi/ic_close_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxxhdpi/ic_pause_light.png b/core/src/main/res/drawable-xxxhdpi/ic_pause_light.png
new file mode 100644
index 000000000..7de2ef4ed
--- /dev/null
+++ b/core/src/main/res/drawable-xxxhdpi/ic_pause_light.png
Binary files differ
diff --git a/core/src/main/res/drawable-xxxhdpi/ic_play_light.png b/core/src/main/res/drawable-xxxhdpi/ic_play_light.png
new file mode 100644
index 000000000..4428c8477
--- /dev/null
+++ b/core/src/main/res/drawable-xxxhdpi/ic_play_light.png
Binary files differ
diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml
index c3ee4d3e2..04ad7ea64 100644
--- a/core/src/main/res/values/attrs.xml
+++ b/core/src/main/res/values/attrs.xml
@@ -50,6 +50,7 @@
<attr name="ic_sort" format="reference"/>
<attr name="ic_sd_storage" format="reference"/>
<attr name="ic_create_new_folder" format="reference"/>
+ <attr name="ic_cast_disconnect" format="reference"/>
<!-- Used in itemdescription -->
<attr name="non_transparent_background" format="reference"/>
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 9026e2129..4b0f15df6 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -403,6 +403,8 @@
<string name="pref_faq">FAQ</string>
<string name="pref_known_issues">Known issues</string>
<string name="pref_no_browser_found">No web browser found.</string>
+ <string name="pref_cast_title">Cast support</string>
+ <string name="pref_cast_message">Enable support for remote media playback on Cast devices (such as Chromecast, Audio Speakers or Android TV)</string>
<!-- Auto-Flattr dialog -->
<string name="auto_flattr_enable">Enable automatic flattring</string>
@@ -614,4 +616,20 @@
<string name="proxy_host_invalid_error">Host is not a valid IP address or domain</string>
<string name="proxy_port_invalid_error">Port not valid</string>
+ <!-- Casting -->
+ <string name="cast_media_route_menu_title">Play on&#8230;</string>
+ <string name="cast_disconnect_label">Disconnect the cast session</string>
+ <string name="cast_not_castable">Media selected is not compatible with cast device</string>
+ <string name="cast_failed_to_play">Failed to start the playback of media</string>
+ <string name="cast_failed_to_stop">Failed to stop the playback of media</string>
+ <string name="cast_failed_to_pause">Failed to pause the playback of media</string>
+ <!--<string name="cast_failed_to_connect">Could not connect to the device</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_perform_action">Failed to perform the action</string>
+ <string name="cast_failed_status_request">Failed to sync up with the cast device</string>
+ <string name="cast_failed_seek">Failed to seek to the new position on the cast device</string>
+ <string name="cast_failed_receiver_player_error">Receiver player has encountered a severe error</string>
+ <string name="cast_failed_media_error_skipping">Error playing media. Skipping&#8230;</string>
</resources>
diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml
index f7775a0bf..6a4dc4781 100644
--- a/core/src/main/res/values/styles.xml
+++ b/core/src/main/res/values/styles.xml
@@ -57,6 +57,7 @@
<item name="attr/ic_sort">@drawable/ic_sort_grey600_24dp</item>
<item name="attr/ic_sd_storage">@drawable/ic_sd_storage_grey600_36dp</item>
<item name="attr/ic_create_new_folder">@drawable/ic_create_new_folder_grey600_24dp</item>
+ <item name="attr/ic_cast_disconnect">@drawable/ic_cast_disconnect_grey600_36dp</item>
</style>
<style name="Theme.AntennaPod.Dark" parent="Theme.AppCompat">
@@ -115,6 +116,7 @@
<item name="attr/ic_sort">@drawable/ic_sort_white_24dp</item>
<item name="attr/ic_sd_storage">@drawable/ic_sd_storage_white_36dp</item>
<item name="attr/ic_create_new_folder">@drawable/ic_create_new_folder_white_24dp</item>
+ <item name="attr/ic_cast_disconnect">@drawable/ic_cast_disconnect_white_36dp</item>
</style>
<style name="Theme.AntennaPod.Light.NoTitle" parent="Theme.AppCompat.Light.NoActionBar">
@@ -174,6 +176,7 @@
<item name="attr/ic_sort">@drawable/ic_sort_grey600_24dp</item>
<item name="attr/ic_sd_storage">@drawable/ic_sd_storage_grey600_36dp</item>
<item name="attr/ic_create_new_folder">@drawable/ic_create_new_folder_grey600_24dp</item>
+ <item name="attr/ic_cast_disconnect">@drawable/ic_cast_disconnect_grey600_36dp</item>
</style>
<style name="Theme.AntennaPod.Dark.NoTitle" parent="Theme.AppCompat.NoActionBar">
@@ -233,6 +236,7 @@
<item name="attr/ic_sort">@drawable/ic_sort_white_24dp</item>
<item name="attr/ic_sd_storage">@drawable/ic_sd_storage_white_36dp</item>
<item name="attr/ic_create_new_folder">@drawable/ic_create_new_folder_white_24dp</item>
+ <item name="attr/ic_cast_disconnect">@drawable/ic_cast_disconnect_white_36dp</item>
</style>
<style name="Theme.AntennaPod.VideoPlayer" parent="@style/Theme.AntennaPod.Dark">