diff options
author | ByteHamster <info@bytehamster.com> | 2019-10-02 16:03:11 +0200 |
---|---|---|
committer | ByteHamster <info@bytehamster.com> | 2019-10-02 16:03:11 +0200 |
commit | 71a5a00fd088ede5b6c0bddc773e04f18f5b9be9 (patch) | |
tree | 0e7aeb0db7532540d2fa66089d5e0820590e5087 /core | |
parent | cade85b9c2d9fc5c0eb3bfbdd7c6f5f91bf2b23f (diff) | |
parent | 2db5c00d667f88eddf210b817d380ed5bdf0ccd0 (diff) | |
download | AntennaPod-71a5a00fd088ede5b6c0bddc773e04f18f5b9be9.zip |
Merge branch 'develop' into notification-default-icon
Diffstat (limited to 'core')
42 files changed, 486 insertions, 361 deletions
diff --git a/core/build.gradle b/core/build.gradle index 133f1b262..8614d5589 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -76,7 +76,6 @@ dependencies { annotationProcessor "org.greenrobot:eventbus-annotation-processor:$eventbusVersion" implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" - implementation "org.awaitility:awaitility:$awaitilityVersion" implementation "com.google.android.exoplayer:exoplayer:$exoPlayerVersion" implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" @@ -92,6 +91,7 @@ dependencies { System.out.println("core: free build hack, skipping some dependencies") } + testImplementation "org.awaitility:awaitility:$awaitilityVersion" testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:1.10.19' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java index 124fd3e64..24a71ec96 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java @@ -25,4 +25,8 @@ public class DownloadEvent { "update=" + update + '}'; } + + public boolean hasChangedFeedUpdateStatus(boolean oldStatus) { + return oldStatus != update.feedIds.length > 0; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java new file mode 100644 index 000000000..3327d8a02 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/PlaybackPositionEvent.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.event; + +public class PlaybackPositionEvent { + private final int position; + private final int duration; + + public PlaybackPositionEvent(int position, int duration) { + this.position = position; + this.duration = duration; + } + + public int getPosition() { + return position; + } + + public int getDuration() { + return duration; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java deleted file mode 100644 index 3769d6bb1..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.danoeh.antennapod.core.event; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - -public class ProgressEvent { - - public enum Action { - START, END - } - - public final Action action; - public final String message; - - private ProgressEvent(Action action, String message) { - this.action = action; - this.message = message; - } - - public static ProgressEvent start(String message) { - return new ProgressEvent(Action.START, message); - } - - public static ProgressEvent end() { - return new ProgressEvent(Action.END, null); - } - - @Override - public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) - .append("action", action) - .append("message", message) - .toString(); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java index 3495164a6..744e9b924 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -218,6 +218,8 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR return itemIdentifier; } else if (title != null && !title.isEmpty()) { return title; + } else if (hasMedia() && media.getDownload_url() != null) { + return media.getDownload_url(); } else { return link; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java index bb34e2c0f..3dd87cc0b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java @@ -92,11 +92,9 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { @Nullable @Override public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) { - Log.d(TAG, "buildLoadData() called with: " + "model = [" + model + "], width = [" - + width + "], height = [" + height + "]"); - if(TextUtils.isEmpty(model)) { + if (TextUtils.isEmpty(model)) { return null; - } else if(model.startsWith("/")) { + } else if (model.startsWith("/")) { return new LoadData<>(new ObjectKey(model), new AudioCoverFetcher(model)); } else { GlideUrl url = new GlideUrl(model); diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java index a4cc22d8b..f2c0c8fe3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -4,7 +4,12 @@ import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.util.Log; import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.service.playback.PlayerStatus; +import de.danoeh.antennapod.core.util.playback.Playable; /** * Provides access to preferences set by the playback service. A private @@ -19,35 +24,35 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference * Contains the feed id of the currently playing item if it is a FeedMedia * object. */ - public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId"; + private static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId"; /** * Contains the id of the currently playing FeedMedia object or * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object. */ - public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId"; + private static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId"; /** * Type of the media object that is currently being played. This preference * is set to NO_MEDIA_PLAYING after playback has been completed and is set * as soon as the 'play' button is pressed. */ - public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia"; + private static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia"; /** * True if last played media was streamed. */ - public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream"; + private static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream"; /** * True if last played media was a video. */ - public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo"; + private static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo"; /** * The current player status as int. */ - public static final String PREF_CURRENT_PLAYER_STATUS = "de.danoeh.antennapod.preferences.currentPlayerStatus"; + private static final String PREF_CURRENT_PLAYER_STATUS = "de.danoeh.antennapod.preferences.currentPlayerStatus"; /** * Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. @@ -87,10 +92,6 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference } } - public static long getLastPlayedFeedId() { - return prefs.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1); - } - public static long getCurrentlyPlayingMedia() { return prefs.getLong(PREF_CURRENTLY_PLAYING_MEDIA, NO_MEDIA_PLAYING); } @@ -119,4 +120,52 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference editor.putInt(PREF_CURRENT_PLAYER_STATUS, PLAYER_STATUS_OTHER); editor.apply(); } + + public static void writeMediaPlaying(Playable playable, PlayerStatus playerStatus, boolean stream) { + Log.d(TAG, "Writing playback preferences"); + SharedPreferences.Editor editor = prefs.edit(); + + if (playable == null) { + writeNoMediaPlaying(); + } else { + editor.putLong(PREF_CURRENTLY_PLAYING_MEDIA, playable.getPlayableType()); + editor.putBoolean(PREF_CURRENT_EPISODE_IS_STREAM, stream); + editor.putBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, playable.getMediaType() == MediaType.VIDEO); + if (playable instanceof FeedMedia) { + FeedMedia fMedia = (FeedMedia) playable; + editor.putLong(PREF_CURRENTLY_PLAYING_FEED_ID, fMedia.getItem().getFeed().getId()); + editor.putLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, fMedia.getId()); + } else { + editor.putLong(PREF_CURRENTLY_PLAYING_FEED_ID, NO_MEDIA_PLAYING); + editor.putLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); + } + playable.writeToPreferences(editor); + } + editor.putInt(PREF_CURRENT_PLAYER_STATUS, getCurrentPlayerStatusAsInt(playerStatus)); + + editor.apply(); + } + + public static void writePlayerStatus(PlayerStatus playerStatus) { + Log.d(TAG, "Writing player status playback preferences"); + + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(PREF_CURRENT_PLAYER_STATUS, getCurrentPlayerStatusAsInt(playerStatus)); + editor.apply(); + } + + private static int getCurrentPlayerStatusAsInt(PlayerStatus playerStatus) { + int playerStatusAsInt; + switch (playerStatus) { + case PLAYING: + playerStatusAsInt = PLAYER_STATUS_PLAYING; + break; + case PAUSED: + playerStatusAsInt = PLAYER_STATUS_PAUSED; + break; + default: + playerStatusAsInt = PLAYER_STATUS_OTHER; + } + return playerStatusAsInt; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index a06047229..787e32ccc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -622,7 +622,7 @@ public class UserPreferences { .apply(); // when updating with an interval, we assume the user wants // to update *now* and then every 'hours' interval thereafter. - restartUpdateAlarm(); + AutoUpdateManager.restartUpdateAlarm(); } /** @@ -632,7 +632,7 @@ public class UserPreferences { prefs.edit() .putString(PREF_UPDATE_INTERVAL, hourOfDay + ":" + minute) .apply(); - restartUpdateAlarm(); + AutoUpdateManager.restartUpdateAlarm(); } public static void disableAutoUpdate() { @@ -882,19 +882,6 @@ public class UserPreferences { return getUpdateTimeOfDay().length == 2; } - public static void restartUpdateAlarm() { - if (isAutoUpdateDisabled()) { - AutoUpdateManager.disableAutoUpdate(); - } else if (isAutoUpdateTimeOfDay()) { - int[] timeOfDay = getUpdateTimeOfDay(); - Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay)); - AutoUpdateManager.restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]); - } else { - long milliseconds = getUpdateInterval(); - AutoUpdateManager.restartUpdateIntervalAlarm(milliseconds); - } - } - /** * Evaluates whether Cast support (Chromecast, Audio Cast, etc) is enabled on the preferences. */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java index 126f12247..af0a0d426 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -6,8 +6,7 @@ import android.content.Intent; import android.util.Log; import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.FeedUpdateUtils; +import de.danoeh.antennapod.core.util.download.AutoUpdateManager; /** * Refreshes all feeds when it receives an intent @@ -20,7 +19,8 @@ public class FeedUpdateReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received intent"); ClientConfig.initialize(context); - FeedUpdateUtils.startAutoUpdate(context, null); + + AutoUpdateManager.runOnce(); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java index efdb96dc1..87c18227b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java @@ -2,17 +2,23 @@ package de.danoeh.antennapod.core.service; import android.content.Context; import android.support.annotation.NonNull; +import android.util.Log; + import androidx.work.Worker; import androidx.work.WorkerParameters; + import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.FeedUpdateUtils; -import org.awaitility.Awaitility; - -import java.util.concurrent.atomic.AtomicBoolean; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.download.AutoUpdateManager; public class FeedUpdateWorker extends Worker { + private static final String TAG = "FeedUpdateWorker"; + + public static final String PARAM_RUN_ONCE = "runOnce"; + public FeedUpdateWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); } @@ -20,16 +26,20 @@ public class FeedUpdateWorker extends Worker { @Override @NonNull public Result doWork() { + final boolean isRunOnce = getInputData().getBoolean(PARAM_RUN_ONCE, false); + Log.d(TAG, "doWork() : isRunOnce = " + isRunOnce); ClientConfig.initialize(getApplicationContext()); - AtomicBoolean finished = new AtomicBoolean(false); - FeedUpdateUtils.startAutoUpdate(getApplicationContext(), () -> finished.set(true)); - Awaitility.await().until(finished::get); + if (NetworkUtils.networkAvailable() && NetworkUtils.isFeedRefreshAllowed()) { + DBTasks.refreshAllFeeds(getApplicationContext()); + } else { + Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed"); + } - if (UserPreferences.isAutoUpdateTimeOfDay()) { + if (!isRunOnce && UserPreferences.isAutoUpdateTimeOfDay()) { // WorkManager does not allow to set specific time for repeated tasks. // We repeatedly schedule a OneTimeWorkRequest instead. - UserPreferences.restartUpdateAlarm(); + AutoUpdateManager.restartUpdateAlarm(); } return Result.success(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index 0f346893e..b429740bf 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -13,6 +13,8 @@ import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.v4.app.NotificationCompat; import android.text.TextUtils; import android.util.Log; @@ -190,10 +192,8 @@ public class DownloadService extends Service { handleFailedDownload(status, downloader.getDownloadRequest()); if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - long id = status.getFeedfileId(); - FeedMedia media = DBReader.getFeedMedia(id); - FeedItem item; - if (media == null || (item = media.getItem()) == null) { + FeedItem item = getFeedItemFromId(status.getFeedfileId()); + if (item == null) { return; } boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR @@ -213,9 +213,8 @@ public class DownloadService extends Service { // if FeedMedia download has been canceled, fake FeedItem update // so that lists reload that it if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); - FeedItem item; - if (media == null || (item = media.getItem()) == null) { + FeedItem item = getFeedItemFromId(status.getFeedfileId()); + if (item == null) { return; } EventBus.getDefault().post(FeedItemEvent.updated(item)); @@ -386,6 +385,12 @@ public class DownloadService extends Service { Downloader d = getDownloader(url); if (d != null) { d.cancel(); + DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); + + FeedItem item = getFeedItemFromId(d.getDownloadRequest().getFeedfileId()); + if (item != null) { + EventBus.getDefault().post(FeedItemEvent.updated(item)); + } } else { Log.e(TAG, "Could not cancel download with url " + url); } @@ -430,12 +435,40 @@ public class DownloadService extends Service { queryDownloads(); } - private Downloader getDownloader(DownloadRequest request) { - if (!URLUtil.isHttpUrl(request.getSource()) && !URLUtil.isHttpsUrl(request.getSource())) { - Log.e(TAG, "Could not find appropriate downloader for " + request.getSource()); - return null; + @VisibleForTesting + public interface DownloaderFactory { + @Nullable + Downloader create(@NonNull DownloadRequest request); + } + + private static class DefaultDownloaderFactory implements DownloaderFactory { + @Nullable + @Override + public Downloader create(@NonNull DownloadRequest request) { + if (!URLUtil.isHttpUrl(request.getSource()) && !URLUtil.isHttpsUrl(request.getSource())) { + Log.e(TAG, "Could not find appropriate downloader for " + request.getSource()); + return null; + } + return new HttpDownloader(request); } - return new HttpDownloader(request); + } + + private static DownloaderFactory downloaderFactory = new DefaultDownloaderFactory(); + + @VisibleForTesting + public static DownloaderFactory getDownloaderFactory() { + return downloaderFactory; + } + + // public scope rather than package private, + // because androidTest put classes in the non-standard de.test.antennapod hierarchy + @VisibleForTesting + public static void setDownloaderFactory(DownloaderFactory downloaderFactory) { + DownloadService.downloaderFactory = downloaderFactory; + } + + private Downloader getDownloader(@NonNull DownloadRequest request) { + return downloaderFactory.create(request); } /** @@ -578,6 +611,16 @@ public class DownloadService extends Service { syncExecutor.execute(new FailedDownloadHandler(status, request)); } + @Nullable + private FeedItem getFeedItemFromId(long id) { + FeedMedia media = DBReader.getFeedMedia(id); + if (media != null) { + return media.getItem(); + } else { + return null; + } + } + /** * Takes a single Feed, parses the corresponding file and refreshes * information in the manager @@ -654,6 +697,7 @@ public class DownloadService extends Service { Log.e(TAG, "FeedSyncThread was interrupted"); } catch (ExecutionException e) { Log.e(TAG, "ExecutionException in FeedSyncThread: " + e.getMessage()); + e.printStackTrace(); } } @@ -688,6 +732,7 @@ public class DownloadService extends Service { Log.e(TAG, "FeedSyncThread was interrupted"); } catch (ExecutionException e) { Log.e(TAG, "ExecutionException in FeedSyncThread: " + e.getMessage()); + e.printStackTrace(); } } @@ -983,14 +1028,17 @@ public class DownloadService extends Service { final FeedItem item = media.getItem(); try { + DBWriter.setFeedMedia(media).get(); + // we've received the media, we don't want to autodownload it again if (item != null) { item.setAutoDownload(false); + // setFeedItem() signals (via EventBus) that the item has been updated, + // so we do it after the enclosing media has been updated above, + // to ensure subscribers will get the updated FeedMedia as well DBWriter.setFeedItem(item).get(); } - DBWriter.setFeedMedia(media).get(); - if (item != null && UserPreferences.enqueueDownloadedEpisodes() && !DBTasks.isInQueue(DownloadService.this, item.getId())) { DBWriter.addQueueItem(DownloadService.this, item).get(); @@ -1058,7 +1106,13 @@ public class DownloadService extends Service { private final Runnable postDownloaderTask = new Runnable() { @Override public void run() { - List<Downloader> list = Collections.unmodifiableList(downloads); + List<Downloader> runningDownloads = new ArrayList<>(); + for (Downloader downloader : downloads) { + if (!downloader.cancelled) { + runningDownloads.add(downloader); + } + } + List<Downloader> list = Collections.unmodifiableList(runningDownloads); EventBus.getDefault().postSticky(DownloadEvent.refresh(list)); postHandler.postDelayed(postDownloaderTask, 1500); } @@ -1076,6 +1130,9 @@ public class DownloadService extends Service { private static String compileNotificationString(List<Downloader> downloads) { List<String> lines = new ArrayList<>(downloads.size()); for (Downloader downloader : downloads) { + if (downloader.cancelled) { + continue; + } StringBuilder line = new StringBuilder("• "); DownloadRequest request = downloader.getDownloadRequest(); switch (request.getFeedfileType()) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java index 7af33f8f9..c988ef70e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.service.playback; import android.content.Context; import android.net.Uri; -import android.os.Handler; +import android.util.Log; import android.view.SurfaceHolder; import com.google.android.exoplayer2.C; @@ -23,38 +23,42 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.Util; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; import org.antennapod.audio.MediaPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer; +import java.util.concurrent.TimeUnit; + public class ExoPlayerWrapper implements IPlayer { + private static final String TAG = "ExoPlayerWrapper"; private final Context mContext; + private final Disposable bufferingUpdateDisposable; private SimpleExoPlayer mExoPlayer; private MediaSource mediaSource; private MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener; private MediaPlayer.OnCompletionListener audioCompletionListener; private MediaPlayer.OnErrorListener audioErrorListener; private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener; - private boolean shouldCheckBufferingUpdates = true; ExoPlayerWrapper(Context context) { mContext = context; mExoPlayer = createPlayer(); - Handler handler = new Handler(); // Main thread - handler.postDelayed(new Runnable() { - @Override - public void run() { - if (bufferingUpdateListener != null) { - bufferingUpdateListener.onBufferingUpdate(null, mExoPlayer.getBufferedPercentage()); - } - if (shouldCheckBufferingUpdates) { - handler.postDelayed(this, 2000); - } - } - }, 2000); + bufferingUpdateDisposable = Observable.interval(2, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(aLong -> { + if (bufferingUpdateListener != null) { + bufferingUpdateListener.onBufferingUpdate(null, mExoPlayer.getBufferedPercentage()); + } + }); } private SimpleExoPlayer createPlayer() { @@ -62,6 +66,7 @@ public class ExoPlayerWrapper implements IPlayer { loadControl.setBufferDurationsMs(30000, 120000, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); + loadControl.setBackBuffer(UserPreferences.getRewindSecs() * 1000 + 500, true); SimpleExoPlayer p = ExoPlayerFactory.newSimpleInstance(mContext, new DefaultRenderersFactory(mContext), new DefaultTrackSelector(), loadControl.createDefaultLoadControl()); p.setSeekParameters(SeekParameters.PREVIOUS_SYNC); @@ -168,7 +173,7 @@ public class ExoPlayerWrapper implements IPlayer { @Override public void release() { - shouldCheckBufferingUpdates = false; + bufferingUpdateDisposable.dispose(); if (mExoPlayer != null) { mExoPlayer.release(); } @@ -202,8 +207,13 @@ public class ExoPlayerWrapper implements IPlayer { @Override public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { - DataSource.Factory dataSourceFactory = - new DefaultDataSourceFactory(mContext, Util.getUserAgent(mContext, mContext.getPackageName()), null); + Log.d(TAG, "setDataSource: " + s); + DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory( + Util.getUserAgent(mContext, mContext.getPackageName()), null, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, + true); + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(mContext, null, httpDataSourceFactory); ExtractorMediaSource.Factory f = new ExtractorMediaSource.Factory(dataSourceFactory); mediaSource = f.createMediaSource(Uri.parse(s)); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index 9164f561f..f8f095ea8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -584,7 +584,6 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } playerLock.unlock(); - Log.d(TAG, "getPosition() -> " + retVal); return retVal; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index ace89e40a..e58d5cb31 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -12,7 +12,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; @@ -45,10 +44,12 @@ import com.bumptech.glide.request.target.Target; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.MessageEvent; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; @@ -66,7 +67,6 @@ import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.FeedSearcher; -import de.danoeh.antennapod.core.util.IntList; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.QueueAccess; @@ -74,6 +74,9 @@ import de.danoeh.antennapod.core.util.gui.NotificationUtils; import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; import org.greenrobot.eventbus.EventBus; /** @@ -212,6 +215,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { private PlaybackServiceTaskManager taskManager; private PlaybackServiceFlavorHelper flavorHelper; private PlaybackServiceStateManager stateManager; + private Disposable positionEventTimer; /** * Used for Lollipop notifications, Android Wear, and Android Auto. @@ -331,8 +335,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { isRunning = false; currentMediaType = MediaType.UNKNOWN; - PreferenceManager.getDefaultSharedPreferences(this) - .unregisterOnSharedPreferenceChangeListener(prefListener); + cancelPositionObserver(); + PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(prefListener); if (mediaSession != null) { mediaSession.release(); } @@ -451,7 +455,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationBuilder.loadIcon(getPlayable()); } } - startForeground(NOTIFICATION_ID, notificationBuilder.build()); + stateManager.startForeground(NOTIFICATION_ID, notificationBuilder.build()); } NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); @@ -566,7 +570,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); + mediaPlayer.pause(!UserPreferences.isPersistNotify(), false); } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { mediaPlayer.resume(); } else if (status == PlayerStatus.PREPARING) { @@ -590,7 +594,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { return true; case KeyEvent.KEYCODE_MEDIA_PAUSE: if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); + mediaPlayer.pause(!UserPreferences.isPersistNotify(), false); } return true; @@ -717,7 +721,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { updateMediaSession(newInfo.playerStatus); switch (newInfo.playerStatus) { case INITIALIZED: - writePlaybackPreferences(); + PlaybackPreferences.writeMediaPlaying(mediaPlayer.getPSMPInfo().playable, + mediaPlayer.getPSMPInfo().playerStatus, mediaPlayer.isStreaming()); break; case PREPARED: @@ -734,7 +739,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { // remove notification on pause stateManager.stopForeground(true); } - writePlayerStatusPlaybackPreferences(); + cancelPositionObserver(); + PlaybackPreferences.writePlayerStatus(mediaPlayer.getPlayerStatus()); break; case STOPPED: @@ -743,8 +749,9 @@ public class PlaybackService extends MediaBrowserServiceCompat { break; case PLAYING: - writePlayerStatusPlaybackPreferences(); + PlaybackPreferences.writePlayerStatus(mediaPlayer.getPlayerStatus()); setupNotification(newInfo); + setupPositionUpdater(); stateManager.validStartCommandWasReceived(); // set sleep timer if auto-enabled if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && @@ -1023,82 +1030,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_disabled_label))); } - private int getCurrentPlayerStatusAsInt(PlayerStatus playerStatus) { - int playerStatusAsInt; - switch (playerStatus) { - case PLAYING: - playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PLAYING; - break; - case PAUSED: - playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PAUSED; - break; - default: - playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_OTHER; - } - return playerStatusAsInt; - } - - private void writePlaybackPreferences() { - Log.d(TAG, "Writing playback preferences"); - - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - MediaType mediaType = mediaPlayer.getCurrentMediaType(); - boolean stream = mediaPlayer.isStreaming(); - int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus); - - if (info.playable != null) { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - info.playable.getPlayableType()); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - stream); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, - mediaType == MediaType.VIDEO); - if (info.playable instanceof FeedMedia) { - FeedMedia fMedia = (FeedMedia) info.playable; - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - fMedia.getItem().getFeed().getId()); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - fMedia.getId()); - } else { - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - info.playable.writeToPreferences(editor); - } else { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - editor.putInt( - PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); - - editor.apply(); - } - - private void writePlayerStatusPlaybackPreferences() { - Log.d(TAG, "Writing player status playback preferences"); - - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - int playerStatus = getCurrentPlayerStatusAsInt(mediaPlayer.getPlayerStatus()); - editor.putInt(PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); - editor.apply(); - } - private void sendNotificationBroadcast(int type, int code) { Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); @@ -1653,6 +1584,24 @@ public class PlaybackService extends MediaBrowserServiceCompat { return mediaPlayer.getVideoSize(); } + private void setupPositionUpdater() { + if (positionEventTimer != null) { + positionEventTimer.dispose(); + } + + Log.d(TAG, "Setting up position observer"); + positionEventTimer = Observable.interval(1, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(aLong -> + EventBus.getDefault().post(new PlaybackPositionEvent(getCurrentPosition(), getDuration()))); + } + + private void cancelPositionObserver() { + if (positionEventTimer != null) { + positionEventTimer.dispose(); + } + } + private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() { private static final String TAG = "MediaSessionCompat"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java index b9ad2afb0..39490dc10 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java @@ -141,7 +141,7 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build stopCastingIntent.putExtra(PlaybackService.EXTRA_CAST_DISCONNECT, true); PendingIntent stopCastingPendingIntent = PendingIntent.getService(context, numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT); - addAction(R.drawable.ic_media_cast_disconnect, + addAction(R.drawable.ic_notification_cast_off, context.getString(R.string.cast_disconnect_label), stopCastingPendingIntent); numActions++; @@ -150,7 +150,7 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build // always let them rewind PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_REWIND, numActions); - addAction(android.R.drawable.ic_media_rew, context.getString(R.string.rewind_label), rewindButtonPendingIntent); + addAction(R.drawable.ic_notification_fast_rewind, context.getString(R.string.rewind_label), rewindButtonPendingIntent); if (UserPreferences.showRewindOnCompactNotification()) { compactActionList.add(numActions); } @@ -159,14 +159,14 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build if (playerStatus == PlayerStatus.PLAYING) { PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_PAUSE, numActions); - addAction(android.R.drawable.ic_media_pause, //pause action + addAction(R.drawable.ic_notification_pause, //pause action context.getString(R.string.pause_label), pauseButtonPendingIntent); compactActionList.add(numActions++); } else { PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_PLAY, numActions); - addAction(android.R.drawable.ic_media_play, //play action + addAction(R.drawable.ic_notification_play, //play action context.getString(R.string.play_label), playButtonPendingIntent); compactActionList.add(numActions++); @@ -175,7 +175,7 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build // ff follows play, then we have skip (if it's present) PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions); - addAction(android.R.drawable.ic_media_ff, context.getString(R.string.fast_forward_label), ffButtonPendingIntent); + addAction(R.drawable.ic_notification_fast_forward, context.getString(R.string.fast_forward_label), ffButtonPendingIntent); if (UserPreferences.showFastForwardOnCompactNotification()) { compactActionList.add(numActions); } @@ -184,7 +184,7 @@ public class PlaybackServiceNotificationBuilder extends NotificationCompat.Build if (UserPreferences.isFollowQueue()) { PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_NEXT, numActions); - addAction(android.R.drawable.ic_media_next, + addAction(R.drawable.ic_notification_skip, context.getString(R.string.skip_episode_label), skipButtonPendingIntent); if (UserPreferences.showSkipOnCompactNotification()) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index 68839023e..1a13f9e9f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -7,6 +7,9 @@ import android.os.Vibrator; import android.support.annotation.NonNull; import android.util.Log; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -14,13 +17,12 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.Playable; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; @@ -102,6 +104,34 @@ public class PlaybackServiceTaskManager { } } + @Subscribe + public void onEvent(FeedItemEvent event) { + // Use case: when an item in the queue has been downloaded, + // listening to the event to ensure the downloaded item will be used. + Log.d(TAG, "onEvent(FeedItemEvent " + event + ")"); + + for (FeedItem item : event.items) { + if (isItemInQueue(item.getId())) { + Log.d(TAG, "onEvent(FeedItemEvent) - some item (" + item.getId() + ") in the queue has been updated (usually downloaded). Refresh the queue."); + cancelQueueLoader(); + loadQueue(); + return; + } + } + } + + private boolean isItemInQueue(long itemId) { + List<FeedItem> queue = getQueueIfLoaded(); + if (queue != null) { + for (FeedItem item : queue) { + if (item.getId() == itemId) { + return true; + } + } + } + return false; + } + /** * Returns the queue if it is already loaded or null if it hasn't been loaded yet. * In order to wait until the queue has been loaded, use getQueue() diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index 0fb181299..46fa4b99c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -3,7 +3,8 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; -import android.support.annotation.Nullable; +import android.os.Looper; +import android.text.TextUtils; import android.util.Log; import java.util.ArrayList; @@ -144,53 +145,36 @@ public final class DBTasks { private static final AtomicBoolean isRefreshing = new AtomicBoolean(false); /** - * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still + * Refreshes all feeds. + * It must not be from the main thread. + * This method might ignore subsequent calls if it is still * enqueuing Feeds for download from a previous call * * @param context Might be used for accessing the database - * @param feeds List of Feeds that should be refreshed. */ - public static void refreshAllFeeds(final Context context, final List<Feed> feeds) { - refreshAllFeeds(context, feeds, null); - } - - /** - * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still - * enqueuing Feeds for download from a previous call - * - * @param context Might be used for accessing the database - * @param feeds List of Feeds that should be refreshed. - * @param callback Called after everything was added enqueued for download. Might be null. - */ - public static void refreshAllFeeds(final Context context, final List<Feed> feeds, @Nullable Runnable callback) { + public static void refreshAllFeeds(final Context context) { if (!isRefreshing.compareAndSet(false, true)) { Log.d(TAG, "Ignoring request to refresh all feeds: Refresh lock is locked"); return; } - new Thread(() -> { - if (feeds != null) { - refreshFeeds(context, feeds); - } else { - refreshFeeds(context, DBReader.getFeedList()); - } - isRefreshing.set(false); + if (Looper.myLooper() == Looper.getMainLooper()) { + throw new IllegalStateException("DBTasks.refreshAllFeeds() must not be called from the main thread."); + } - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); - prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply(); + refreshFeeds(context, DBReader.getFeedList()); + isRefreshing.set(false); - if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { - GpodnetSyncService.sendSyncIntent(context); - } - // Note: automatic download of episodes will be done but not here. - // Instead it is done after all feeds have been refreshed (asynchronously), - // in DownloadService.onDestroy() - // See Issue #2577 for the details of the rationale + SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, MODE_PRIVATE); + prefs.edit().putLong(PREF_LAST_REFRESH, System.currentTimeMillis()).apply(); - if (callback != null) { - callback.run(); - } - }).start(); + if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { + GpodnetSyncService.sendSyncIntent(context); + } + // Note: automatic download of episodes will be done but not here. + // Instead it is done after all feeds have been refreshed (asynchronously), + // in DownloadService.onDestroy() + // See Issue #2577 for the details of the rationale } /** @@ -457,10 +441,9 @@ public final class DBTasks { /** * Get a FeedItem by its identifying value. */ - private static FeedItem searchFeedItemByIdentifyingValue(Feed feed, - String identifier) { + private static FeedItem searchFeedItemByIdentifyingValue(Feed feed, String identifier) { for (FeedItem item : feed.getItems()) { - if (item.getIdentifyingValue().equals(identifier)) { + if (TextUtils.equals(item.getIdentifyingValue(), identifier)) { return item; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index a3271bcf9..a1732e08f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -28,8 +28,6 @@ import java.util.Collections; import java.util.List; import java.util.Set; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.ProgressEvent; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; @@ -38,7 +36,6 @@ import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.LongIntMap; -import org.greenrobot.eventbus.EventBus; // TODO Remove media column from feeditem table @@ -1478,11 +1475,9 @@ public class PodDBAdapter { @Override public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { - EventBus.getDefault().post(ProgressEvent.start(context.getString(R.string.progress_upgrading_database))); Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " + newVersion + "."); DBUpgrader.upgrade(db, oldVersion, newVersion); - EventBus.getDefault().post(ProgressEvent.end()); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java deleted file mode 100644 index b425687ae..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedUpdateUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import android.content.Context; -import android.util.Log; - -import org.awaitility.core.ConditionTimeoutException; - -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.core.storage.DBTasks; - -import static org.awaitility.Awaitility.with; - -public class FeedUpdateUtils { - private static final String TAG = "FeedUpdateUtils"; - - private FeedUpdateUtils() {} - - public static void startAutoUpdate(Context context, Runnable callback) { - // the network check is blocking for possibly a long time: so run the logic - // in a separate thread to prevent the code blocking the callers - final Runnable runnable = () -> { - try { - with().pollInterval(1, TimeUnit.SECONDS) - .await() - .atMost(10, TimeUnit.SECONDS) - .until(() -> NetworkUtils.networkAvailable() && NetworkUtils.isFeedRefreshAllowed()); - DBTasks.refreshAllFeeds(context, null, callback); - } catch (ConditionTimeoutException ignore) { - Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed"); - } - }; - new Thread(runnable).start(); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java index e81ab47ed..656b518bf 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java @@ -1,13 +1,20 @@ package de.danoeh.antennapod.core.util; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; +import de.danoeh.antennapod.core.R; import java.util.List; public class IntentUtils { + private static final String TAG = "IntentUtils"; + private IntentUtils(){} /* @@ -28,4 +35,13 @@ public class IntentUtils { context.sendBroadcast(new Intent(action).setPackage(context.getPackageName())); } + public static void openInBrowser(Context context, String url) { + try { + Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + context.startActivity(myIntent); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.pref_no_browser_found, Toast.LENGTH_LONG).show(); + Log.e(TAG, Log.getStackTraceString(e)); + } + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java b/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java index 0fe11ec53..37f12c01c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Optional.java @@ -77,7 +77,7 @@ public final class Optional<T> { * @param <T> Type of the non-existent value * @return an empty {@code Optional} */ - public static<T> Optional<T> empty() { + public static <T> Optional<T> empty() { @SuppressWarnings("unchecked") Optional<T> t = (Optional<T>) EMPTY; return t; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java index 37172d042..0c21ca393 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java @@ -164,7 +164,7 @@ public class QueueSorter { Collections.sort(feedItems, itemComparator); if (spread == 0) { spread = feedItems.size(); - } else if (feedItems.size() % spread != 0){ + } else if (spread % feedItems.size() != 0){ spread *= feedItems.size(); } } @@ -180,6 +180,9 @@ public class QueueSorter { Map<Long, List<FeedItem>> spreadItems = new HashMap<>(); for (List<FeedItem> feedItems : feeds) { long thisSpread = spread / feedItems.size(); + if (thisSpread == 0) { + thisSpread = 1; + } // Starting from 0 ensures we front-load, so the queue starts with one episode from // each feed in the queue long itemSpread = 0; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java index 412b150fa..ebeec058d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java @@ -1,21 +1,29 @@ package de.danoeh.antennapod.core.util.download; +import android.content.Context; +import android.support.annotation.NonNull; import android.util.Log; + import androidx.work.Constraints; +import androidx.work.Data; import androidx.work.ExistingPeriodicWorkPolicy; import androidx.work.ExistingWorkPolicy; import androidx.work.NetworkType; import androidx.work.OneTimeWorkRequest; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.FeedUpdateWorker; +import java.util.Arrays; import java.util.Calendar; import java.util.concurrent.TimeUnit; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.FeedUpdateWorker; +import de.danoeh.antennapod.core.storage.DBTasks; + public class AutoUpdateManager { - private static final String WORK_ID_FEED_UPDATE = FeedUpdateWorker.class.getName(); + private static final String WORK_ID_FEED_UPDATE = "de.danoeh.antennapod.core.service.FeedUpdateWorker"; + private static final String WORK_ID_FEED_UPDATE_ONCE = WORK_ID_FEED_UPDATE + "Once"; private static final String TAG = "AutoUpdateManager"; private AutoUpdateManager() { @@ -23,9 +31,25 @@ public class AutoUpdateManager { } /** + * Start / restart periodic auto feed refresh + */ + public static void restartUpdateAlarm() { + if (UserPreferences.isAutoUpdateDisabled()) { + disableAutoUpdate(); + } else if (UserPreferences.isAutoUpdateTimeOfDay()) { + int[] timeOfDay = UserPreferences.getUpdateTimeOfDay(); + Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay)); + restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]); + } else { + long milliseconds = UserPreferences.getUpdateInterval(); + restartUpdateIntervalAlarm(milliseconds); + } + } + + /** * Sets the interval in which the feeds are refreshed automatically */ - public static void restartUpdateIntervalAlarm(long intervalMillis) { + private static void restartUpdateIntervalAlarm(long intervalMillis) { Log.d(TAG, "Restarting update alarm."); PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(FeedUpdateWorker.class, @@ -40,7 +64,7 @@ public class AutoUpdateManager { /** * Sets time of day the feeds are refreshed automatically */ - public static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute) { + private static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute) { Log.d(TAG, "Restarting update alarm."); Calendar now = Calendar.getInstance(); @@ -60,6 +84,41 @@ public class AutoUpdateManager { WorkManager.getInstance().enqueueUniqueWork(WORK_ID_FEED_UPDATE, ExistingWorkPolicy.REPLACE, workRequest); } + /** + * Run auto feed refresh once in background, as soon as what OS scheduling allows. + * + * Callers from UI should use {@link #runImmediate(Context)}, as it will guarantee + * the refresh be run immediately. + */ + public static void runOnce() { + Log.d(TAG, "Run auto update once, as soon as OS allows."); + + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) + .setConstraints(getConstraints()) + .setInitialDelay(0L, TimeUnit.MILLISECONDS) + .setInputData(new Data.Builder() + .putBoolean(FeedUpdateWorker.PARAM_RUN_ONCE, true) + .build() + ) + .build(); + + WorkManager.getInstance().enqueueUniqueWork(WORK_ID_FEED_UPDATE_ONCE, ExistingWorkPolicy.REPLACE, workRequest); + + } + + /** + /** + * Run auto feed refresh once in background immediately, using its own thread. + * + * Callers where the additional threads is not suitable should use {@link #runOnce()} + */ + public static void runImmediate(@NonNull Context context) { + Log.d(TAG, "Run auto update immediately in background."); + new Thread(() -> { + DBTasks.refreshAllFeeds(context.getApplicationContext()); + }, "ManualRefreshAllFeeds").start(); + } + public static void disableAutoUpdate() { WorkManager.getInstance().cancelUniqueWork(WORK_ID_FEED_UPDATE); } @@ -74,4 +133,5 @@ public class AutoUpdateManager { } return constraints.build(); } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/exception/RxJavaErrorHandlerSetup.java b/core/src/main/java/de/danoeh/antennapod/core/util/exception/RxJavaErrorHandlerSetup.java index 12f0c1c6e..223104d2e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/exception/RxJavaErrorHandlerSetup.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/exception/RxJavaErrorHandlerSetup.java @@ -1,12 +1,9 @@ package de.danoeh.antennapod.core.util.exception; import android.util.Log; -import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; import io.reactivex.exceptions.UndeliverableException; import io.reactivex.plugins.RxJavaPlugins; -import java.io.InterruptedIOException; - public class RxJavaErrorHandlerSetup { private RxJavaErrorHandlerSetup() { @@ -14,21 +11,14 @@ public class RxJavaErrorHandlerSetup { } public static void setupRxJavaErrorHandler() { - RxJavaPlugins.setErrorHandler(originalCause -> { - Throwable e = originalCause; + RxJavaPlugins.setErrorHandler(e -> { if (e instanceof UndeliverableException) { - e = e.getCause(); - } - if (e instanceof GpodnetServiceException) { - e = e.getCause(); - } - if (e instanceof InterruptedException || e instanceof InterruptedIOException) { - // fine, some blocking code was interrupted by a dispose call - Log.d("RxJavaErrorHandler", "Ignored exception: " + Log.getStackTraceString(originalCause)); + // Probably just disposed because the fragment was left + Log.d("RxJavaErrorHandler", "Ignored exception: " + Log.getStackTraceString(e)); return; } Thread.currentThread().getUncaughtExceptionHandler() - .uncaughtException(Thread.currentThread(), originalCause); + .uncaughtException(Thread.currentThread(), e); }); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java index d22d08e09..a3f747e09 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java @@ -72,7 +72,7 @@ public class ChapterReader extends ID3Reader { String decodedLink = URLDecoder.decode(link.toString(), "UTF-8"); currentChapter.setLink(decodedLink); Log.d(TAG, "Found link: " + currentChapter.getLink()); - } catch (IllegalArgumentException _iae) { + } catch (IllegalArgumentException iae) { Log.w(TAG, "Bad URL found in ID3 data"); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index ac5418dd0..1456ebd8d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -21,11 +21,10 @@ import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -69,9 +68,6 @@ public class PlaybackController { private final ScheduledThreadPoolExecutor schedExecutor; private static final int SCHED_EX_POOLSIZE = 1; - private MediaPositionObserver positionObserver; - private ScheduledFuture<?> positionObserverFuture; - private boolean mediaInfoLoaded = false; private boolean released = false; private boolean initialized = false; @@ -177,7 +173,6 @@ public class PlaybackController { } catch (IllegalArgumentException e) { // ignore } - cancelPositionObserver(); schedExecutor.shutdownNow(); media = null; released = true; @@ -254,29 +249,6 @@ public class PlaybackController { .getIntent()); } - - - private void setupPositionObserver() { - if (positionObserverFuture == null || - positionObserverFuture.isCancelled() || - positionObserverFuture.isDone()) { - - Log.d(TAG, "Setting up position observer"); - positionObserver = new MediaPositionObserver(); - positionObserverFuture = schedExecutor.scheduleWithFixedDelay( - positionObserver, MediaPositionObserver.WAITING_INTERVALL, - MediaPositionObserver.WAITING_INTERVALL, - TimeUnit.MILLISECONDS); - } - } - - private void cancelPositionObserver() { - if (positionObserverFuture != null) { - boolean result = positionObserverFuture.cancel(true); - Log.d(TAG, "PositionObserver cancelled. Result: " + result); - } - } - private final ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { if(service instanceof PlaybackService.LocalBinder) { @@ -337,7 +309,6 @@ public class PlaybackController { onBufferUpdate(progress); break; case PlaybackService.NOTIFICATION_TYPE_RELOAD: - cancelPositionObserver(); mediaInfoLoaded = false; queryService(); onReloadNotification(intent.getIntExtra( @@ -447,7 +418,6 @@ public class PlaybackController { case PAUSED: clearStatusMsg(); checkMediaInfoLoaded(); - cancelPositionObserver(); onPositionObserverUpdate(); updatePlayButtonAppearance(playResource, playText); if (!PlaybackService.isCasting() && @@ -463,7 +433,6 @@ public class PlaybackController { onAwaitingVideoSurface(); setScreenOn(true); } - setupPositionObserver(); updatePlayButtonAppearance(pauseResource, pauseText); break; case PREPARING: @@ -581,7 +550,6 @@ public class PlaybackController { */ public void onSeekBarStartTrackingTouch(SeekBar seekBar) { // interrupt position Observer, restart later - cancelPositionObserver(); } /** @@ -590,7 +558,6 @@ public class PlaybackController { public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { if (playbackService != null && media != null) { playbackService.seekTo((int) (prog * media.getDuration())); - setupPositionObserver(); } } @@ -836,19 +803,4 @@ public class PlaybackController { } }, error -> Log.e(TAG, Log.getStackTraceString(error))); } - - /** - * Refreshes the current position of the media file that is playing. - */ - public class MediaPositionObserver implements Runnable { - - static final int WAITING_INTERVALL = 1000; - - @Override - public void run() { - if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) { - activity.runOnUiThread(PlaybackController.this::onPositionObserverUpdate); - } - } - } } diff --git a/core/src/main/res/drawable-hdpi/ic_media_cast_disconnect.png b/core/src/main/res/drawable-hdpi/ic_media_cast_disconnect.png Binary files differdeleted file mode 100644 index 700c116e5..000000000 --- a/core/src/main/res/drawable-hdpi/ic_media_cast_disconnect.png +++ /dev/null diff --git a/core/src/main/res/drawable-mdpi/ic_media_cast_disconnect.png b/core/src/main/res/drawable-mdpi/ic_media_cast_disconnect.png Binary files differdeleted file mode 100644 index 767f420df..000000000 --- a/core/src/main/res/drawable-mdpi/ic_media_cast_disconnect.png +++ /dev/null diff --git a/core/src/main/res/drawable-xhdpi/ic_media_cast_disconnect.png b/core/src/main/res/drawable-xhdpi/ic_media_cast_disconnect.png Binary files differdeleted file mode 100644 index 740867129..000000000 --- a/core/src/main/res/drawable-xhdpi/ic_media_cast_disconnect.png +++ /dev/null diff --git a/core/src/main/res/drawable-xxhdpi/ic_media_cast_disconnect.png b/core/src/main/res/drawable-xxhdpi/ic_media_cast_disconnect.png Binary files differdeleted file mode 100644 index 2d2ec9035..000000000 --- a/core/src/main/res/drawable-xxhdpi/ic_media_cast_disconnect.png +++ /dev/null diff --git a/core/src/main/res/drawable/ic_notification_cast_off.xml b/core/src/main/res/drawable/ic_notification_cast_off.xml new file mode 100644 index 000000000..63a21fbe2 --- /dev/null +++ b/core/src/main/res/drawable/ic_notification_cast_off.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="24dp"> + <path android:fillColor="#FFFFFFFF" android:pathData="M1.6,1.27L0.25,2.75L1.41,3.8C1.16,4.13 1,4.55 1,5V8H3V5.23L18.2,19H14V21H20.41L22.31,22.72L23.65,21.24M6.5,3L8.7,5H21V16.14L23,17.95V5C23,3.89 22.1,3 21,3M1,10V12A9,9 0 0,1 10,21H12C12,14.92 7.08,10 1,10M1,14V16A5,5 0 0,1 6,21H8A7,7 0 0,0 1,14M1,18V21H4A3,3 0 0,0 1,18Z" /> +</vector>
\ No newline at end of file diff --git a/core/src/main/res/drawable/ic_notification_fast_forward.xml b/core/src/main/res/drawable/ic_notification_fast_forward.xml new file mode 100644 index 000000000..bf564977c --- /dev/null +++ b/core/src/main/res/drawable/ic_notification_fast_forward.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="24dp"> + <path android:fillColor="#FFFFFFFF" android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_notification_fast_rewind.xml b/core/src/main/res/drawable/ic_notification_fast_rewind.xml new file mode 100644 index 000000000..847159cc5 --- /dev/null +++ b/core/src/main/res/drawable/ic_notification_fast_rewind.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="24dp"> + <path android:fillColor="#FFFFFFFF" android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_notification_key.xml b/core/src/main/res/drawable/ic_notification_key.xml index b1e2f9b8c..c8a817eeb 100644 --- a/core/src/main/res/drawable/ic_notification_key.xml +++ b/core/src/main/res/drawable/ic_notification_key.xml @@ -1,5 +1,6 @@ -<vector android:height="24dp" - android:viewportHeight="24.0" android:viewportWidth="24.0" - android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - <path android:fillColor="#FFFFFFFF" android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="24dp"> + <path android:fillColor="#FFFFFFFF" + android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/> </vector> diff --git a/core/src/main/res/drawable/ic_notification_pause.xml b/core/src/main/res/drawable/ic_notification_pause.xml new file mode 100644 index 000000000..d46efb2f5 --- /dev/null +++ b/core/src/main/res/drawable/ic_notification_pause.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="24dp"> + <path android:fillColor="#FFFFFFFF" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_notification_play.xml b/core/src/main/res/drawable/ic_notification_play.xml new file mode 100644 index 000000000..d571460c6 --- /dev/null +++ b/core/src/main/res/drawable/ic_notification_play.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="24dp"> + <path android:fillColor="#FFFFFFFF" android:pathData="M8,5v14l11,-7z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_notification_skip.xml b/core/src/main/res/drawable/ic_notification_skip.xml new file mode 100644 index 000000000..0c65448cc --- /dev/null +++ b/core/src/main/res/drawable/ic_notification_skip.xml @@ -0,0 +1,5 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="24dp"> + <path android:fillColor="#FFFFFFFF" android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/> +</vector> diff --git a/core/src/main/res/drawable/ic_warning_red.xml b/core/src/main/res/drawable/ic_warning_red.xml new file mode 100644 index 000000000..475a41bbb --- /dev/null +++ b/core/src/main/res/drawable/ic_warning_red.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#FF0000" + android:viewportHeight="24.0" android:viewportWidth="24.0" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/> +</vector> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index c7bd2d2a2..adf938d75 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -394,6 +394,8 @@ <string name="pref_automatic_download_sum">Configure the automatic download of episodes.</string> <string name="pref_autodl_wifi_filter_title">Enable Wi-Fi filter</string> <string name="pref_autodl_wifi_filter_sum">Allow automatic download only for selected Wi-Fi networks.</string> + <string name="autodl_wifi_filter_permission_title">Permission required</string> + <string name="autodl_wifi_filter_permission_message">Location permission is required for Wi-Fi filter. Tap to grant the permission.</string> <string name="pref_automatic_download_on_battery_title">Download when not charging</string> <string name="pref_automatic_download_on_battery_sum">Allow automatic download when the battery is not charging</string> <string name="pref_parallel_downloads_title">Parallel Downloads</string> diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java index 244fb0254..800222ada 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.core; import android.content.Context; +import android.util.Log; import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; @@ -15,6 +16,8 @@ import de.danoeh.antennapod.core.util.exception.RxJavaErrorHandlerSetup; * Apps using the core module of AntennaPod should register implementations of all interfaces here. */ public class ClientConfig { + private static final String TAG = "ClientConfig"; + private ClientConfig(){} /** @@ -44,7 +47,15 @@ public class ClientConfig { UserPreferences.init(context); PlaybackPreferences.init(context); NetworkUtils.init(context); - CastManager.init(context); + // Don't initialize Cast-related logic unless it is enabled, to avoid the unnecessary + // Google Play Service usage. + // Down side: when the user decides to enable casting, AntennaPod needs to be restarted + // for it to take effect. + if (UserPreferences.isCastEnabled()) { + CastManager.init(context); + } else { + Log.v(TAG, "Cast is disabled. All Cast-related initialization will be skipped."); + } SleepTimerPreferences.init(context); RxJavaErrorHandlerSetup.setupRxJavaErrorHandler(); initialized = true; diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java index 5198a76bd..414a7840c 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java +++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java @@ -163,6 +163,10 @@ public class CastManager extends BaseCastManager implements OnFailedListener { return INSTANCE; } + public static boolean isInitialized() { + return INSTANCE != null; + } + /** * Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media * control APIs that this library do not provide a wrapper for, client applications can call diff --git a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java index 7ab1be380..79c71f164 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java +++ b/core/src/play/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceFlavorHelper.java @@ -56,11 +56,18 @@ public class PlaybackServiceFlavorHelper { PlaybackServiceFlavorHelper(Context context, PlaybackService.FlavorHelperCallback callback) { this.callback = callback; + if (!CastManager.isInitialized()) { + return; + } mediaRouter = MediaRouter.getInstance(context.getApplicationContext()); setCastConsumer(context); } void initializeMediaPlayer(Context context) { + if (!CastManager.isInitialized()) { + callback.setMediaPlayer(new LocalPSMP(context, callback.getMediaPlayerCallback())); + return; + } castManager = CastManager.getInstance(); castManager.addCastConsumer(castConsumer); boolean isCasting = castManager.isConnected(); @@ -77,10 +84,16 @@ public class PlaybackServiceFlavorHelper { } void removeCastConsumer() { + if (!CastManager.isInitialized()) { + return; + } castManager.removeCastConsumer(castConsumer); } boolean castDisconnect(boolean castDisconnect) { + if (!CastManager.isInitialized()) { + return false; + } if (castDisconnect) { castManager.disconnect(); } @@ -88,6 +101,9 @@ public class PlaybackServiceFlavorHelper { } boolean onMediaPlayerInfo(Context context, int code, @StringRes int resourceId) { + if (!CastManager.isInitialized()) { + return false; + } switch (code) { case RemotePSMP.CAST_ERROR: callback.sendNotificationBroadcast(PlaybackService.NOTIFICATION_TYPE_SHOW_TOAST, resourceId); @@ -218,6 +234,9 @@ public class PlaybackServiceFlavorHelper { } void registerWifiBroadcastReceiver() { + if (!CastManager.isInitialized()) { + return; + } if (wifiBroadcastReceiver != null) { return; } @@ -243,6 +262,9 @@ public class PlaybackServiceFlavorHelper { } void unregisterWifiBroadcastReceiver() { + if (!CastManager.isInitialized()) { + return; + } if (wifiBroadcastReceiver != null) { callback.unregisterReceiver(wifiBroadcastReceiver); wifiBroadcastReceiver = null; @@ -250,6 +272,9 @@ public class PlaybackServiceFlavorHelper { } boolean onSharedPreference(String key) { + if (!CastManager.isInitialized()) { + return false; + } if (UserPreferences.PREF_CAST_ENABLED.equals(key)) { if (!UserPreferences.isCastEnabled()) { if (castManager.isConnecting() || castManager.isConnected()) { @@ -263,6 +288,9 @@ public class PlaybackServiceFlavorHelper { } void sessionStateAddActionForWear(PlaybackStateCompat.Builder sessionState, String actionName, CharSequence name, int icon) { + if (!CastManager.isInitialized()) { + return; + } PlaybackStateCompat.CustomAction.Builder actionBuilder = new PlaybackStateCompat.CustomAction.Builder(actionName, name, icon); Bundle actionExtras = new Bundle(); @@ -273,6 +301,9 @@ public class PlaybackServiceFlavorHelper { } void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) { + if (!CastManager.isInitialized()) { + return; + } Bundle sessionExtras = new Bundle(); sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, true); sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, true); |