Add synchronization with gPodder Nextcloud server app (#5243)
This commit is contained in:
parent
dab44b6843
commit
bc85ebc806
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
|
||||
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.util.FileNameGenerator;
|
||||
import de.danoeh.antennapod.core.util.IntentUtils;
|
||||
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;
|
||||
|
||||
import de.danoeh.antennapod.R;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,158 +51,56 @@ 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
|
||||
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
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) {
|
||||
OneTimeWorkRequest workRequest = getWorkRequest().build();
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
|
@ -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>
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue