diff options
author | thrillfall <thrillfall@users.noreply.github.com> | 2021-10-06 22:12:47 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-06 22:12:47 +0200 |
commit | bc85ebc806367d863973bc9434e7b0d9d5fd2168 (patch) | |
tree | 5a729b84f1a12c3de8d3178ad7d688eb6bb552be /core | |
parent | dab44b68436601f415edb095da605811e985eb00 (diff) | |
download | AntennaPod-bc85ebc806367d863973bc9434e7b0d9d5fd2168.zip |
Add synchronization with gPodder Nextcloud server app (#5243)
Diffstat (limited to 'core')
18 files changed, 621 insertions, 455 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java deleted file mode 100644 index e338e0d01..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java +++ /dev/null @@ -1,115 +0,0 @@ -package de.danoeh.antennapod.core.preferences; - -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; - -/** - * Manages preferences for accessing gpodder.net service - */ -public class GpodnetPreferences { - - private GpodnetPreferences(){} - - private static final String TAG = "GpodnetPreferences"; - - private static final String PREF_NAME = "gpodder.net"; - private static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; - private static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; - private static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; - private static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; - - private static String username; - private static String password; - private static String deviceID; - private static String hosturl; - - private static boolean preferencesLoaded = false; - - private static SharedPreferences getPreferences() { - return ClientConfig.applicationCallbacks.getApplicationInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - } - - private static synchronized void ensurePreferencesLoaded() { - if (!preferencesLoaded) { - SharedPreferences prefs = getPreferences(); - username = prefs.getString(PREF_GPODNET_USERNAME, null); - password = prefs.getString(PREF_GPODNET_PASSWORD, null); - deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null); - hosturl = prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST); - - preferencesLoaded = true; - } - } - - private static void writePreference(String key, String value) { - SharedPreferences.Editor editor = getPreferences().edit(); - editor.putString(key, value); - editor.apply(); - } - - public static String getUsername() { - ensurePreferencesLoaded(); - return username; - } - - public static void setUsername(String username) { - GpodnetPreferences.username = username; - writePreference(PREF_GPODNET_USERNAME, username); - } - - public static String getPassword() { - ensurePreferencesLoaded(); - return password; - } - - public static void setPassword(String password) { - GpodnetPreferences.password = password; - writePreference(PREF_GPODNET_PASSWORD, password); - } - - public static String getDeviceID() { - ensurePreferencesLoaded(); - return deviceID; - } - - public static void setDeviceID(String deviceID) { - GpodnetPreferences.deviceID = deviceID; - writePreference(PREF_GPODNET_DEVICEID, deviceID); - } - - public static String getHosturl() { - ensurePreferencesLoaded(); - return hosturl; - } - - public static void setHosturl(String value) { - if (!value.equals(hosturl)) { - logout(); - writePreference(PREF_GPODNET_HOSTNAME, value); - hosturl = value; - } - } - - /** - * Returns true if device ID, username and password have a non-null value - */ - public static boolean loggedIn() { - ensurePreferencesLoaded(); - return deviceID != null && username != null && password != null; - } - - public static synchronized void logout() { - if (BuildConfig.DEBUG) Log.d(TAG, "Logout: Clearing preferences"); - setUsername(null); - setPassword(null); - setDeviceID(null); - SyncService.clearQueue(ClientConfig.applicationCallbacks.getApplicationInstance()); - UserPreferences.setGpodnetNotificationsEnabled(); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index 8c9035621..6bbd704e2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -6,21 +6,22 @@ import android.util.Log; import androidx.annotation.NonNull; +import org.greenrobot.eventbus.EventBus; + import java.io.File; import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.core.service.download.DownloadRequest; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.sync.model.EpisodeAction; -import org.greenrobot.eventbus.EventBus; /** * Handles a completed media download. @@ -103,7 +104,7 @@ public class MediaDownloadedHandler implements Runnable { EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD) .currentTimestamp() .build(); - SyncService.enqueueEpisodeAction(context, action); + SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); } } 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 f503c16f4..848ea7cfc 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 @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.service.playback; +import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; + import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; @@ -21,13 +23,7 @@ import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Vibrator; -import androidx.preference.PreferenceManager; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; import android.support.v4.media.MediaBrowserCompat; -import androidx.media.MediaBrowserServiceCompat; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; @@ -40,6 +36,17 @@ import android.view.SurfaceHolder; import android.webkit.URLUtil; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.media.MediaBrowserServiceCompat; +import androidx.preference.PreferenceManager; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -52,12 +59,6 @@ import de.danoeh.antennapod.core.event.ServiceEvent; import de.danoeh.antennapod.core.event.settings.SkipIntroEndingChangedEvent; import de.danoeh.antennapod.core.event.settings.SpeedPresetChangedEvent; import de.danoeh.antennapod.core.event.settings.VolumeAdaptionChangedEvent; -import de.danoeh.antennapod.model.feed.Chapter; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.playback.MediaType; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -66,15 +67,21 @@ 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.sync.SyncService; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.gui.NotificationUtils; -import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.core.util.playback.PlayableUtils; import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter; import de.danoeh.antennapod.core.widget.WidgetUpdater; +import de.danoeh.antennapod.model.feed.Chapter; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; +import de.danoeh.antennapod.model.playback.MediaType; +import de.danoeh.antennapod.model.playback.Playable; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter; import io.reactivex.Completable; @@ -83,11 +90,6 @@ import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import static de.danoeh.antennapod.model.feed.FeedPreferences.SPEED_USE_GLOBAL; /** * Controls the MediaPlayer that plays a FeedMedia-file @@ -966,7 +968,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { taskManager.cancelWidgetUpdater(); if (playable != null) { if (playable instanceof FeedMedia) { - SyncService.enqueueEpisodePlayed(getApplicationContext(), (FeedMedia) playable, false); + SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(getApplicationContext(), + (FeedMedia) playable, false); } playable.onPlaybackPause(getApplicationContext()); } @@ -1110,10 +1113,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { } if (ended || smartMarkAsPlayed) { - SyncService.enqueueEpisodePlayed(getApplicationContext(), media, true); + SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( + getApplicationContext(), media, true); media.onPlaybackCompleted(getApplicationContext()); } else { - SyncService.enqueueEpisodePlayed(getApplicationContext(), media, false); + SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( + getApplicationContext(), media, false); media.onPlaybackPause(getApplicationContext()); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 185d85e7a..f776fe111 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -575,7 +575,6 @@ public final class DBReader { @Nullable private static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl, PodDBAdapter adapter) { - Log.d(TAG, "Loading feeditem with guid " + guid + " or episode url " + episodeUrl); try (Cursor cursor = adapter.getFeedItemCursor(guid, episodeUrl)) { if (!cursor.moveToNext()) { return null; @@ -633,8 +632,6 @@ public final class DBReader { * Does NOT load additional attributes like feed or queue state. */ public static FeedItem getFeedItemByGuidOrEpisodeUrl(final String guid, final String episodeUrl) { - Log.d(TAG, "getFeedItem() called with: " + "guid = [" + guid + "], episodeUrl = [" + episodeUrl + "]"); - PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); try { 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 9dd979dc7..377202c4b 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 @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.storage; +import static android.content.Context.MODE_PRIVATE; + import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; @@ -9,22 +11,6 @@ import android.util.Log; import androidx.annotation.VisibleForTesting; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.FeedItemEvent; -import de.danoeh.antennapod.core.event.FeedListUpdateEvent; -import de.danoeh.antennapod.core.event.MessageEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.LocalFeedUpdater; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.service.download.DownloadStatus; -import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.util.LongList; -import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; import org.greenrobot.eventbus.EventBus; import java.util.ArrayList; @@ -41,7 +27,23 @@ import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.atomic.AtomicBoolean; -import static android.content.Context.MODE_PRIVATE; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.event.FeedItemEvent; +import de.danoeh.antennapod.core.event.FeedListUpdateEvent; +import de.danoeh.antennapod.core.event.MessageEvent; +import de.danoeh.antennapod.core.feed.LocalFeedUpdater; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.LongList; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; /** * Provides methods for doing common tasks that use DBReader and DBWriter. @@ -482,7 +484,7 @@ public final class DBTasks { .position(oldItem.getMedia().getDuration() / 1000) .total(oldItem.getMedia().getDuration() / 1000) .build(); - SyncService.enqueueEpisodeAction(context, action); + SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index 34ea5e207..479a7763c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -7,8 +7,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; import org.greenrobot.eventbus.EventBus; import java.io.File; @@ -32,23 +30,24 @@ import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.event.PlaybackHistoryEvent; import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.feed.FeedEvent; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.FeedItemPermutors; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.Permutor; +import de.danoeh.antennapod.core.util.playback.PlayableUtils; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.core.util.playback.PlayableUtils; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; /** * Provides methods for writing data to AntennaPod's database. @@ -132,13 +131,11 @@ public class DBWriter { } // Gpodder: queue delete action for synchronization - if (GpodnetPreferences.loggedIn()) { - FeedItem item = media.getItem(); - EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE) - .currentTimestamp() - .build(); - SyncService.enqueueEpisodeAction(context, action); - } + FeedItem item = media.getItem(); + EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE) + .currentTimestamp() + .build(); + SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); } EventBus.getDefault().post(FeedItemEvent.deletedMedia(Collections.singletonList(media.getItem()))); return true; @@ -170,7 +167,7 @@ public class DBWriter { adapter.removeFeed(feed); adapter.close(); - SyncService.enqueueFeedRemoved(context, feed.getDownload_url()); + SynchronizationQueueSink.enqueueFeedRemovedIfSynchronizationIsActive(context, feed.getDownload_url()); EventBus.getDefault().post(new FeedListUpdateEvent(feed)); }); } @@ -782,7 +779,7 @@ public class DBWriter { adapter.close(); for (Feed feed : feeds) { - SyncService.enqueueFeedAdded(context, feed.getDownload_url()); + SynchronizationQueueSink.enqueueFeedAddedIfSynchronizationIsActive(context, feed.getDownload_url()); } BackupManager backupManager = new BackupManager(context); 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 85ce2dc99..55cfafbbb 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 @@ -1123,7 +1123,6 @@ public class PodDBAdapter { + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + whereClauseCondition; - Log.d(TAG, "SQL: " + query); return db.rawQuery(query, null); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java b/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java index 6d80a6457..74e5d5cdf 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java @@ -4,7 +4,8 @@ public class GuidValidator { public static boolean isValidGuid(String guid) { return guid != null - && !guid.trim().isEmpty(); + && !guid.trim().isEmpty() + && !guid.equals("null"); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java b/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java new file mode 100644 index 000000000..e7dbbbd3c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.core.sync; + +import java.util.concurrent.locks.ReentrantLock; + +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; + +public class LockingAsyncExecutor { + + static final ReentrantLock lock = new ReentrantLock(); + + /** + * Take the lock and execute runnable (to prevent changes to preferences being lost when enqueueing while sync is + * in progress). If the lock is free, the runnable is directly executed in the calling thread to prevent overhead. + */ + public static void executeLockedAsync(Runnable runnable) { + if (lock.tryLock()) { + try { + runnable.run(); + } finally { + lock.unlock(); + } + } else { + Completable.fromRunnable(() -> { + lock.lock(); + try { + runnable.run(); + } finally { + lock.unlock(); + } + }).subscribeOn(Schedulers.io()) + .subscribe(); + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java index 9803a29db..8edc37ac4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java @@ -5,7 +5,6 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.NonNull; @@ -20,12 +19,16 @@ import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; +import org.apache.commons.lang3.StringUtils; +import org.greenrobot.eventbus.EventBus; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.SyncServiceEvent; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.storage.DBReader; @@ -33,10 +36,14 @@ import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueStorage; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.URLChecker; import de.danoeh.antennapod.core.util.gui.NotificationUtils; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; import de.danoeh.antennapod.net.sync.model.EpisodeAction; import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges; @@ -44,156 +51,54 @@ import de.danoeh.antennapod.net.sync.model.ISyncService; import de.danoeh.antennapod.net.sync.model.SubscriptionChanges; import de.danoeh.antennapod.net.sync.model.SyncServiceException; import de.danoeh.antennapod.net.sync.model.UploadChangesResponse; -import io.reactivex.Completable; -import io.reactivex.schedulers.Schedulers; - -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; -import org.json.JSONArray; -import org.json.JSONException; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; +import de.danoeh.antennapod.net.sync.nextcloud.NextcloudSyncService; public class SyncService extends Worker { - private static final String PREF_NAME = "SyncService"; - private static final String PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "last_sync_timestamp"; - private static final String PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "last_episode_actions_sync_timestamp"; - private static final String PREF_QUEUED_FEEDS_ADDED = "sync_added"; - private static final String PREF_QUEUED_FEEDS_REMOVED = "sync_removed"; - private static final String PREF_QUEUED_EPISODE_ACTIONS = "sync_queued_episode_actions"; - private static final String PREF_LAST_SYNC_ATTEMPT_TIMESTAMP = "last_sync_attempt_timestamp"; - private static final String PREF_LAST_SYNC_ATTEMPT_SUCCESS = "last_sync_attempt_success"; - private static final String TAG = "SyncService"; - private static final String WORK_ID_SYNC = "SyncServiceWorkId"; - private static final ReentrantLock lock = new ReentrantLock(); + public static final String TAG = "SyncService"; - private ISyncService syncServiceImpl; + private static final String WORK_ID_SYNC = "SyncServiceWorkId"; + private final SynchronizationQueueStorage synchronizationQueueStorage; public SyncService(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); + synchronizationQueueStorage = new SynchronizationQueueStorage(context); } @Override @NonNull public Result doWork() { - if (!GpodnetPreferences.loggedIn()) { + ISyncService activeSyncProvider = getActiveSyncProvider(); + if (activeSyncProvider == null) { return Result.success(); } - syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), - GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(), - GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); - SharedPreferences.Editor prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .edit(); - prefs.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply(); + + SynchronizationSettings.updateLastSynchronizationAttempt(); try { - syncServiceImpl.login(); + activeSyncProvider.login(); EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_subscriptions)); - syncSubscriptions(); - syncEpisodeActions(); - syncServiceImpl.logout(); + syncSubscriptions(activeSyncProvider); + syncEpisodeActions(activeSyncProvider); + activeSyncProvider.logout(); clearErrorNotifications(); EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_success)); - prefs.putBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, true).apply(); + SynchronizationSettings.setLastSynchronizationAttemptSuccess(true); return Result.success(); - } catch (SyncServiceException e) { + } catch (Exception e) { EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_error)); - prefs.putBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, false).apply(); + SynchronizationSettings.setLastSynchronizationAttemptSuccess(false); Log.e(TAG, Log.getStackTraceString(e)); - if (getRunAttemptCount() % 3 == 2) { - // Do not spam users with notification and retry before notifying - updateErrorNotification(e); - } - return Result.retry(); - } - } - - public static void clearQueue(Context context) { - executeLockedAsync(() -> - context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0) - .putString(PREF_QUEUED_EPISODE_ACTIONS, "[]") - .putString(PREF_QUEUED_FEEDS_ADDED, "[]") - .putString(PREF_QUEUED_FEEDS_REMOVED, "[]") - .apply()); - } - public static void enqueueFeedAdded(Context context, String downloadUrl) { - if (!GpodnetPreferences.loggedIn()) { - return; - } - executeLockedAsync(() -> { - try { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_FEEDS_ADDED, "[]"); - JSONArray queue = new JSONArray(json); - queue.put(downloadUrl); - prefs.edit().putString(PREF_QUEUED_FEEDS_ADDED, queue.toString()).apply(); - } catch (JSONException e) { - e.printStackTrace(); - } - sync(context); - }); - } - - public static void enqueueFeedRemoved(Context context, String downloadUrl) { - if (!GpodnetPreferences.loggedIn()) { - return; - } - executeLockedAsync(() -> { - try { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_FEEDS_REMOVED, "[]"); - JSONArray queue = new JSONArray(json); - queue.put(downloadUrl); - prefs.edit().putString(PREF_QUEUED_FEEDS_REMOVED, queue.toString()).apply(); - } catch (JSONException e) { - e.printStackTrace(); - } - sync(context); - }); - } - - public static void enqueueEpisodeAction(Context context, EpisodeAction action) { - if (!GpodnetPreferences.loggedIn()) { - return; - } - executeLockedAsync(() -> { - try { - SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_EPISODE_ACTIONS, "[]"); - JSONArray queue = new JSONArray(json); - queue.put(action.writeToJsonObject()); - prefs.edit().putString(PREF_QUEUED_EPISODE_ACTIONS, queue.toString()).apply(); - } catch (JSONException e) { - e.printStackTrace(); + if (e instanceof SyncServiceException) { + if (getRunAttemptCount() % 3 == 2) { + // Do not spam users with notification and retry before notifying + updateErrorNotification(e); + } + return Result.retry(); + } else { + updateErrorNotification(e); + return Result.failure(); } - sync(context); - }); - } - - public static void enqueueEpisodePlayed(Context context, FeedMedia media, boolean completed) { - if (!GpodnetPreferences.loggedIn()) { - return; } - if (media.getItem() == null) { - return; - } - if (media.getStartPosition() < 0 || (!completed && media.getStartPosition() >= media.getPosition())) { - return; - } - EpisodeAction action = new EpisodeAction.Builder(media.getItem(), EpisodeAction.PLAY) - .currentTimestamp() - .started(media.getStartPosition() / 1000) - .position((completed ? media.getDuration() : media.getPosition()) / 1000) - .total(media.getDuration() / 1000) - .build(); - SyncService.enqueueEpisodeAction(context, action); } public static void sync(Context context) { @@ -211,13 +116,8 @@ public class SyncService extends Worker { } public static void fullSync(Context context) { - executeLockedAsync(() -> { - context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0) - .putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0) - .apply(); - + LockingAsyncExecutor.executeLockedAsync(() -> { + SynchronizationSettings.resetTimestamps(); OneTimeWorkRequest workRequest = getWorkRequest() .setInitialDelay(0L, TimeUnit.SECONDS) .build(); @@ -226,108 +126,14 @@ public class SyncService extends Worker { }); } - private static OneTimeWorkRequest.Builder getWorkRequest() { - Constraints.Builder constraints = new Constraints.Builder(); - if (UserPreferences.isAllowMobileFeedRefresh()) { - constraints.setRequiredNetworkType(NetworkType.CONNECTED); - } else { - constraints.setRequiredNetworkType(NetworkType.UNMETERED); - } - - return new OneTimeWorkRequest.Builder(SyncService.class) - .setConstraints(constraints.build()) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) - .setInitialDelay(5L, TimeUnit.SECONDS); // Give it some time, so other actions can be queued - } - - /** - * Take the lock and execute runnable (to prevent changes to preferences being lost when enqueueing while sync is - * in progress). If the lock is free, the runnable is directly executed in the calling thread to prevent overhead. - */ - private static void executeLockedAsync(Runnable runnable) { - if (lock.tryLock()) { - try { - runnable.run(); - } finally { - lock.unlock(); - } - } else { - Completable.fromRunnable(() -> { - lock.lock(); - try { - runnable.run(); - } finally { - lock.unlock(); - } - }).subscribeOn(Schedulers.io()) - .subscribe(); - } - } - - public static boolean isLastSyncSuccessful(Context context) { - return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(PREF_LAST_SYNC_ATTEMPT_SUCCESS, false); - } - - public static long getLastSyncAttempt(Context context) { - return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, 0); - } - - private List<EpisodeAction> getQueuedEpisodeActions() { - ArrayList<EpisodeAction> actions = new ArrayList<>(); - try { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_EPISODE_ACTIONS, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - actions.add(EpisodeAction.readFromJsonObject(queue.getJSONObject(i))); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return actions; - } - - private List<String> getQueuedRemovedFeeds() { - ArrayList<String> actions = new ArrayList<>(); - try { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_FEEDS_REMOVED, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - actions.add(queue.getString(i)); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return actions; - } - - private List<String> getQueuedAddedFeeds() { - ArrayList<String> actions = new ArrayList<>(); - try { - SharedPreferences prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String json = prefs.getString(PREF_QUEUED_FEEDS_ADDED, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - actions.add(queue.getString(i)); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return actions; - } - - private void syncSubscriptions() throws SyncServiceException { - final long lastSync = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0); + private void syncSubscriptions(ISyncService syncServiceImpl) throws SyncServiceException { + final long lastSync = SynchronizationSettings.getLastSubscriptionSynchronizationTimestamp(); final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(); SubscriptionChanges subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync); long newTimeStamp = subscriptionChanges.getTimestamp(); - List<String> queuedRemovedFeeds = getQueuedRemovedFeeds(); - List<String> queuedAddedFeeds = getQueuedAddedFeeds(); + List<String> queuedRemovedFeeds = synchronizationQueueStorage.getQueuedRemovedFeeds(); + List<String> queuedAddedFeeds = synchronizationQueueStorage.getQueuedAddedFeeds(); Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); for (String downloadUrl : subscriptionChanges.getAdded()) { @@ -359,26 +165,21 @@ public class SyncService extends Worker { Log.d(TAG, "Added: " + StringUtils.join(queuedAddedFeeds, ", ")); Log.d(TAG, "Removed: " + StringUtils.join(queuedRemovedFeeds, ", ")); - lock.lock(); + LockingAsyncExecutor.lock.lock(); try { UploadChangesResponse uploadResponse = syncServiceImpl .uploadSubscriptionChanges(queuedAddedFeeds, queuedRemovedFeeds); - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putString(PREF_QUEUED_FEEDS_ADDED, "[]").apply(); - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putString(PREF_QUEUED_FEEDS_REMOVED, "[]").apply(); + synchronizationQueueStorage.clearFeedQueues(); newTimeStamp = uploadResponse.timestamp; } finally { - lock.unlock(); + LockingAsyncExecutor.lock.unlock(); } } - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply(); + SynchronizationSettings.setLastSubscriptionSynchronizationAttemptTimestamp(newTimeStamp); } - private void syncEpisodeActions() throws SyncServiceException { - final long lastSync = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - .getLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0); + private void syncEpisodeActions(ISyncService syncServiceImpl) throws SyncServiceException { + final long lastSync = SynchronizationSettings.getLastEpisodeActionSynchronizationTimestamp(); EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_download)); EpisodeActionChanges getResponse = syncServiceImpl.getEpisodeActionChanges(lastSync); long newTimeStamp = getResponse.getTimestamp(); @@ -387,7 +188,7 @@ public class SyncService extends Worker { // upload local actions EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_upload)); - List<EpisodeAction> queuedEpisodeActions = getQueuedEpisodeActions(); + List<EpisodeAction> queuedEpisodeActions = synchronizationQueueStorage.getQueuedEpisodeActions(); if (lastSync == 0) { EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_upload_played)); List<FeedItem> readItems = DBReader.getPlayedItems(); @@ -407,24 +208,21 @@ public class SyncService extends Worker { } } if (queuedEpisodeActions.size() > 0) { - lock.lock(); + LockingAsyncExecutor.lock.lock(); try { Log.d(TAG, "Uploading " + queuedEpisodeActions.size() + " actions: " + StringUtils.join(queuedEpisodeActions, ", ")); UploadChangesResponse postResponse = syncServiceImpl.uploadEpisodeActions(queuedEpisodeActions); newTimeStamp = postResponse.timestamp; Log.d(TAG, "Upload episode response: " + postResponse); - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putString(PREF_QUEUED_EPISODE_ACTIONS, "[]").apply(); + synchronizationQueueStorage.clearEpisodeActionQueue(); } finally { - lock.unlock(); + LockingAsyncExecutor.lock.unlock(); } } - getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).edit() - .putLong(PREF_LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, newTimeStamp).apply(); + SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp); } - private synchronized void processEpisodeActions(List<EpisodeAction> remoteActions) { Log.d(TAG, "Processing " + remoteActions.size() + " actions"); if (remoteActions.size() == 0) { @@ -432,7 +230,8 @@ public class SyncService extends Worker { } Map<Pair<String, String>, EpisodeAction> playActionsToUpdate = EpisodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, getQueuedEpisodeActions()); + .getRemoteActionsOverridingLocalActions(remoteActions, + synchronizationQueueStorage.getQueuedEpisodeActions()); LongList queueToBeRemoved = new LongList(); List<FeedItem> updatedItems = new ArrayList<>(); for (EpisodeAction action : playActionsToUpdate.values()) { @@ -442,20 +241,24 @@ public class SyncService extends Worker { Log.i(TAG, "Unknown feed item: " + action); continue; } + if (feedItem.getMedia() == null) { + Log.i(TAG, "Feed item has no media: " + action); + continue; + } if (action.getAction() == EpisodeAction.NEW) { DBWriter.markItemPlayed(feedItem, FeedItem.UNPLAYED, true); continue; } - Log.d(TAG, "Most recent play action: " + action.toString()); - FeedMedia media = feedItem.getMedia(); - media.setPosition(action.getPosition() * 1000); + feedItem.getMedia().setPosition(action.getPosition() * 1000); if (FeedItemUtil.hasAlmostEnded(feedItem.getMedia())) { - Log.d(TAG, "Marking as played"); + Log.d(TAG, "Marking as played: " + action); feedItem.setPlayed(true); + feedItem.getMedia().setPosition(0); queueToBeRemoved.add(feedItem.getId()); + } else { + Log.d(TAG, "Setting position: " + action); } updatedItems.add(feedItem); - } DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray()); DBReader.loadAdditionalFeedItemListData(updatedItems); @@ -469,7 +272,7 @@ public class SyncService extends Worker { nm.cancel(R.id.notification_gpodnet_sync_autherror); } - private void updateErrorNotification(SyncServiceException exception) { + private void updateErrorNotification(Exception exception) { if (!UserPreferences.gpodnetNotificationsEnabled()) { Log.d(TAG, "Skipping sync error notification because of user setting"); return; @@ -486,6 +289,7 @@ public class SyncService extends Worker { NotificationUtils.CHANNEL_ID_SYNC_ERROR) .setContentTitle(getApplicationContext().getString(R.string.gpodnetsync_error_title)) .setContentText(description) + .setStyle(new NotificationCompat.BigTextStyle().bigText(description)) .setContentIntent(pendingIntent) .setSmallIcon(R.drawable.ic_notification_sync_error) .setAutoCancel(true) @@ -495,4 +299,36 @@ public class SyncService extends Worker { .getSystemService(Context.NOTIFICATION_SERVICE); nm.notify(R.id.notification_gpodnet_sync_error, notification); } + + private static OneTimeWorkRequest.Builder getWorkRequest() { + Constraints.Builder constraints = new Constraints.Builder(); + if (UserPreferences.isAllowMobileFeedRefresh()) { + constraints.setRequiredNetworkType(NetworkType.CONNECTED); + } else { + constraints.setRequiredNetworkType(NetworkType.UNMETERED); + } + + return new OneTimeWorkRequest.Builder(SyncService.class) + .setConstraints(constraints.build()) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES) + .setInitialDelay(5L, TimeUnit.SECONDS); // Give it some time, so other actions can be queued + } + + private ISyncService getActiveSyncProvider() { + String selectedSyncProviderKey = SynchronizationSettings.getSelectedSyncProviderKey(); + SynchronizationProviderViewData selectedService = SynchronizationProviderViewData + .valueOf(selectedSyncProviderKey); + switch (selectedService) { + case GPODDER_NET: + return new GpodnetService(AntennapodHttpClient.getHttpClient(), + SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(), + SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword()); + case NEXTCLOUD_GPODDER: + return new NextcloudSyncService(AntennapodHttpClient.getHttpClient(), + SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getUsername(), + SynchronizationCredentials.getPassword()); + default: + return null; + } + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java new file mode 100644 index 000000000..e08bc66ad --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.sync; + +import android.content.Context; +import android.content.SharedPreferences; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; + +/** + * Manages preferences for accessing gpodder.net service and other sync providers + */ +public class SynchronizationCredentials { + + private SynchronizationCredentials() { + } + + private static final String PREF_NAME = "gpodder.net"; + private static final String PREF_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; + private static final String PREF_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; + private static final String PREF_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; + private static final String PREF_HOSTNAME = "prefGpodnetHostname"; + + private static SharedPreferences getPreferences() { + return ClientConfig.applicationCallbacks.getApplicationInstance() + .getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + public static String getUsername() { + return getPreferences().getString(PREF_USERNAME, null); + } + + public static void setUsername(String username) { + getPreferences().edit().putString(PREF_USERNAME, username).apply(); + } + + public static String getPassword() { + return getPreferences().getString(PREF_PASSWORD, null); + } + + public static void setPassword(String password) { + getPreferences().edit().putString(PREF_PASSWORD, password).apply(); + } + + public static String getDeviceID() { + return getPreferences().getString(PREF_DEVICEID, null); + } + + public static void setDeviceID(String deviceID) { + getPreferences().edit().putString(PREF_DEVICEID, deviceID).apply(); + } + + public static String getHosturl() { + return getPreferences().getString(PREF_HOSTNAME, null); + } + + public static void setHosturl(String value) { + getPreferences().edit().putString(PREF_HOSTNAME, value).apply(); + } + + public static synchronized void clear(Context context) { + setUsername(null); + setPassword(null); + setDeviceID(null); + SynchronizationQueueSink.clearQueue(context); + UserPreferences.setGpodnetNotificationsEnabled(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java new file mode 100644 index 000000000..cba713f60 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.core.sync; + +import de.danoeh.antennapod.core.R; + +public enum SynchronizationProviderViewData { + GPODDER_NET( + "GPODDER_NET", + R.string.gpodnet_description, + R.drawable.gpodder_icon + ), + NEXTCLOUD_GPODDER( + "NEXTCLOUD_GPODDER", + R.string.synchronization_summary_nextcloud, + R.drawable.nextcloud_logo + ); + + public static SynchronizationProviderViewData fromIdentifier(String provider) { + for (SynchronizationProviderViewData synchronizationProvider : SynchronizationProviderViewData.values()) { + if (synchronizationProvider.getIdentifier().equals(provider)) { + return synchronizationProvider; + } + } + return null; + } + + private final String identifier; + private final int iconResource; + private final int summaryResource; + + SynchronizationProviderViewData(String identifier, int summaryResource, int iconResource) { + this.identifier = identifier; + this.iconResource = iconResource; + this.summaryResource = summaryResource; + } + + public String getIdentifier() { + return identifier; + } + + public int getIconResource() { + return iconResource; + } + + public int getSummaryResource() { + return summaryResource; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java new file mode 100644 index 000000000..1a53ac0fb --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java @@ -0,0 +1,83 @@ +package de.danoeh.antennapod.core.sync; + +import android.content.Context; +import android.content.SharedPreferences; + +import de.danoeh.antennapod.core.ClientConfig; + +public class SynchronizationSettings { + + public static final String LAST_SYNC_ATTEMPT_TIMESTAMP = "last_sync_attempt_timestamp"; + private static final String NAME = "synchronization"; + private static final String SELECTED_SYNC_PROVIDER = "selected_sync_provider"; + private static final String LAST_SYNC_ATTEMPT_SUCCESS = "last_sync_attempt_success"; + private static final String LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "last_episode_actions_sync_timestamp"; + private static final String LAST_SUBSCRIPTION_SYNC_TIMESTAMP = "last_sync_timestamp"; + + public static boolean isProviderConnected() { + return getSelectedSyncProviderKey() != null; + } + + public static void resetTimestamps() { + getSharedPreferences().edit() + .putLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0) + .putLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0) + .putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, 0) + .apply(); + } + + public static boolean isLastSyncSuccessful() { + return getSharedPreferences().getBoolean(LAST_SYNC_ATTEMPT_SUCCESS, false); + } + + public static long getLastSyncAttempt() { + return getSharedPreferences().getLong(LAST_SYNC_ATTEMPT_TIMESTAMP, 0); + } + + public static void setSelectedSyncProvider(SynchronizationProviderViewData provider) { + getSharedPreferences() + .edit() + .putString(SELECTED_SYNC_PROVIDER, provider == null ? null : provider.getIdentifier()) + .apply(); + } + + public static String getSelectedSyncProviderKey() { + return getSharedPreferences().getString(SELECTED_SYNC_PROVIDER, null); + } + + public static void updateLastSynchronizationAttempt() { + getSharedPreferences().edit() + .putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()) + .apply(); + } + + public static void setLastSynchronizationAttemptSuccess(boolean isSuccess) { + getSharedPreferences().edit() + .putBoolean(LAST_SYNC_ATTEMPT_SUCCESS, isSuccess) + .apply(); + } + + public static long getLastSubscriptionSynchronizationTimestamp() { + return getSharedPreferences().getLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0); + } + + public static void setLastSubscriptionSynchronizationAttemptTimestamp(long newTimeStamp) { + getSharedPreferences().edit() + .putLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply(); + } + + public static long getLastEpisodeActionSynchronizationTimestamp() { + return getSharedPreferences() + .getLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0); + } + + public static void setLastEpisodeActionSynchronizationAttemptTimestamp(long timestamp) { + getSharedPreferences().edit() + .putLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp).apply(); + } + + private static SharedPreferences getSharedPreferences() { + return ClientConfig.applicationCallbacks.getApplicationInstance() + .getSharedPreferences(NAME, Context.MODE_PRIVATE); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java new file mode 100644 index 000000000..445faf60f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.sync.queue; + +import android.content.Context; + +import de.danoeh.antennapod.core.sync.LockingAsyncExecutor; +import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.core.sync.SynchronizationSettings; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; + +public class SynchronizationQueueSink { + + public static void clearQueue(Context context) { + LockingAsyncExecutor.executeLockedAsync(new SynchronizationQueueStorage(context)::clearQueue); + } + + public static void enqueueFeedAddedIfSynchronizationIsActive(Context context, String downloadUrl) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + LockingAsyncExecutor.executeLockedAsync(() -> { + new SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl); + SyncService.sync(context); + }); + } + + public static void enqueueFeedRemovedIfSynchronizationIsActive(Context context, String downloadUrl) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + LockingAsyncExecutor.executeLockedAsync(() -> { + new SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl); + SyncService.sync(context); + }); + } + + public static void enqueueEpisodeActionIfSynchronizationIsActive(Context context, EpisodeAction action) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + LockingAsyncExecutor.executeLockedAsync(() -> { + new SynchronizationQueueStorage(context).enqueueEpisodeAction(action); + SyncService.sync(context); + }); + } + + public static void enqueueEpisodePlayedIfSynchronizationIsActive(Context context, FeedMedia media, + boolean completed) { + if (!SynchronizationSettings.isProviderConnected()) { + return; + } + if (media.getItem() == null) { + return; + } + if (media.getStartPosition() < 0 || (!completed && media.getStartPosition() >= media.getPosition())) { + return; + } + EpisodeAction action = new EpisodeAction.Builder(media.getItem(), EpisodeAction.PLAY) + .currentTimestamp() + .started(media.getStartPosition() / 1000) + .position((completed ? media.getDuration() : media.getPosition()) / 1000) + .total(media.getDuration() / 1000) + .build(); + enqueueEpisodeActionIfSynchronizationIsActive(context, action); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java new file mode 100644 index 000000000..5c6d58fe3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java @@ -0,0 +1,140 @@ +package de.danoeh.antennapod.core.sync.queue; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.ArrayList; + +import de.danoeh.antennapod.core.sync.SynchronizationSettings; +import de.danoeh.antennapod.net.sync.model.EpisodeAction; + +public class SynchronizationQueueStorage { + + private static final String NAME = "synchronization"; + private static final String QUEUED_EPISODE_ACTIONS = "sync_queued_episode_actions"; + private static final String QUEUED_FEEDS_REMOVED = "sync_removed"; + private static final String QUEUED_FEEDS_ADDED = "sync_added"; + private final SharedPreferences sharedPreferences; + + public SynchronizationQueueStorage(Context context) { + this.sharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); + } + + public ArrayList<EpisodeAction> getQueuedEpisodeActions() { + ArrayList<EpisodeAction> actions = new ArrayList<>(); + try { + String json = getSharedPreferences() + .getString(QUEUED_EPISODE_ACTIONS, "[]"); + JSONArray queue = new JSONArray(json); + for (int i = 0; i < queue.length(); i++) { + actions.add(EpisodeAction.readFromJsonObject(queue.getJSONObject(i))); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return actions; + } + + public ArrayList<String> getQueuedRemovedFeeds() { + ArrayList<String> removedFeedUrls = new ArrayList<>(); + try { + String json = getSharedPreferences() + .getString(QUEUED_FEEDS_REMOVED, "[]"); + JSONArray queue = new JSONArray(json); + for (int i = 0; i < queue.length(); i++) { + removedFeedUrls.add(queue.getString(i)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return removedFeedUrls; + + } + + public ArrayList<String> getQueuedAddedFeeds() { + ArrayList<String> addedFeedUrls = new ArrayList<>(); + try { + String json = getSharedPreferences() + .getString(QUEUED_FEEDS_ADDED, "[]"); + JSONArray queue = new JSONArray(json); + for (int i = 0; i < queue.length(); i++) { + addedFeedUrls.add(queue.getString(i)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + return addedFeedUrls; + } + + public void clearEpisodeActionQueue() { + getSharedPreferences().edit() + .putString(QUEUED_EPISODE_ACTIONS, "[]").apply(); + + } + + public void clearFeedQueues() { + getSharedPreferences().edit() + .putString(QUEUED_FEEDS_ADDED, "[]") + .putString(QUEUED_FEEDS_REMOVED, "[]") + .apply(); + } + + protected void clearQueue() { + SynchronizationSettings.resetTimestamps(); + getSharedPreferences().edit() + .putString(QUEUED_EPISODE_ACTIONS, "[]") + .putString(QUEUED_FEEDS_ADDED, "[]") + .putString(QUEUED_FEEDS_REMOVED, "[]") + .apply(); + + } + + protected void enqueueFeedAdded(String downloadUrl) { + SharedPreferences sharedPreferences = getSharedPreferences(); + String json = sharedPreferences + .getString(QUEUED_FEEDS_ADDED, "[]"); + try { + JSONArray queue = new JSONArray(json); + queue.put(downloadUrl); + sharedPreferences + .edit().putString(QUEUED_FEEDS_ADDED, queue.toString()).apply(); + + } catch (JSONException jsonException) { + jsonException.printStackTrace(); + } + } + + protected void enqueueFeedRemoved(String downloadUrl) { + SharedPreferences sharedPreferences = getSharedPreferences(); + String json = sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]"); + try { + JSONArray queue = new JSONArray(json); + queue.put(downloadUrl); + sharedPreferences.edit().putString(QUEUED_FEEDS_REMOVED, queue.toString()) + .apply(); + } catch (JSONException jsonException) { + jsonException.printStackTrace(); + } + } + + protected void enqueueEpisodeAction(EpisodeAction action) { + SharedPreferences sharedPreferences = getSharedPreferences(); + String json = sharedPreferences.getString(QUEUED_EPISODE_ACTIONS, "[]"); + try { + JSONArray queue = new JSONArray(json); + queue.put(action.writeToJsonObject()); + sharedPreferences.edit().putString( + QUEUED_EPISODE_ACTIONS, queue.toString() + ).apply(); + } catch (JSONException jsonException) { + jsonException.printStackTrace(); + } + } + + private SharedPreferences getSharedPreferences() { + return sharedPreferences; + } +} diff --git a/core/src/main/res/drawable-nodpi/nextcloud_logo.png b/core/src/main/res/drawable-nodpi/nextcloud_logo.png Binary files differnew file mode 100644 index 000000000..2164e37fb --- /dev/null +++ b/core/src/main/res/drawable-nodpi/nextcloud_logo.png diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 5f509a9b6..7aa32abe3 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -356,7 +356,7 @@ <string name="storage_sum">Episode auto delete, Import, Export</string> <string name="project_pref">Project</string> <string name="synchronization_pref">Synchronization</string> - <string name="synchronization_sum">Synchronize with other devices using gpodder.net</string> + <string name="synchronization_sum">Synchronize with other devices</string> <string name="automation">Automation</string> <string name="download_pref_details">Details</string> <string name="import_export_pref">Import/Export</string> @@ -447,17 +447,20 @@ <string name="pref_theme_title_dark">Dark</string> <string name="pref_theme_title_trueblack">Black (AMOLED ready)</string> <string name="pref_episode_cache_unlimited">Unlimited</string> - <string name="pref_gpodnet_authenticate_title">Login</string> - <string name="pref_gpodnet_authenticate_sum">Login with your gpodder.net account in order to sync your subscriptions.</string> - <string name="pref_gpodnet_logout_title">Logout</string> - <string name="pref_gpodnet_logout_toast">Logout was successful</string> + <string name="synchronization_logout">Logout</string> + <string name="pref_synchronization_logout_toast">Logout was successful</string> <string name="pref_gpodnet_setlogin_information_title">Change login information</string> <string name="pref_gpodnet_setlogin_information_sum">Change the login information for your gpodder.net account.</string> - <string name="pref_gpodnet_sync_changes_title">Synchronize now</string> - <string name="pref_gpodnet_sync_changes_sum">Sync subscription and episode state changes with gpodder.net.</string> - <string name="pref_gpodnet_full_sync_title">Force full synchronization</string> - <string name="pref_gpodnet_full_sync_sum">Sync all subscriptions and episode states with gpodder.net.</string> - <string name="pref_gpodnet_login_status"><![CDATA[Logged in as <i>%1$s</i> with device <i>%2$s</i>]]></string> + <string name="synchronization_sync_changes_title">Synchronize now</string> + <string name="synchronization_full_sync_title">Force full synchronization</string> + <string name="synchronization_login_status"><![CDATA[Logged in as <i>%1$s</i> on <i>%2$s</i>. <br/><br/>You can choose your synchronization provider again once you have logged out]]></string> + <string name="synchronization_summary_unchoosen">You can choose from multiple providers to synchronize your subscriptions and episode play state with</string> + <string name="synchronization_summary_nextcloud">Gpoddersync is an open-source Nextcloud app that you can easily install on your own server. The app is independent of the AntennaPod project.</string> + <string name="synchronization_nextcloud_authenticate_browser">Grant access using the opened web browser and come back to AntennaPod.</string> + <string name="synchronization_choose_title">Choose synchronization provider</string> + <string name="synchronization_force_sync_summary">Re-synchronize all subscriptions and episode states</string> + <string name="synchronization_sync_summary">Synchronize subscription and episode state changes</string> + <string name="dialog_choose_sync_service_title">Choose synchronization provider</string> <string name="pref_playback_speed_sum">Customize the speeds available for variable speed playback</string> <string name="pref_feed_playback_speed_sum">The speed to use when starting audio playback for episodes in this podcast</string> <string name="pref_feed_skip">Auto Skip</string> diff --git a/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java index 356a7f77e..552f7d70a 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java @@ -14,5 +14,6 @@ public class GuidValidatorTest extends TestCase { assertFalse(GuidValidator.isValidGuid("\n")); assertFalse(GuidValidator.isValidGuid(" \n")); assertFalse(GuidValidator.isValidGuid(null)); + assertFalse(GuidValidator.isValidGuid("null")); } }
\ No newline at end of file |