Merge pull request #1879 from domingos86/chromecast-issue-340
Chromecast issue 340 initial PR
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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,43 +38,66 @@ public class AudioplayerActivity extends MediaplayerActivity implements NavDrawe
|
||||
launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY,
|
||||
true);
|
||||
startService(launchIntent);
|
||||
}
|
||||
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) {
|
||||
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 if (PlaybackService.isCasting()) {
|
||||
Intent intent = PlaybackService.getPlayerActivityIntent(this);
|
||||
if (!intent.getComponent().getClassName().equals(AudioplayerActivity.class.getName())) {
|
||||
saveCurrentFragment();
|
||||
finish();
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void clearStatusMsg() {
|
||||
// TODO Hide progress bar here
|
||||
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));
|
||||
|
||||
} else {
|
||||
super.onReloadNotification(notificationCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupGUI() {
|
||||
@ -221,412 +105,49 @@ public class AudioplayerActivity extends MediaplayerActivity implements NavDrawe
|
||||
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(butCastDisconnect != null) {
|
||||
butCastDisconnect.setVisibility(View.GONE);
|
||||
}
|
||||
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
|
||||
navList = (ListView) findViewById(R.id.nav_list);
|
||||
navDrawer = findViewById(R.id.nav_layout);
|
||||
if(butPlaybackSpeed != null) {
|
||||
butPlaybackSpeed.setOnClickListener(v -> {
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
if (controller.canSetPlaybackSpeed()) {
|
||||
String[] availableSpeeds = UserPreferences.getPlaybackSpeedArray();
|
||||
String currentSpeed = UserPreferences.getPlaybackSpeed();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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()) {
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
@ -506,6 +504,26 @@ public class MainActivity extends AppCompatActivity implements NavDrawerActivity
|
||||
Glide.get(this).clearMemory();
|
||||
}
|
||||
|
||||
@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)) {
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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
|
10
app/src/main/res/menu/cast_enabled.xml
Normal file
@ -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>
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
1766
core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java
Normal file
317
core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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";
|
||||
/**
|
||||
@ -73,6 +86,10 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
|
||||
* Parcelable of type Playable.
|
||||
*/
|
||||
public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra";
|
||||
/**
|
||||
* True if cast session should disconnect.
|
||||
*/
|
||||
public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect";
|
||||
/**
|
||||
* True if media should be streamed.
|
||||
*/
|
||||
@ -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;
|
||||
@ -153,12 +171,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.
|
||||
*/
|
||||
@ -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();
|
||||
}
|
||||
@ -306,23 +366,18 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
@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;
|
||||
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();
|
||||
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 (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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -852,11 +918,6 @@ public class PlaybackService extends Service implements SharedPreferences.OnShar
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,37 +967,68 @@ 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.
|
||||
*/
|
||||
@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -0,0 +1,5 @@
|
||||
package de.danoeh.antennapod.core.util;
|
||||
|
||||
public interface Supplier<T> {
|
||||
T get();
|
||||
}
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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()))) {
|
||||
|
BIN
core/src/main/res/drawable-hdpi/ic_audiotrack_light.png
Normal file
After Width: | Height: | Size: 417 B |
BIN
core/src/main/res/drawable-hdpi/ic_cast_disabled_light.png
Normal file
After Width: | Height: | Size: 770 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 968 B |
BIN
core/src/main/res/drawable-hdpi/ic_cast_light.png
Normal file
After Width: | Height: | Size: 975 B |
BIN
core/src/main/res/drawable-hdpi/ic_cast_off_light.png
Normal file
After Width: | Height: | Size: 867 B |
BIN
core/src/main/res/drawable-hdpi/ic_cast_on_0_light.png
Normal file
After Width: | Height: | Size: 961 B |
BIN
core/src/main/res/drawable-hdpi/ic_cast_on_1_light.png
Normal file
After Width: | Height: | Size: 979 B |
BIN
core/src/main/res/drawable-hdpi/ic_cast_on_2_light.png
Normal file
After Width: | Height: | Size: 976 B |
BIN
core/src/main/res/drawable-hdpi/ic_cast_on_light.png
Normal file
After Width: | Height: | Size: 982 B |
BIN
core/src/main/res/drawable-hdpi/ic_close_light.png
Normal file
After Width: | Height: | Size: 493 B |
BIN
core/src/main/res/drawable-hdpi/ic_media_cast_disconnect.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
core/src/main/res/drawable-hdpi/ic_pause_light.png
Normal file
After Width: | Height: | Size: 191 B |
BIN
core/src/main/res/drawable-hdpi/ic_play_light.png
Normal file
After Width: | Height: | Size: 562 B |
BIN
core/src/main/res/drawable-mdpi/ic_audiotrack_light.png
Normal file
After Width: | Height: | Size: 333 B |
BIN
core/src/main/res/drawable-mdpi/ic_cast_disabled_light.png
Normal file
After Width: | Height: | Size: 536 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 694 B |
BIN
core/src/main/res/drawable-mdpi/ic_cast_light.png
Normal file
After Width: | Height: | Size: 693 B |
BIN
core/src/main/res/drawable-mdpi/ic_cast_off_light.png
Normal file
After Width: | Height: | Size: 635 B |
BIN
core/src/main/res/drawable-mdpi/ic_cast_on_0_light.png
Normal file
After Width: | Height: | Size: 684 B |
BIN
core/src/main/res/drawable-mdpi/ic_cast_on_1_light.png
Normal file
After Width: | Height: | Size: 696 B |
BIN
core/src/main/res/drawable-mdpi/ic_cast_on_2_light.png
Normal file
After Width: | Height: | Size: 690 B |
BIN
core/src/main/res/drawable-mdpi/ic_cast_on_light.png
Normal file
After Width: | Height: | Size: 694 B |
BIN
core/src/main/res/drawable-mdpi/ic_close_light.png
Normal file
After Width: | Height: | Size: 379 B |
BIN
core/src/main/res/drawable-mdpi/ic_media_cast_disconnect.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
core/src/main/res/drawable-mdpi/ic_pause_light.png
Normal file
After Width: | Height: | Size: 280 B |
BIN
core/src/main/res/drawable-mdpi/ic_play_light.png
Normal file
After Width: | Height: | Size: 447 B |
BIN
core/src/main/res/drawable-xhdpi/ic_audiotrack_light.png
Normal file
After Width: | Height: | Size: 447 B |
BIN
core/src/main/res/drawable-xhdpi/ic_cast_disabled_light.png
Normal file
After Width: | Height: | Size: 976 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.3 KiB |
BIN
core/src/main/res/drawable-xhdpi/ic_cast_light.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
core/src/main/res/drawable-xhdpi/ic_cast_off_light.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
core/src/main/res/drawable-xhdpi/ic_cast_on_0_light.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
core/src/main/res/drawable-xhdpi/ic_cast_on_1_light.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
core/src/main/res/drawable-xhdpi/ic_cast_on_2_light.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
core/src/main/res/drawable-xhdpi/ic_cast_on_light.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
core/src/main/res/drawable-xhdpi/ic_close_light.png
Normal file
After Width: | Height: | Size: 526 B |
BIN
core/src/main/res/drawable-xhdpi/ic_media_cast_disconnect.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
core/src/main/res/drawable-xhdpi/ic_pause_light.png
Normal file
After Width: | Height: | Size: 221 B |
BIN
core/src/main/res/drawable-xhdpi/ic_play_light.png
Normal file
After Width: | Height: | Size: 678 B |
BIN
core/src/main/res/drawable-xxhdpi/ic_audiotrack_light.png
Normal file
After Width: | Height: | Size: 584 B |
BIN
core/src/main/res/drawable-xxhdpi/ic_cast_disabled_light.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.9 KiB |
BIN
core/src/main/res/drawable-xxhdpi/ic_cast_light.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
core/src/main/res/drawable-xxhdpi/ic_cast_off_light.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
core/src/main/res/drawable-xxhdpi/ic_cast_on_0_light.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
core/src/main/res/drawable-xxhdpi/ic_cast_on_1_light.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
core/src/main/res/drawable-xxhdpi/ic_cast_on_2_light.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
core/src/main/res/drawable-xxhdpi/ic_cast_on_light.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
core/src/main/res/drawable-xxhdpi/ic_close_light.png
Normal file
After Width: | Height: | Size: 673 B |
BIN
core/src/main/res/drawable-xxhdpi/ic_media_cast_disconnect.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
core/src/main/res/drawable-xxhdpi/ic_pause_light.png
Normal file
After Width: | Height: | Size: 317 B |
BIN
core/src/main/res/drawable-xxhdpi/ic_play_light.png
Normal file
After Width: | Height: | Size: 955 B |
BIN
core/src/main/res/drawable-xxxhdpi/ic_close_light.png
Normal file
After Width: | Height: | Size: 805 B |