summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorthrillfall <thrillfall@users.noreply.github.com>2021-10-06 22:12:47 +0200
committerGitHub <noreply@github.com>2021-10-06 22:12:47 +0200
commitbc85ebc806367d863973bc9434e7b0d9d5fd2168 (patch)
tree5a729b84f1a12c3de8d3178ad7d688eb6bb552be
parentdab44b68436601f415edb095da605811e985eb00 (diff)
downloadAntennaPod-bc85ebc806367d863973bc9434e7b0d9d5fd2168.zip
Add synchronization with gPodder Nextcloud server app (#5243)
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java11
-rw-r--r--app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java128
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java11
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java6
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java (renamed from app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java)52
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/NextcloudAuthenticationFragment.java92
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/SynchronizationPreferencesFragment.java222
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java12
-rw-r--r--app/src/main/res/layout/alertdialog_sync_provider_chooser.xml24
-rw-r--r--app/src/main/res/layout/nextcloud_auth_dialog.xml63
-rw-r--r--app/src/main/res/xml/preferences.xml2
-rw-r--r--app/src/main/res/xml/preferences_gpodder.xml28
-rw-r--r--app/src/main/res/xml/preferences_synchronization.xml31
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java115
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java11
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java49
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java38
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java31
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java1
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java3
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java35
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java362
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationCredentials.java67
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java47
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationSettings.java83
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java67
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java140
-rw-r--r--core/src/main/res/drawable-nodpi/nextcloud_logo.pngbin0 -> 3432 bytes
-rw-r--r--core/src/main/res/values/strings.xml23
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java1
-rw-r--r--net/sync/gpoddernet/build.gradle3
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java41
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java125
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java60
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java107
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java169
-rw-r--r--net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java9
41 files changed, 1526 insertions, 764 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
index 600204554..1fc16ab32 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java
@@ -6,6 +6,7 @@ import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
import android.view.MenuItem;
+
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -21,13 +22,13 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.databinding.SettingsActivityBinding;
import de.danoeh.antennapod.fragment.preferences.AutoDownloadPreferencesFragment;
-import de.danoeh.antennapod.fragment.preferences.GpodderPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.ImportExportPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.MainPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.NetworkPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.NotificationPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.StoragePreferencesFragment;
+import de.danoeh.antennapod.fragment.preferences.synchronization.SynchronizationPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment;
@@ -76,8 +77,8 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe
prefFragment = new ImportExportPreferencesFragment();
} else if (screen == R.xml.preferences_autodownload) {
prefFragment = new AutoDownloadPreferencesFragment();
- } else if (screen == R.xml.preferences_gpodder) {
- prefFragment = new GpodderPreferencesFragment();
+ } else if (screen == R.xml.preferences_synchronization) {
+ prefFragment = new SynchronizationPreferencesFragment();
} else if (screen == R.xml.preferences_playback) {
prefFragment = new PlaybackPreferencesFragment();
} else if (screen == R.xml.preferences_notifications) {
@@ -101,8 +102,8 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe
return R.string.import_export_pref;
} else if (preferences == R.xml.preferences_user_interface) {
return R.string.user_interface_label;
- } else if (preferences == R.xml.preferences_gpodder) {
- return R.string.gpodnet_main_label;
+ } else if (preferences == R.xml.preferences_synchronization) {
+ return R.string.synchronization_pref;
} else if (preferences == R.xml.preferences_notifications) {
return R.string.notification_pref_fragment;
} else if (preferences == R.xml.feed_settings) {
diff --git a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java
index f97c1c7ab..340783208 100644
--- a/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java
+++ b/app/src/main/java/de/danoeh/antennapod/discovery/GpodnetPodcastSearcher.java
@@ -1,6 +1,6 @@
package de.danoeh.antennapod.discovery;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException;
@@ -18,8 +18,8 @@ public class GpodnetPodcastSearcher implements PodcastSearcher {
return Single.create((SingleOnSubscribe<List<PodcastSearchResult>>) subscriber -> {
try {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
List<GpodnetPodcast> gpodnetPodcasts = service.searchPodcasts(query, 0);
List<PodcastSearchResult> results = new ArrayList<>();
for (GpodnetPodcast podcast : gpodnetPodcasts) {
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
index c813cbf7a..af502ce13 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java
@@ -15,7 +15,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.activity.OnlineFeedViewActivity;
import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetServiceException;
@@ -76,8 +76,8 @@ public abstract class PodcastListFragment extends Fragment {
disposable = Observable.fromCallable(
() -> {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
return loadPodcastData(service);
})
.subscribeOn(Schedulers.io())
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
index f961e30bb..abdfab941 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java
@@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
import androidx.fragment.app.ListFragment;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
@@ -51,8 +51,8 @@ public class TagListFragment extends ListFragment {
disposable = Observable.fromCallable(
() -> {
GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
return service.getTopTags(COUNT);
})
.subscribeOn(Schedulers.io())
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java
deleted file mode 100644
index 4fb734e17..000000000
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderPreferencesFragment.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package de.danoeh.antennapod.fragment.preferences;
-
-import android.app.Activity;
-import android.os.Bundle;
-import androidx.core.text.HtmlCompat;
-import androidx.preference.PreferenceFragmentCompat;
-
-import android.text.Spanned;
-import android.text.format.DateUtils;
-import com.google.android.material.snackbar.Snackbar;
-import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.PreferenceActivity;
-import de.danoeh.antennapod.core.event.SyncServiceEvent;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
-import de.danoeh.antennapod.core.sync.SyncService;
-import de.danoeh.antennapod.dialog.AuthenticationDialog;
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-
-public class GpodderPreferencesFragment extends PreferenceFragmentCompat {
- private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate";
- private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information";
- private static final String PREF_GPODNET_SYNC = "pref_gpodnet_sync";
- private static final String PREF_GPODNET_FORCE_FULL_SYNC = "pref_gpodnet_force_full_sync";
- private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout";
-
- @Override
- public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
- addPreferencesFromResource(R.xml.preferences_gpodder);
- setupGpodderScreen();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.gpodnet_main_label);
- updateGpodnetPreferenceScreen();
- EventBus.getDefault().register(this);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- EventBus.getDefault().unregister(this);
- ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle("");
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
- public void syncStatusChanged(SyncServiceEvent event) {
- updateGpodnetPreferenceScreen();
- if (!GpodnetPreferences.loggedIn()) {
- return;
- }
- if (event.getMessageResId() == R.string.sync_status_error
- || event.getMessageResId() == R.string.sync_status_success) {
- updateLastGpodnetSyncReport(SyncService.isLastSyncSuccessful(getContext()),
- SyncService.getLastSyncAttempt(getContext()));
- } else {
- ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(event.getMessageResId());
- }
- }
-
- private void setupGpodderScreen() {
- final Activity activity = getActivity();
-
- findPreference(PREF_GPODNET_LOGIN).setOnPreferenceClickListener(preference -> {
- new GpodderAuthenticationFragment().show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG);
- return true;
- });
- findPreference(PREF_GPODNET_SETLOGIN_INFORMATION)
- .setOnPreferenceClickListener(preference -> {
- AuthenticationDialog dialog = new AuthenticationDialog(activity,
- R.string.pref_gpodnet_setlogin_information_title, false, GpodnetPreferences.getUsername(),
- null) {
-
- @Override
- protected void onConfirmed(String username, String password) {
- GpodnetPreferences.setPassword(password);
- }
- };
- dialog.show();
- return true;
- });
- findPreference(PREF_GPODNET_SYNC).setOnPreferenceClickListener(preference -> {
- SyncService.syncImmediately(getActivity().getApplicationContext());
- return true;
- });
- findPreference(PREF_GPODNET_FORCE_FULL_SYNC).setOnPreferenceClickListener(preference -> {
- SyncService.fullSync(getContext());
- return true;
- });
- findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(preference -> {
- GpodnetPreferences.logout();
- Snackbar.make(getView(), R.string.pref_gpodnet_logout_toast, Snackbar.LENGTH_LONG).show();
- updateGpodnetPreferenceScreen();
- return true;
- });
- }
-
- private void updateGpodnetPreferenceScreen() {
- final boolean loggedIn = GpodnetPreferences.loggedIn();
- findPreference(PREF_GPODNET_LOGIN).setEnabled(!loggedIn);
- findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn);
- findPreference(PREF_GPODNET_SYNC).setEnabled(loggedIn);
- findPreference(PREF_GPODNET_FORCE_FULL_SYNC).setEnabled(loggedIn);
- findPreference(PREF_GPODNET_LOGOUT).setEnabled(loggedIn);
- if (loggedIn) {
- String format = getActivity().getString(R.string.pref_gpodnet_login_status);
- String summary = String.format(format, GpodnetPreferences.getUsername(),
- GpodnetPreferences.getDeviceID());
- Spanned formattedSummary = HtmlCompat.fromHtml(summary, HtmlCompat.FROM_HTML_MODE_LEGACY);
- findPreference(PREF_GPODNET_LOGOUT).setSummary(formattedSummary);
- updateLastGpodnetSyncReport(SyncService.isLastSyncSuccessful(getContext()),
- SyncService.getLastSyncAttempt(getContext()));
- } else {
- findPreference(PREF_GPODNET_LOGOUT).setSummary(null);
- }
- }
-
- private void updateLastGpodnetSyncReport(boolean successful, long lastTime) {
- String status = String.format("%1$s (%2$s)", getString(successful
- ? R.string.gpodnetsync_pref_report_successful : R.string.gpodnetsync_pref_report_failed),
- DateUtils.getRelativeDateTimeString(getContext(),
- lastTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME));
- ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(status);
- }
-}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java
index cc09acbca..83ad3110a 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java
@@ -17,12 +17,11 @@ import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.fragment.preferences.about.AboutFragment;
public class MainPreferencesFragment extends PreferenceFragmentCompat {
- private static final String TAG = "MainPreferencesFragment";
private static final String PREF_SCREEN_USER_INTERFACE = "prefScreenInterface";
private static final String PREF_SCREEN_PLAYBACK = "prefScreenPlayback";
private static final String PREF_SCREEN_NETWORK = "prefScreenNetwork";
- private static final String PREF_SCREEN_GPODDER = "prefScreenGpodder";
+ private static final String PREF_SCREEN_SYNCHRONIZATION = "prefScreenSynchronization";
private static final String PREF_SCREEN_STORAGE = "prefScreenStorage";
private static final String PREF_DOCUMENTATION = "prefDocumentation";
private static final String PREF_VIEW_FORUM = "prefViewForum";
@@ -74,8 +73,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_network);
return true;
});
- findPreference(PREF_SCREEN_GPODDER).setOnPreferenceClickListener(preference -> {
- ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_gpodder);
+ findPreference(PREF_SCREEN_SYNCHRONIZATION).setOnPreferenceClickListener(preference -> {
+ ((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_synchronization);
return true;
});
findPreference(PREF_SCREEN_STORAGE).setOnPreferenceClickListener(preference -> {
@@ -142,8 +141,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_network))
.addBreadcrumb(R.string.automation)
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_autodownload));
- config.index(R.xml.preferences_gpodder)
- .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_gpodder));
+ config.index(R.xml.preferences_synchronization)
+ .addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_synchronization));
config.index(R.xml.preferences_notifications)
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_notifications));
config.index(R.xml.feed_settings)
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java
index 94e151f7a..ba17cedb2 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/NotificationPreferencesFragment.java
@@ -4,11 +4,10 @@ import android.os.Bundle;
import androidx.preference.PreferenceFragmentCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
public class NotificationPreferencesFragment extends PreferenceFragmentCompat {
- private static final String TAG = "NotificationPrefFragment";
private static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications";
@Override
@@ -24,7 +23,6 @@ public class NotificationPreferencesFragment extends PreferenceFragmentCompat {
}
private void setUpScreen() {
- final boolean loggedIn = GpodnetPreferences.loggedIn();
- findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(loggedIn);
+ findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(SynchronizationSettings.isProviderConnected());
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java
index c0bf3e0ea..9dfe6840c 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/GpodderAuthenticationFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/GpodderAuthenticationFragment.java
@@ -1,4 +1,4 @@
-package de.danoeh.antennapod.fragment.preferences;
+package de.danoeh.antennapod.fragment.preferences.synchronization;
import android.app.Dialog;
import android.content.Context;
@@ -15,30 +15,35 @@ import android.widget.ProgressBar;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.ViewFlipper;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
+
import com.google.android.material.button.MaterialButton;
import com.google.android.material.textfield.TextInputLayout;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
import de.danoeh.antennapod.core.sync.SyncService;
-import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
import de.danoeh.antennapod.core.util.FileNameGenerator;
import de.danoeh.antennapod.core.util.IntentUtils;
+import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
/**
* Guides the user through the authentication process.
*/
@@ -83,23 +88,24 @@ public class GpodderAuthenticationFragment extends DialogFragment {
final RadioGroup serverRadioGroup = view.findViewById(R.id.serverRadioGroup);
final EditText serverUrlText = view.findViewById(R.id.serverUrlText);
- if (!GpodnetService.DEFAULT_BASE_HOST.equals(GpodnetPreferences.getHosturl())) {
- serverUrlText.setText(GpodnetPreferences.getHosturl());
+ if (!GpodnetService.DEFAULT_BASE_HOST.equals(SynchronizationCredentials.getHosturl())) {
+ serverUrlText.setText(SynchronizationCredentials.getHosturl());
}
final TextInputLayout serverUrlTextInput = view.findViewById(R.id.serverUrlTextInput);
serverRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
serverUrlTextInput.setVisibility(checkedId == R.id.customServerRadio ? View.VISIBLE : View.GONE);
});
selectHost.setOnClickListener(v -> {
+ SynchronizationCredentials.clear(getContext());
if (serverRadioGroup.getCheckedRadioButtonId() == R.id.customServerRadio) {
- GpodnetPreferences.setHosturl(serverUrlText.getText().toString());
+ SynchronizationCredentials.setHosturl(serverUrlText.getText().toString());
} else {
- GpodnetPreferences.setHosturl(GpodnetService.DEFAULT_BASE_HOST);
+ SynchronizationCredentials.setHosturl(GpodnetService.DEFAULT_BASE_HOST);
}
service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
- GpodnetPreferences.getHosturl(), GpodnetPreferences.getDeviceID(),
- GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword());
- getDialog().setTitle(GpodnetPreferences.getHosturl());
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
+ getDialog().setTitle(SynchronizationCredentials.getHosturl());
advance();
});
}
@@ -116,7 +122,7 @@ public class GpodderAuthenticationFragment extends DialogFragment {
createAccount.setPaintFlags(createAccount.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
createAccount.setOnClickListener(v -> IntentUtils.openInBrowser(getContext(), "https://gpodder.net/register/"));
- if (GpodnetPreferences.getHosturl().startsWith("http://")) {
+ if (SynchronizationCredentials.getHosturl().startsWith("http://")) {
createAccountWarning.setVisibility(View.VISIBLE);
}
password.setOnEditorActionListener((v, actionID, event) ->
@@ -265,15 +271,8 @@ public class GpodderAuthenticationFragment extends DialogFragment {
});
}
- private void writeLoginCredentials() {
- GpodnetPreferences.setUsername(username);
- GpodnetPreferences.setPassword(password);
- GpodnetPreferences.setDeviceID(selectedDevice.getId());
- }
-
private void advance() {
if (currentStep < STEP_FINISH) {
-
View view = viewFlipper.getChildAt(currentStep + 1);
if (currentStep == STEP_DEFAULT) {
setupHostView(view);
@@ -289,7 +288,10 @@ public class GpodderAuthenticationFragment extends DialogFragment {
if (selectedDevice == null) {
throw new IllegalStateException("Device must not be null here");
} else {
- writeLoginCredentials();
+ SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.GPODDER_NET);
+ SynchronizationCredentials.setUsername(username);
+ SynchronizationCredentials.setPassword(password);
+ SynchronizationCredentials.setDeviceID(selectedDevice.getId());
setupFinishView(view);
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/NextcloudAuthenticationFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/NextcloudAuthenticationFragment.java
new file mode 100644
index 000000000..2e9260c1d
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/NextcloudAuthenticationFragment.java
@@ -0,0 +1,92 @@
+package de.danoeh.antennapod.fragment.preferences.synchronization;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
+import de.danoeh.antennapod.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
+import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+import de.danoeh.antennapod.databinding.NextcloudAuthDialogBinding;
+import de.danoeh.antennapod.net.sync.nextcloud.NextcloudLoginFlow;
+
+/**
+ * Guides the user through the authentication process.
+ */
+public class NextcloudAuthenticationFragment extends DialogFragment
+ implements NextcloudLoginFlow.AuthenticationCallback {
+ public static final String TAG = "NextcloudAuthenticationFragment";
+ private NextcloudAuthDialogBinding viewBinding;
+ private NextcloudLoginFlow nextcloudLoginFlow;
+ private boolean shouldDismiss = false;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
+ dialog.setTitle(R.string.gpodnetauth_login_butLabel);
+ dialog.setNegativeButton(R.string.cancel_label, null);
+ dialog.setCancelable(false);
+ this.setCancelable(false);
+
+ viewBinding = NextcloudAuthDialogBinding.inflate(getLayoutInflater());
+ dialog.setView(viewBinding.getRoot());
+
+ viewBinding.loginButton.setOnClickListener(v -> {
+ viewBinding.errorText.setVisibility(View.GONE);
+ viewBinding.loginButton.setVisibility(View.GONE);
+ viewBinding.loginProgressContainer.setVisibility(View.VISIBLE);
+ nextcloudLoginFlow = new NextcloudLoginFlow(AntennapodHttpClient.getHttpClient(),
+ viewBinding.serverUrlText.getText().toString(), getContext(), this);
+ nextcloudLoginFlow.start();
+ });
+
+ return dialog.create();
+ }
+
+ @Override
+ public void onDismiss(@NonNull DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (nextcloudLoginFlow != null) {
+ nextcloudLoginFlow.cancel();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (shouldDismiss) {
+ dismiss();
+ }
+ }
+
+ @Override
+ public void onNextcloudAuthenticated(String server, String username, String password) {
+ SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.NEXTCLOUD_GPODDER);
+ SynchronizationCredentials.clear(getContext());
+ SynchronizationCredentials.setPassword(password);
+ SynchronizationCredentials.setHosturl(server);
+ SynchronizationCredentials.setUsername(username);
+ SyncService.fullSync(getContext());
+ if (isVisible()) {
+ dismiss();
+ } else {
+ shouldDismiss = true;
+ }
+ }
+
+ @Override
+ public void onNextcloudAuthError(String errorMessage) {
+ viewBinding.loginProgressContainer.setVisibility(View.GONE);
+ viewBinding.errorText.setVisibility(View.VISIBLE);
+ viewBinding.errorText.setText(errorMessage);
+ viewBinding.loginButton.setVisibility(View.VISIBLE);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/SynchronizationPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/SynchronizationPreferencesFragment.java
new file mode 100644
index 000000000..9b63b38ec
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/synchronization/SynchronizationPreferencesFragment.java
@@ -0,0 +1,222 @@
+package de.danoeh.antennapod.fragment.preferences.synchronization;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.Spanned;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.text.HtmlCompat;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.PreferenceActivity;
+import de.danoeh.antennapod.core.event.SyncServiceEvent;
+import de.danoeh.antennapod.core.sync.SynchronizationCredentials;
+import de.danoeh.antennapod.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+import de.danoeh.antennapod.dialog.AuthenticationDialog;
+
+public class SynchronizationPreferencesFragment extends PreferenceFragmentCompat {
+ private static final String PREFERENCE_SYNCHRONIZATION_DESCRIPTION = "preference_synchronization_description";
+ private static final String PREFERENCE_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information";
+ private static final String PREFERENCE_SYNC = "pref_synchronization_sync";
+ private static final String PREFERENCE_FORCE_FULL_SYNC = "pref_synchronization_force_full_sync";
+ private static final String PREFERENCE_LOGOUT = "pref_synchronization_logout";
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.preferences_synchronization);
+ setupScreen();
+ updateScreen();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.synchronization_pref);
+ updateScreen();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventBus.getDefault().unregister(this);
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle("");
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
+ public void syncStatusChanged(SyncServiceEvent event) {
+ if (!SynchronizationSettings.isProviderConnected()) {
+ return;
+ }
+ updateScreen();
+ if (event.getMessageResId() == R.string.sync_status_error
+ || event.getMessageResId() == R.string.sync_status_success) {
+ updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful(),
+ SynchronizationSettings.getLastSyncAttempt());
+ } else {
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(event.getMessageResId());
+ }
+ }
+
+ private void setupScreen() {
+ final Activity activity = getActivity();
+ findPreference(PREFERENCE_GPODNET_SETLOGIN_INFORMATION)
+ .setOnPreferenceClickListener(preference -> {
+ AuthenticationDialog dialog = new AuthenticationDialog(activity,
+ R.string.pref_gpodnet_setlogin_information_title,
+ false, SynchronizationCredentials.getUsername(), null) {
+ @Override
+ protected void onConfirmed(String username, String password) {
+ SynchronizationCredentials.setPassword(password);
+ }
+ };
+ dialog.show();
+ return true;
+ });
+ findPreference(PREFERENCE_SYNC).setOnPreferenceClickListener(preference -> {
+ SyncService.syncImmediately(getActivity().getApplicationContext());
+ return true;
+ });
+ findPreference(PREFERENCE_FORCE_FULL_SYNC).setOnPreferenceClickListener(preference -> {
+ SyncService.fullSync(getContext());
+ return true;
+ });
+ findPreference(PREFERENCE_LOGOUT).setOnPreferenceClickListener(preference -> {
+ SynchronizationCredentials.clear(getContext());
+ Snackbar.make(getView(), R.string.pref_synchronization_logout_toast, Snackbar.LENGTH_LONG).show();
+ SynchronizationSettings.setSelectedSyncProvider(null);
+ updateScreen();
+ return true;
+ });
+ }
+
+ private void updateScreen() {
+ final boolean loggedIn = SynchronizationSettings.isProviderConnected();
+ Preference preferenceHeader = findPreference(PREFERENCE_SYNCHRONIZATION_DESCRIPTION);
+ if (loggedIn) {
+ SynchronizationProviderViewData selectedProvider =
+ SynchronizationProviderViewData.fromIdentifier(getSelectedSyncProviderKey());
+ preferenceHeader.setTitle("");
+ preferenceHeader.setSummary(selectedProvider.getSummaryResource());
+ preferenceHeader.setIcon(selectedProvider.getIconResource());
+ preferenceHeader.setOnPreferenceClickListener(null);
+ } else {
+ preferenceHeader.setTitle(R.string.synchronization_choose_title);
+ preferenceHeader.setSummary(R.string.synchronization_summary_unchoosen);
+ preferenceHeader.setIcon(R.drawable.ic_cloud);
+ preferenceHeader.setOnPreferenceClickListener((preference) -> {
+ chooseProviderAndLogin();
+ return true;
+ });
+ }
+
+ Preference gpodnetSetLoginPreference = findPreference(PREFERENCE_GPODNET_SETLOGIN_INFORMATION);
+ gpodnetSetLoginPreference.setVisible(isProviderSelected(SynchronizationProviderViewData.GPODDER_NET));
+ gpodnetSetLoginPreference.setEnabled(loggedIn);
+ findPreference(PREFERENCE_SYNC).setEnabled(loggedIn);
+ findPreference(PREFERENCE_FORCE_FULL_SYNC).setEnabled(loggedIn);
+ findPreference(PREFERENCE_LOGOUT).setEnabled(loggedIn);
+ if (loggedIn) {
+ String summary = getString(R.string.synchronization_login_status,
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getHosturl());
+ Spanned formattedSummary = HtmlCompat.fromHtml(summary, HtmlCompat.FROM_HTML_MODE_LEGACY);
+ findPreference(PREFERENCE_LOGOUT).setSummary(formattedSummary);
+ updateLastSyncReport(SynchronizationSettings.isLastSyncSuccessful(),
+ SynchronizationSettings.getLastSyncAttempt());
+ } else {
+ findPreference(PREFERENCE_LOGOUT).setSummary(null);
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(null);
+ }
+ }
+
+ private void chooseProviderAndLogin() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setTitle(R.string.dialog_choose_sync_service_title);
+
+ SynchronizationProviderViewData[] providers = SynchronizationProviderViewData.values();
+ ListAdapter adapter = new ArrayAdapter<SynchronizationProviderViewData>(
+ getContext(), R.layout.alertdialog_sync_provider_chooser, providers) {
+
+ ViewHolder holder;
+
+ class ViewHolder {
+ ImageView icon;
+ TextView title;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final LayoutInflater inflater = LayoutInflater.from(getContext());
+ if (convertView == null) {
+ convertView = inflater.inflate(
+ R.layout.alertdialog_sync_provider_chooser, null);
+
+ holder = new ViewHolder();
+ holder.icon = (ImageView) convertView.findViewById(R.id.icon);
+ holder.title = (TextView) convertView.findViewById(R.id.title);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+ SynchronizationProviderViewData synchronizationProviderViewData = getItem(position);
+ holder.title.setText(synchronizationProviderViewData.getSummaryResource());
+ holder.icon.setImageResource(synchronizationProviderViewData.getIconResource());
+ return convertView;
+ }
+ };
+
+ builder.setAdapter(adapter, (dialog, which) -> {
+ switch (providers[which]) {
+ case GPODDER_NET:
+ new GpodderAuthenticationFragment()
+ .show(getChildFragmentManager(), GpodderAuthenticationFragment.TAG);
+ break;
+ case NEXTCLOUD_GPODDER:
+ new NextcloudAuthenticationFragment()
+ .show(getChildFragmentManager(), NextcloudAuthenticationFragment.TAG);
+ break;
+ default:
+ break;
+ }
+ updateScreen();
+ });
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private boolean isProviderSelected(@NonNull SynchronizationProviderViewData provider) {
+ String selectedSyncProviderKey = getSelectedSyncProviderKey();
+ return provider.getIdentifier().equals(selectedSyncProviderKey);
+ }
+
+ private String getSelectedSyncProviderKey() {
+ return SynchronizationSettings.getSelectedSyncProviderKey();
+ }
+
+ private void updateLastSyncReport(boolean successful, long lastTime) {
+ String status = String.format("%1$s (%2$s)", getString(successful
+ ? R.string.gpodnetsync_pref_report_successful : R.string.gpodnetsync_pref_report_failed),
+ DateUtils.getRelativeDateTimeString(getContext(),
+ lastTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME));
+ ((PreferenceActivity) getActivity()).getSupportActionBar().setSubtitle(status);
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
index c272af7d5..23fdb86de 100644
--- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
@@ -13,12 +13,12 @@ import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
-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.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBWriter;
-import de.danoeh.antennapod.core.sync.SyncService;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+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.ShareUtils;
@@ -151,7 +151,7 @@ public class FeedItemMenuHandler {
} else if (menuItemId == R.id.mark_read_item) {
selectedItem.setPlayed(true);
DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, true);
- if (GpodnetPreferences.loggedIn()) {
+ if (SynchronizationSettings.isProviderConnected()) {
FeedMedia media = selectedItem.getMedia();
// not all items have media, Gpodder only cares about those that do
if (media != null) {
@@ -161,17 +161,17 @@ public class FeedItemMenuHandler {
.position(media.getDuration() / 1000)
.total(media.getDuration() / 1000)
.build();
- SyncService.enqueueEpisodeAction(context, actionPlay);
+ SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, actionPlay);
}
}
} else if (menuItemId == R.id.mark_unread_item) {
selectedItem.setPlayed(false);
DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, false);
- if (GpodnetPreferences.loggedIn() && selectedItem.getMedia() != null) {
+ if (selectedItem.getMedia() != null) {
EpisodeAction actionNew = new EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
.currentTimestamp()
.build();
- SyncService.enqueueEpisodeAction(context, actionNew);
+ SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, actionNew);
}
} else if (menuItemId == R.id.add_to_queue_item) {
DBWriter.addQueueItem(context, selectedItem);
diff --git a/app/src/main/res/layout/alertdialog_sync_provider_chooser.xml b/app/src/main/res/layout/alertdialog_sync_provider_chooser.xml
new file mode 100644
index 000000000..9b4d62804
--- /dev/null
+++ b/app/src/main/res/layout/alertdialog_sync_provider_chooser.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="16dp">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginRight="16dip"
+ android:layout_marginEnd="16dip"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:layout_gravity="center" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/nextcloud_auth_dialog.xml b/app/src/main/res/layout/nextcloud_auth_dialog.xml
new file mode 100644
index 000000000..345eec88b
--- /dev/null
+++ b/app/src/main/res/layout/nextcloud_auth_dialog.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:orientation="vertical"
+ android:clipToPadding="false">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/serverUrlTextInput"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/serverUrlText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/gpodnetauth_host"
+ android:inputType="textNoSuggestions"
+ android:lines="1"
+ android:imeOptions="actionNext|flagNoFullscreen" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <LinearLayout
+ android:id="@+id/loginProgressContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:orientation="horizontal"
+ android:layout_gravity="center_vertical">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:layout_marginRight="8dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/synchronization_nextcloud_authenticate_browser" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/errorText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:textColor="@color/download_failed_red"
+ android:layout_marginBottom="16dp" />
+
+ <Button
+ android:id="@+id/loginButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_login_butLabel" />
+
+</LinearLayout>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index d528945c7..7c5012899 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -28,7 +28,7 @@
android:icon="@drawable/ic_network" />
<Preference
- android:key="prefScreenGpodder"
+ android:key="prefScreenSynchronization"
android:title="@string/synchronization_pref"
android:summary="@string/synchronization_sum"
android:icon="@drawable/ic_cloud" />
diff --git a/app/src/main/res/xml/preferences_gpodder.xml b/app/src/main/res/xml/preferences_gpodder.xml
deleted file mode 100644
index a210b8e11..000000000
--- a/app/src/main/res/xml/preferences_gpodder.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<PreferenceScreen
- xmlns:android="http://schemas.android.com/apk/res/android">
- <Preference
- android:key="pref_gpodnet_description"
- android:icon="@drawable/gpodder_icon"
- android:summary="@string/gpodnet_description"/>
- <Preference
- android:key="pref_gpodnet_authenticate"
- android:title="@string/pref_gpodnet_authenticate_title"
- android:summary="@string/pref_gpodnet_authenticate_sum"/>
- <Preference
- android:key="pref_gpodnet_setlogin_information"
- android:title="@string/pref_gpodnet_setlogin_information_title"
- android:summary="@string/pref_gpodnet_setlogin_information_sum"/>
- <Preference
- android:key="pref_gpodnet_sync"
- android:title="@string/pref_gpodnet_sync_changes_title"
- android:summary="@string/pref_gpodnet_sync_changes_sum"/>
- <Preference
- android:key="pref_gpodnet_force_full_sync"
- android:title="@string/pref_gpodnet_full_sync_title"
- android:summary="@string/pref_gpodnet_full_sync_sum"/>
- <Preference
- android:key="pref_gpodnet_logout"
- android:title="@string/pref_gpodnet_logout_title"/>
-
-</PreferenceScreen>
diff --git a/app/src/main/res/xml/preferences_synchronization.xml b/app/src/main/res/xml/preferences_synchronization.xml
new file mode 100644
index 000000000..fbd4ccc79
--- /dev/null
+++ b/app/src/main/res/xml/preferences_synchronization.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <Preference
+ android:key="preference_synchronization_description"
+ android:icon="@drawable/ic_notification_sync"
+ android:summary="@string/synchronization_summary_unchoosen"/>
+
+ <Preference
+ android:key="pref_gpodnet_setlogin_information"
+ android:title="@string/pref_gpodnet_setlogin_information_title"
+ android:summary="@string/pref_gpodnet_setlogin_information_sum"
+ app:isPreferenceVisible="false"/>
+
+ <Preference
+ android:key="pref_synchronization_sync"
+ android:title="@string/synchronization_sync_changes_title"
+ android:summary="@string/synchronization_sync_summary"/>
+
+ <Preference
+ android:key="pref_synchronization_force_full_sync"
+ android:title="@string/synchronization_full_sync_title"
+ android:summary="@string/synchronization_force_sync_summary"/>
+
+ <Preference
+ android:key="pref_synchronization_logout"
+ android:title="@string/synchronization_logout"/>
+
+</PreferenceScreen>
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
new file mode 100644
index 000000000..2164e37fb
--- /dev/null
+++ b/core/src/main/res/drawable-nodpi/nextcloud_logo.png
Binary files differ
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
diff --git a/net/sync/gpoddernet/build.gradle b/net/sync/gpoddernet/build.gradle
index eb5af1b60..77e9ce0f3 100644
--- a/net/sync/gpoddernet/build.gradle
+++ b/net/sync/gpoddernet/build.gradle
@@ -9,4 +9,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
implementation "org.apache.commons:commons-lang3:$commonslangVersion"
+ implementation 'commons-io:commons-io:2.5'
+ implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
+ implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java
new file mode 100644
index 000000000..ebb415248
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/HostnameParser.java
@@ -0,0 +1,41 @@
+package de.danoeh.antennapod.net.sync;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HostnameParser {
+ public String scheme;
+ public int port;
+ public String host;
+
+ // split into schema, host and port - missing parts are null
+ private static final Pattern URLSPLIT_REGEX = Pattern.compile("(?:(https?)://)?([^:]+)(?::(\\d+))?");
+
+ public HostnameParser(String hosturl) {
+ Matcher m = URLSPLIT_REGEX.matcher(hosturl);
+ if (m.matches()) {
+ scheme = m.group(1);
+ host = m.group(2);
+ if (m.group(3) == null) {
+ port = -1;
+ } else {
+ port = Integer.parseInt(m.group(3)); // regex -> can only be digits
+ }
+ } else {
+ // URL does not match regex: use it anyway -> this will cause an exception on connect
+ scheme = "https";
+ host = hosturl;
+ port = 443;
+ }
+
+ if (scheme == null) { // assume https
+ scheme = "https";
+ }
+
+ if (scheme.equals("https") && port == -1) {
+ port = 443;
+ } else if (scheme.equals("http") && port == -1) {
+ port = 80;
+ }
+ }
+}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
index eb18da80b..439a528b7 100644
--- a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/GpodnetService.java
@@ -1,25 +1,10 @@
package de.danoeh.antennapod.net.sync.gpoddernet;
import android.util.Log;
+
import androidx.annotation.NonNull;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
-import de.danoeh.antennapod.net.sync.model.EpisodeAction;
-import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast;
-import de.danoeh.antennapod.net.sync.model.ISyncService;
-import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
-import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
-import de.danoeh.antennapod.net.sync.model.SyncServiceException;
-import de.danoeh.antennapod.net.sync.model.UploadChangesResponse;
-import okhttp3.Credentials;
-import okhttp3.MediaType;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.RequestBody;
-import okhttp3.Response;
-import okhttp3.ResponseBody;
+
+import de.danoeh.antennapod.net.sync.HostnameParser;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -35,12 +20,28 @@ import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import de.danoeh.antennapod.net.sync.gpoddernet.mapper.ResponseMapper;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetPodcast;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetTag;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+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 okhttp3.Credentials;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
/**
* Communicates with the gpodder.net service.
@@ -61,43 +62,16 @@ public class GpodnetService implements ISyncService {
private final OkHttpClient httpClient;
- // split into schema, host and port - missing parts are null
- private static final Pattern URLSPLIT_REGEX = Pattern.compile("(?:(https?)://)?([^:]+)(?::(\\d+))?");
-
public GpodnetService(OkHttpClient httpClient, String baseHosturl,
String deviceId, String username, String password) {
this.httpClient = httpClient;
this.deviceId = deviceId;
this.username = username;
this.password = password;
-
- Matcher m = URLSPLIT_REGEX.matcher(baseHosturl);
- if (m.matches()) {
- this.baseScheme = m.group(1);
- this.baseHost = m.group(2);
- if (m.group(3) == null) {
- this.basePort = -1;
- } else {
- this.basePort = Integer.parseInt(m.group(3)); // regex -> can only be digits
- }
- } else {
- // URL does not match regex: use it anyway -> this will cause an exception on connect
- this.baseScheme = "https";
- this.baseHost = baseHosturl;
- this.basePort = 443;
- }
-
- if (this.baseScheme == null) { // assume https
- this.baseScheme = "https";
- }
-
- if (this.baseScheme.equals("https") && this.basePort == -1) {
- this.basePort = 443;
- }
-
- if (this.baseScheme.equals("http") && this.basePort == -1) {
- this.basePort = 80;
- }
+ HostnameParser hostname = new HostnameParser(baseHosturl == null ? DEFAULT_BASE_HOST : baseHosturl);
+ this.baseHost = hostname.host;
+ this.basePort = hostname.port;
+ this.baseScheme = hostname.scheme;
}
private void requireLoggedIn() {
@@ -434,7 +408,7 @@ public class GpodnetService implements ISyncService {
String response = executeRequest(request);
JSONObject changes = new JSONObject(response);
- return readSubscriptionChangesFromJsonObject(changes);
+ return ResponseMapper.readSubscriptionChangesFromJsonObject(changes);
} catch (URISyntaxException e) {
e.printStackTrace();
throw new IllegalStateException(e);
@@ -515,7 +489,7 @@ public class GpodnetService implements ISyncService {
String response = executeRequest(request);
JSONObject json = new JSONObject(response);
- return readEpisodeActionsFromJsonObject(json);
+ return ResponseMapper.readEpisodeActionsFromJsonObject(json);
} catch (URISyntaxException e) {
e.printStackTrace();
throw new IllegalStateException(e);
@@ -526,7 +500,6 @@ public class GpodnetService implements ISyncService {
}
-
/**
* Logs in a specific user. This method must be called if any of the methods
* that require authentication is used.
@@ -689,48 +662,6 @@ public class GpodnetService implements ISyncService {
return new GpodnetDevice(id, caption, type, subscriptions);
}
- private SubscriptionChanges readSubscriptionChangesFromJsonObject(@NonNull JSONObject object)
- throws JSONException {
-
- List<String> added = new LinkedList<>();
- JSONArray jsonAdded = object.getJSONArray("add");
- for (int i = 0; i < jsonAdded.length(); i++) {
- String addedUrl = jsonAdded.getString(i);
- // gpodder escapes colons unnecessarily
- addedUrl = addedUrl.replace("%3A", ":");
- added.add(addedUrl);
- }
-
- List<String> removed = new LinkedList<>();
- JSONArray jsonRemoved = object.getJSONArray("remove");
- for (int i = 0; i < jsonRemoved.length(); i++) {
- String removedUrl = jsonRemoved.getString(i);
- // gpodder escapes colons unnecessarily
- removedUrl = removedUrl.replace("%3A", ":");
- removed.add(removedUrl);
- }
-
- long timestamp = object.getLong("timestamp");
- return new SubscriptionChanges(added, removed, timestamp);
- }
-
- private EpisodeActionChanges readEpisodeActionsFromJsonObject(@NonNull JSONObject object)
- throws JSONException {
-
- List<EpisodeAction> episodeActions = new ArrayList<>();
-
- long timestamp = object.getLong("timestamp");
- JSONArray jsonActions = object.getJSONArray("actions");
- for (int i = 0; i < jsonActions.length(); i++) {
- JSONObject jsonAction = jsonActions.getJSONObject(i);
- EpisodeAction episodeAction = EpisodeAction.readFromJsonObject(jsonAction);
- if (episodeAction != null) {
- episodeActions.add(episodeAction);
- }
- }
- return new EpisodeActionChanges(episodeActions, timestamp);
- }
-
@Override
public void logout() {
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java
new file mode 100644
index 000000000..c8e607d74
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/gpoddernet/mapper/ResponseMapper.java
@@ -0,0 +1,60 @@
+package de.danoeh.antennapod.net.sync.gpoddernet.mapper;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+import de.danoeh.antennapod.net.sync.model.SubscriptionChanges;
+
+public class ResponseMapper {
+
+ public static SubscriptionChanges readSubscriptionChangesFromJsonObject(@NonNull JSONObject object)
+ throws JSONException {
+
+ List<String> added = new LinkedList<>();
+ JSONArray jsonAdded = object.getJSONArray("add");
+ for (int i = 0; i < jsonAdded.length(); i++) {
+ String addedUrl = jsonAdded.getString(i);
+ // gpodder escapes colons unnecessarily
+ addedUrl = addedUrl.replace("%3A", ":");
+ added.add(addedUrl);
+ }
+
+ List<String> removed = new LinkedList<>();
+ JSONArray jsonRemoved = object.getJSONArray("remove");
+ for (int i = 0; i < jsonRemoved.length(); i++) {
+ String removedUrl = jsonRemoved.getString(i);
+ // gpodder escapes colons unnecessarily
+ removedUrl = removedUrl.replace("%3A", ":");
+ removed.add(removedUrl);
+ }
+
+ long timestamp = object.getLong("timestamp");
+ return new SubscriptionChanges(added, removed, timestamp);
+ }
+
+ public static EpisodeActionChanges readEpisodeActionsFromJsonObject(@NonNull JSONObject object)
+ throws JSONException {
+
+ List<EpisodeAction> episodeActions = new ArrayList<>();
+
+ long timestamp = object.getLong("timestamp");
+ JSONArray jsonActions = object.getJSONArray("actions");
+ for (int i = 0; i < jsonActions.length(); i++) {
+ JSONObject jsonAction = jsonActions.getJSONObject(i);
+ EpisodeAction episodeAction = EpisodeAction.readFromJsonObject(jsonAction);
+ if (episodeAction != null) {
+ episodeActions.add(episodeAction);
+ }
+ }
+ return new EpisodeActionChanges(episodeActions, timestamp);
+ }
+}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java
new file mode 100644
index 000000000..b66c44402
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudLoginFlow.java
@@ -0,0 +1,107 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import de.danoeh.antennapod.net.sync.HostnameParser;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import org.json.JSONException;
+import org.json.JSONObject;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+public class NextcloudLoginFlow {
+ private static final String TAG = "NextcloudLoginFlow";
+
+ private final OkHttpClient httpClient;
+ private final HostnameParser hostname;
+ private final Context context;
+ private final AuthenticationCallback callback;
+ private String token;
+ private String endpoint;
+ private Disposable startDisposable;
+ private Disposable pollDisposable;
+
+ public NextcloudLoginFlow(OkHttpClient httpClient, String hostUrl, Context context,
+ AuthenticationCallback callback) {
+ this.httpClient = httpClient;
+ this.hostname = new HostnameParser(hostUrl);
+ this.context = context;
+ this.callback = callback;
+ }
+
+ public void start() {
+ startDisposable = Observable.fromCallable(() -> {
+ URL url = new URI(hostname.scheme, null, hostname.host, hostname.port,
+ "/index.php/login/v2", null, null).toURL();
+ JSONObject result = doRequest(url, "");
+ String loginUrl = result.getString("login");
+ this.token = result.getJSONObject("poll").getString("token");
+ this.endpoint = result.getJSONObject("poll").getString("endpoint");
+ return loginUrl;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ result -> {
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(result));
+ context.startActivity(browserIntent);
+ poll();
+ }, error -> {
+ Log.e(TAG, Log.getStackTraceString(error));
+ callback.onNextcloudAuthError(error.getLocalizedMessage());
+ });
+ }
+
+ private void poll() {
+ pollDisposable = Observable.fromCallable(() -> doRequest(URI.create(endpoint).toURL(), "token=" + token))
+ .delay(1, TimeUnit.SECONDS)
+ .retry(60 * 10) // 10 minutes
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(result -> {
+ callback.onNextcloudAuthenticated(result.getString("server"),
+ result.getString("loginName"), result.getString("appPassword"));
+ }, Throwable::printStackTrace);
+ }
+
+ public void cancel() {
+ if (startDisposable != null) {
+ startDisposable.dispose();
+ }
+ if (pollDisposable != null) {
+ pollDisposable.dispose();
+ }
+ }
+
+ private JSONObject doRequest(URL url, String bodyContent) throws IOException, JSONException {
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/x-www-form-urlencoded"), bodyContent);
+ Request request = new Request.Builder().url(url).method("POST", requestBody).build();
+ Response response = httpClient.newCall(request).execute();
+ if (response.code() != 200) {
+ throw new IOException("Return code " + response.code());
+ }
+ ResponseBody body = response.body();
+ return new JSONObject(body.string());
+ }
+
+ public interface AuthenticationCallback {
+ void onNextcloudAuthenticated(String server, String username, String password);
+
+ void onNextcloudAuthError(String errorMessage);
+ }
+}
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java
new file mode 100644
index 000000000..647a9073c
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSyncService.java
@@ -0,0 +1,169 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import de.danoeh.antennapod.net.sync.HostnameParser;
+import de.danoeh.antennapod.net.sync.gpoddernet.mapper.ResponseMapper;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetUploadChangesResponse;
+import de.danoeh.antennapod.net.sync.model.EpisodeAction;
+import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges;
+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 okhttp3.Credentials;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.List;
+
+public class NextcloudSyncService implements ISyncService {
+ private static final int UPLOAD_BULK_SIZE = 30;
+ private final OkHttpClient httpClient;
+ private final String baseScheme;
+ private final int basePort;
+ private final String baseHost;
+ private final String username;
+ private final String password;
+
+ public NextcloudSyncService(OkHttpClient httpClient, String baseHosturl,
+ String username, String password) {
+ this.httpClient = httpClient;
+ this.username = username;
+ this.password = password;
+ HostnameParser hostname = new HostnameParser(baseHosturl);
+ this.baseHost = hostname.host;
+ this.basePort = hostname.port;
+ this.baseScheme = hostname.scheme;
+ }
+
+ @Override
+ public void login() {
+ }
+
+ @Override
+ public SubscriptionChanges getSubscriptionChanges(long lastSync) throws SyncServiceException {
+ try {
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/subscriptions");
+ url.addQueryParameter("since", "" + lastSync);
+ String responseString = performRequest(url, "GET", null);
+ JSONObject json = new JSONObject(responseString);
+ return ResponseMapper.readSubscriptionChangesFromJsonObject(json);
+ } catch (JSONException | MalformedURLException e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ }
+ }
+
+ @Override
+ public UploadChangesResponse uploadSubscriptionChanges(List<String> addedFeeds,
+ List<String> removedFeeds)
+ throws NextcloudSynchronizationServiceException {
+ try {
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/subscription_change/create");
+ final JSONObject requestObject = new JSONObject();
+ requestObject.put("add", new JSONArray(addedFeeds));
+ requestObject.put("remove", new JSONArray(removedFeeds));
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/json"), requestObject.toString());
+ performRequest(url, "POST", requestBody);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new NextcloudSynchronizationServiceException(e);
+ }
+
+ return new GpodnetUploadChangesResponse(System.currentTimeMillis() / 1000, new HashMap<>());
+ }
+
+ @Override
+ public EpisodeActionChanges getEpisodeActionChanges(long timestamp) throws SyncServiceException {
+ try {
+ HttpUrl.Builder uri = makeUrl("/index.php/apps/gpoddersync/episode_action");
+ uri.addQueryParameter("since", "" + timestamp);
+ String responseString = performRequest(uri, "GET", null);
+ JSONObject json = new JSONObject(responseString);
+ return ResponseMapper.readEpisodeActionsFromJsonObject(json);
+ } catch (JSONException | MalformedURLException e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new SyncServiceException(e);
+ }
+ }
+
+ @Override
+ public UploadChangesResponse uploadEpisodeActions(List<EpisodeAction> queuedEpisodeActions)
+ throws NextcloudSynchronizationServiceException {
+ for (int i = 0; i < queuedEpisodeActions.size(); i += UPLOAD_BULK_SIZE) {
+ uploadEpisodeActionsPartial(queuedEpisodeActions,
+ i, Math.min(queuedEpisodeActions.size(), i + UPLOAD_BULK_SIZE));
+ }
+ return new NextcloudGpodderEpisodeActionPostResponse(System.currentTimeMillis() / 1000);
+ }
+
+ private void uploadEpisodeActionsPartial(List<EpisodeAction> queuedEpisodeActions, int from, int to)
+ throws NextcloudSynchronizationServiceException {
+ try {
+ final JSONArray list = new JSONArray();
+ for (int i = from; i < to; i++) {
+ EpisodeAction episodeAction = queuedEpisodeActions.get(i);
+ JSONObject obj = episodeAction.writeToJsonObject();
+ if (obj != null) {
+ list.put(obj);
+ }
+ }
+ HttpUrl.Builder url = makeUrl("/index.php/apps/gpoddersync/episode_action/create");
+ RequestBody requestBody = RequestBody.create(
+ MediaType.get("application/json"), list.toString());
+ performRequest(url, "POST", requestBody);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new NextcloudSynchronizationServiceException(e);
+ }
+ }
+
+ private String performRequest(HttpUrl.Builder url, String method, RequestBody body) throws IOException {
+ Request request = new Request.Builder()
+ .url(url.build())
+ .header("Authorization", Credentials.basic(username, password))
+ .header("Accept", "application/json")
+ .method(method, body)
+ .build();
+ Response response = httpClient.newCall(request).execute();
+ if (response.code() != 200) {
+ throw new IOException("Response code: " + response.code());
+ }
+ return response.body().string();
+ }
+
+ private HttpUrl.Builder makeUrl(String path) {
+ return new HttpUrl.Builder()
+ .scheme(baseScheme)
+ .host(baseHost)
+ .port(basePort)
+ .addPathSegments(path);
+ }
+
+ @Override
+ public void logout() {
+ }
+
+ private static class NextcloudGpodderEpisodeActionPostResponse extends UploadChangesResponse {
+ public NextcloudGpodderEpisodeActionPostResponse(long epochSecond) {
+ super(epochSecond);
+ }
+ }
+}
+
diff --git a/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java
new file mode 100644
index 000000000..d907c229e
--- /dev/null
+++ b/net/sync/gpoddernet/src/main/java/de/danoeh/antennapod/net/sync/nextcloud/NextcloudSynchronizationServiceException.java
@@ -0,0 +1,9 @@
+package de.danoeh.antennapod.net.sync.nextcloud;
+
+import de.danoeh.antennapod.net.sync.model.SyncServiceException;
+
+public class NextcloudSynchronizationServiceException extends SyncServiceException {
+ public NextcloudSynchronizationServiceException(Throwable e) {
+ super(e);
+ }
+}