summaryrefslogtreecommitdiff
path: root/ui/preferences/src/main/java
diff options
context:
space:
mode:
authorByteHamster <ByteHamster@users.noreply.github.com>2024-03-23 09:40:40 +0100
committerGitHub <noreply@github.com>2024-03-23 09:40:40 +0100
commitf20ce1fc690788273bb779663a4f3211f47a0973 (patch)
treea1192a00bc1b5153d143fa579b8c6f977111c847 /ui/preferences/src/main/java
parent376c83d200859ef562d6e3de02602ef18de3e7de (diff)
downloadAntennaPod-f20ce1fc690788273bb779663a4f3211f47a0973.zip
Move first batch of preferences code to :ui:preferences (#7010)
Diffstat (limited to 'ui/preferences/src/main/java')
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MasterSwitchPreference.java42
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialListPreference.java43
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialMultiSelectListPreference.java47
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/ThemePreference.java53
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/AutoDownloadPreferencesFragment.java202
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/NotificationPreferencesFragment.java28
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/ContributorsPagerFragment.java87
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/DevelopersFragment.java56
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java125
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SimpleIconListAdapter.java59
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SpecialThanksFragment.java55
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/TranslatorsFragment.java55
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/ChooseDataFolderDialog.java36
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java139
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/AuthenticationDialog.java54
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java279
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java111
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java220
18 files changed, 1691 insertions, 0 deletions
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MasterSwitchPreference.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MasterSwitchPreference.java
new file mode 100644
index 000000000..cab3d3fc3
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MasterSwitchPreference.java
@@ -0,0 +1,42 @@
+package de.danoeh.antennapod.ui.preferences.preference;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import androidx.preference.SwitchPreferenceCompat;
+import androidx.preference.PreferenceViewHolder;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import de.danoeh.antennapod.ui.common.ThemeUtils;
+import de.danoeh.antennapod.ui.preferences.R;
+
+public class MasterSwitchPreference extends SwitchPreferenceCompat {
+
+ public MasterSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public MasterSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public MasterSwitchPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MasterSwitchPreference(Context context) {
+ super(context);
+ }
+
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ holder.itemView.setBackgroundColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorSurfaceVariant));
+ TextView title = (TextView) holder.findViewById(android.R.id.title);
+ if (title != null) {
+ title.setTypeface(title.getTypeface(), Typeface.BOLD);
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialListPreference.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialListPreference.java
new file mode 100644
index 000000000..a1264c569
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialListPreference.java
@@ -0,0 +1,43 @@
+package de.danoeh.antennapod.ui.preferences.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import androidx.preference.ListPreference;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+public class MaterialListPreference extends ListPreference {
+
+ public MaterialListPreference(Context context) {
+ super(context);
+ }
+
+ public MaterialListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onClick() {
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext());
+ builder.setTitle(getTitle());
+ builder.setIcon(getDialogIcon());
+ builder.setNegativeButton(getNegativeButtonText(), null);
+
+ CharSequence[] values = getEntryValues();
+ int selected = -1;
+ for (int i = 0; i < values.length; i++) {
+ if (values[i].toString().equals(getValue())) {
+ selected = i;
+ }
+ }
+ builder.setSingleChoiceItems(getEntries(), selected, (dialog, which) -> {
+ dialog.dismiss();
+ if (which >= 0 && getEntryValues() != null) {
+ String value = getEntryValues()[which].toString();
+ if (callChangeListener(value)) {
+ setValue(value);
+ }
+ }
+ });
+ builder.show();
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialMultiSelectListPreference.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialMultiSelectListPreference.java
new file mode 100644
index 000000000..1cf1ee170
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/MaterialMultiSelectListPreference.java
@@ -0,0 +1,47 @@
+package de.danoeh.antennapod.ui.preferences.preference;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import androidx.preference.MultiSelectListPreference;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class MaterialMultiSelectListPreference extends MultiSelectListPreference {
+
+ public MaterialMultiSelectListPreference(Context context) {
+ super(context);
+ }
+
+ public MaterialMultiSelectListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onClick() {
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext());
+ builder.setTitle(getTitle());
+ builder.setIcon(getDialogIcon());
+ builder.setNegativeButton(getNegativeButtonText(), null);
+
+ boolean[] selected = new boolean[getEntries().length];
+ CharSequence[] values = getEntryValues();
+ for (int i = 0; i < values.length; i++) {
+ selected[i] = getValues().contains(values[i].toString());
+ }
+ builder.setMultiChoiceItems(getEntries(), selected,
+ (DialogInterface dialog, int which, boolean isChecked) -> selected[which] = isChecked);
+ builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ Set<String> selectedValues = new HashSet<>();
+ for (int i = 0; i < values.length; i++) {
+ if (selected[i]) {
+ selectedValues.add(getEntryValues()[i].toString());
+ }
+ }
+ setValues(selectedValues);
+ });
+ builder.show();
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/ThemePreference.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/ThemePreference.java
new file mode 100644
index 000000000..9fdf591c5
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/preference/ThemePreference.java
@@ -0,0 +1,53 @@
+package de.danoeh.antennapod.ui.preferences.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import androidx.cardview.widget.CardView;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+import com.google.android.material.elevation.SurfaceColors;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.ui.preferences.R;
+import de.danoeh.antennapod.ui.preferences.databinding.ThemePreferenceBinding;
+
+public class ThemePreference extends Preference {
+ ThemePreferenceBinding viewBinding;
+
+ public ThemePreference(Context context) {
+ super(context);
+ setLayoutResource(R.layout.theme_preference);
+ }
+
+ public ThemePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setLayoutResource(R.layout.theme_preference);
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+ viewBinding = ThemePreferenceBinding.bind(holder.itemView);
+ updateUi();
+ }
+
+ void updateThemeCard(CardView card, UserPreferences.ThemePreference theme) {
+ float density = getContext().getResources().getDisplayMetrics().density;
+ int surfaceColor = SurfaceColors.getColorForElevation(getContext(), 1 * density);
+ int surfaceColorActive = SurfaceColors.getColorForElevation(getContext(), 32 * density);
+ UserPreferences.ThemePreference activeTheme = UserPreferences.getTheme();
+ card.setCardBackgroundColor(theme == activeTheme ? surfaceColorActive : surfaceColor);
+ card.setOnClickListener(v -> {
+ UserPreferences.setTheme(theme);
+ if (getOnPreferenceChangeListener() != null) {
+ getOnPreferenceChangeListener().onPreferenceChange(this, UserPreferences.getTheme());
+ }
+ updateUi();
+ });
+ }
+
+ void updateUi() {
+ updateThemeCard(viewBinding.themeSystemCard, UserPreferences.ThemePreference.SYSTEM);
+ updateThemeCard(viewBinding.themeLightCard, UserPreferences.ThemePreference.LIGHT);
+ updateThemeCard(viewBinding.themeDarkCard, UserPreferences.ThemePreference.DARK);
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/AutoDownloadPreferencesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/AutoDownloadPreferencesFragment.java
new file mode 100644
index 000000000..7c0c3ed4c
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/AutoDownloadPreferencesFragment.java
@@ -0,0 +1,202 @@
+package de.danoeh.antennapod.ui.preferences.screen;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.CheckBoxPreference;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.ui.preferences.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat {
+ private static final String TAG = "AutoDnldPrefFragment";
+
+ private CheckBoxPreference[] selectedNetworks;
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.preferences_autodownload);
+
+ setupAutoDownloadScreen();
+ buildAutodownloadSelectedNetworksPreference();
+ setSelectedNetworksEnabled(UserPreferences.isEnableAutodownloadWifiFilter());
+ buildEpisodeCleanupPreference();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.pref_automatic_download_title);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ checkAutodownloadItemVisibility(UserPreferences.isEnableAutodownload());
+ }
+
+ private void setupAutoDownloadScreen() {
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL).setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ if (newValue instanceof Boolean) {
+ checkAutodownloadItemVisibility((Boolean) newValue);
+ }
+ return true;
+ });
+ if (Build.VERSION.SDK_INT >= 29) {
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setVisible(false);
+ }
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER)
+ .setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ if (newValue instanceof Boolean) {
+ setSelectedNetworksEnabled((Boolean) newValue);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ );
+ }
+
+ private void checkAutodownloadItemVisibility(boolean autoDownload) {
+ findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setEnabled(autoDownload);
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL_ON_BATTERY).setEnabled(autoDownload);
+ findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER).setEnabled(autoDownload);
+ findPreference(UserPreferences.PREF_EPISODE_CLEANUP).setEnabled(autoDownload);
+ setSelectedNetworksEnabled(autoDownload && UserPreferences.isEnableAutodownloadWifiFilter());
+ }
+
+ private static String blankIfNull(String val) {
+ return val == null ? "" : val;
+ }
+
+ @SuppressLint("MissingPermission") // getConfiguredNetworks needs location permission starting with API 29
+ private void buildAutodownloadSelectedNetworksPreference() {
+ if (Build.VERSION.SDK_INT >= 29) {
+ return;
+ }
+
+ final Activity activity = getActivity();
+
+ if (selectedNetworks != null) {
+ clearAutodownloadSelectedNetworsPreference();
+ }
+ // get configured networks
+ WifiManager wifiservice = (WifiManager) activity.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks();
+
+ if (networks == null) {
+ Log.e(TAG, "Couldn't get list of configure Wi-Fi networks");
+ return;
+ }
+ Collections.sort(networks, (x, y) ->
+ blankIfNull(x.SSID).compareToIgnoreCase(blankIfNull(y.SSID)));
+ selectedNetworks = new CheckBoxPreference[networks.size()];
+ List<String> prefValues = Arrays.asList(UserPreferences
+ .getAutodownloadSelectedNetworks());
+ PreferenceScreen prefScreen = getPreferenceScreen();
+ Preference.OnPreferenceClickListener clickListener = preference -> {
+ if (preference instanceof CheckBoxPreference) {
+ String key = preference.getKey();
+ List<String> prefValuesList = new ArrayList<>(
+ Arrays.asList(UserPreferences
+ .getAutodownloadSelectedNetworks())
+ );
+ boolean newValue = ((CheckBoxPreference) preference)
+ .isChecked();
+ Log.d(TAG, "Selected network " + key + ". New state: " + newValue);
+
+ int index = prefValuesList.indexOf(key);
+ if (index >= 0 && !newValue) {
+ // remove network
+ prefValuesList.remove(index);
+ } else if (index < 0 && newValue) {
+ prefValuesList.add(key);
+ }
+
+ UserPreferences.setAutodownloadSelectedNetworks(prefValuesList.toArray(new String[0]));
+ return true;
+ } else {
+ return false;
+ }
+ };
+ // create preference for each known network. attach listener and set
+ // value
+ for (int i = 0; i < networks.size(); i++) {
+ WifiConfiguration config = networks.get(i);
+
+ CheckBoxPreference pref = new CheckBoxPreference(activity);
+ String key = Integer.toString(config.networkId);
+ pref.setTitle(config.SSID);
+ pref.setKey(key);
+ pref.setOnPreferenceClickListener(clickListener);
+ pref.setPersistent(false);
+ pref.setChecked(prefValues.contains(key));
+ selectedNetworks[i] = pref;
+ prefScreen.addPreference(pref);
+ }
+ }
+
+ private void clearAutodownloadSelectedNetworsPreference() {
+ if (selectedNetworks != null) {
+ PreferenceScreen prefScreen = getPreferenceScreen();
+
+ for (CheckBoxPreference network : selectedNetworks) {
+ if (network != null) {
+ prefScreen.removePreference(network);
+ }
+ }
+ }
+ }
+
+ private void buildEpisodeCleanupPreference() {
+ final Resources res = getActivity().getResources();
+
+ ListPreference pref = findPreference(UserPreferences.PREF_EPISODE_CLEANUP);
+ String[] values = res.getStringArray(
+ R.array.episode_cleanup_values);
+ String[] entries = new String[values.length];
+ for (int x = 0; x < values.length; x++) {
+ int v = Integer.parseInt(values[x]);
+ if (v == UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE) {
+ entries[x] = res.getString(R.string.episode_cleanup_except_favorite_removal);
+ } else if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) {
+ entries[x] = res.getString(R.string.episode_cleanup_queue_removal);
+ } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){
+ entries[x] = res.getString(R.string.episode_cleanup_never);
+ } else if (v == 0) {
+ entries[x] = res.getString(R.string.episode_cleanup_after_listening);
+ } else if (v > 0 && v < 24) {
+ entries[x] = res.getQuantityString(R.plurals.episode_cleanup_hours_after_listening, v, v);
+ } else {
+ int numDays = v / 24; // assume underlying value will be NOT fraction of days, e.g., 36 (hours)
+ entries[x] = res.getQuantityString(R.plurals.episode_cleanup_days_after_listening, numDays, numDays);
+ }
+ }
+ pref.setEntries(entries);
+ }
+
+ private void setSelectedNetworksEnabled(boolean b) {
+ if (selectedNetworks != null) {
+ for (Preference p : selectedNetworks) {
+ p.setEnabled(b);
+ }
+ }
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/NotificationPreferencesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/NotificationPreferencesFragment.java
new file mode 100644
index 000000000..221ea5da1
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/NotificationPreferencesFragment.java
@@ -0,0 +1,28 @@
+package de.danoeh.antennapod.ui.preferences.screen;
+
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.PreferenceFragmentCompat;
+import de.danoeh.antennapod.core.sync.SynchronizationSettings;
+import de.danoeh.antennapod.ui.preferences.R;
+
+public class NotificationPreferencesFragment extends PreferenceFragmentCompat {
+
+ private static final String PREF_GPODNET_NOTIFICATIONS = "pref_gpodnet_notifications";
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ addPreferencesFromResource(R.xml.preferences_notifications);
+ setUpScreen();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.notification_pref_fragment);
+ }
+
+ private void setUpScreen() {
+ findPreference(PREF_GPODNET_NOTIFICATIONS).setEnabled(SynchronizationSettings.isProviderConnected());
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/ContributorsPagerFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/ContributorsPagerFragment.java
new file mode 100644
index 000000000..912d09880
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/ContributorsPagerFragment.java
@@ -0,0 +1,87 @@
+package de.danoeh.antennapod.ui.preferences.screen.about;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+import androidx.viewpager2.widget.ViewPager2;
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayoutMediator;
+import de.danoeh.antennapod.ui.preferences.R;
+
+/**
+ * Displays the 'about->Contributors' pager screen.
+ */
+public class ContributorsPagerFragment extends Fragment {
+ private static final int POS_DEVELOPERS = 0;
+ private static final int POS_TRANSLATORS = 1;
+ private static final int POS_SPECIAL_THANKS = 2;
+ private static final int TOTAL_COUNT = 3;
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+
+ View rootView = inflater.inflate(R.layout.pager_fragment, container, false);
+ ViewPager2 viewPager = rootView.findViewById(R.id.viewpager);
+ viewPager.setAdapter(new StatisticsPagerAdapter(this));
+ // Give the TabLayout the ViewPager
+ TabLayout tabLayout = rootView.findViewById(R.id.sliding_tabs);
+ new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
+ switch (position) {
+ case POS_DEVELOPERS:
+ tab.setText(R.string.developers);
+ break;
+ case POS_TRANSLATORS:
+ tab.setText(R.string.translators);
+ break;
+ case POS_SPECIAL_THANKS:
+ tab.setText(R.string.special_thanks);
+ break;
+ default:
+ break;
+ }
+ }).attach();
+
+ rootView.findViewById(R.id.toolbar).setVisibility(View.GONE);
+
+ return rootView;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.contributors);
+ }
+
+ public static class StatisticsPagerAdapter extends FragmentStateAdapter {
+
+ StatisticsPagerAdapter(@NonNull Fragment fragment) {
+ super(fragment);
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ switch (position) {
+ case POS_TRANSLATORS:
+ return new TranslatorsFragment();
+ case POS_SPECIAL_THANKS:
+ return new SpecialThanksFragment();
+ default:
+ case POS_DEVELOPERS:
+ return new DevelopersFragment();
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return TOTAL_COUNT;
+ }
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/DevelopersFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/DevelopersFragment.java
new file mode 100644
index 000000000..de5a21bc0
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/DevelopersFragment.java
@@ -0,0 +1,56 @@
+package de.danoeh.antennapod.ui.preferences.screen.about;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.ListFragment;
+import io.reactivex.Single;
+import io.reactivex.SingleOnSubscribe;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class DevelopersFragment extends ListFragment {
+ private Disposable developersLoader;
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setDivider(null);
+ getListView().setSelector(android.R.color.transparent);
+
+ developersLoader = Single.create((SingleOnSubscribe<ArrayList<SimpleIconListAdapter.ListItem>>) emitter -> {
+ ArrayList<SimpleIconListAdapter.ListItem> developers = new ArrayList<>();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ getContext().getAssets().open("developers.csv"), "UTF-8"));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String[] info = line.split(";");
+ developers.add(new SimpleIconListAdapter.ListItem(info[0], info[2],
+ "https://avatars2.githubusercontent.com/u/" + info[1] + "?s=60&v=4"));
+ }
+ emitter.onSuccess(developers);
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ developers -> setListAdapter(new SimpleIconListAdapter<>(getContext(), developers)),
+ error -> Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show()
+ );
+
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (developersLoader != null) {
+ developersLoader.dispose();
+ }
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java
new file mode 100644
index 000000000..85badcefc
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java
@@ -0,0 +1,125 @@
+package de.danoeh.antennapod.ui.preferences.screen.about;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import androidx.fragment.app.ListFragment;
+import de.danoeh.antennapod.core.util.IntentUtils;
+import de.danoeh.antennapod.ui.preferences.R;
+import io.reactivex.Single;
+import io.reactivex.SingleOnSubscribe;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class LicensesFragment extends ListFragment {
+ private Disposable licensesLoader;
+ private final ArrayList<LicenseItem> licenses = new ArrayList<>();
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setDivider(null);
+
+ licensesLoader = Single.create((SingleOnSubscribe<ArrayList<LicenseItem>>) emitter -> {
+ licenses.clear();
+ InputStream stream = getContext().getAssets().open("licenses.xml");
+ DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ NodeList libraryList = docBuilder.parse(stream).getElementsByTagName("library");
+ for (int i = 0; i < libraryList.getLength(); i++) {
+ NamedNodeMap lib = libraryList.item(i).getAttributes();
+ licenses.add(new LicenseItem(
+ lib.getNamedItem("name").getTextContent(),
+ String.format("By %s, %s license",
+ lib.getNamedItem("author").getTextContent(),
+ lib.getNamedItem("license").getTextContent()),
+ null,
+ lib.getNamedItem("website").getTextContent(),
+ lib.getNamedItem("licenseText").getTextContent()));
+ }
+ emitter.onSuccess(licenses);
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ developers -> setListAdapter(new SimpleIconListAdapter<LicenseItem>(getContext(), developers)),
+ error -> Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show()
+ );
+
+ }
+
+ private static class LicenseItem extends SimpleIconListAdapter.ListItem {
+ final String licenseUrl;
+ final String licenseTextFile;
+
+ LicenseItem(String title, String subtitle, String imageUrl, String licenseUrl, String licenseTextFile) {
+ super(title, subtitle, imageUrl);
+ this.licenseUrl = licenseUrl;
+ this.licenseTextFile = licenseTextFile;
+ }
+ }
+
+ @Override
+ public void onListItemClick(@NonNull ListView l, @NonNull View v, int position, long id) {
+ super.onListItemClick(l, v, position, id);
+
+ LicenseItem item = licenses.get(position);
+ CharSequence[] items = {"View website", "View license"};
+ new MaterialAlertDialogBuilder(getContext())
+ .setTitle(item.title)
+ .setItems(items, (dialog, which) -> {
+ if (which == 0) {
+ IntentUtils.openInBrowser(getContext(), item.licenseUrl);
+ } else if (which == 1) {
+ showLicenseText(item.licenseTextFile);
+ }
+ }).show();
+ }
+
+ private void showLicenseText(String licenseTextFile) {
+ try {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ getContext().getAssets().open(licenseTextFile), "UTF-8"));
+ StringBuilder licenseText = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ licenseText.append(line).append("\n");
+ }
+
+ new MaterialAlertDialogBuilder(getContext())
+ .setMessage(licenseText)
+ .show();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (licensesLoader != null) {
+ licensesLoader.dispose();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.licenses);
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SimpleIconListAdapter.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SimpleIconListAdapter.java
new file mode 100644
index 000000000..a63b54e5a
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SimpleIconListAdapter.java
@@ -0,0 +1,59 @@
+package de.danoeh.antennapod.ui.preferences.screen.about;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.bumptech.glide.request.RequestOptions;
+import de.danoeh.antennapod.ui.preferences.R;
+
+import java.util.List;
+
+/**
+ * Displays a list of items that have a subtitle and an icon.
+ */
+public class SimpleIconListAdapter<T extends SimpleIconListAdapter.ListItem> extends ArrayAdapter<T> {
+ private final Context context;
+ private final List<T> listItems;
+
+ public SimpleIconListAdapter(Context context, List<T> listItems) {
+ super(context, R.layout.simple_icon_list_item, listItems);
+ this.context = context;
+ this.listItems = listItems;
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ if (view == null) {
+ view = View.inflate(context, R.layout.simple_icon_list_item, null);
+ }
+
+ ListItem item = listItems.get(position);
+ ((TextView) view.findViewById(R.id.title)).setText(item.title);
+ ((TextView) view.findViewById(R.id.subtitle)).setText(item.subtitle);
+ Glide.with(context)
+ .load(item.imageUrl)
+ .apply(new RequestOptions()
+ .diskCacheStrategy(DiskCacheStrategy.NONE)
+ .fitCenter()
+ .dontAnimate())
+ .into(((ImageView) view.findViewById(R.id.icon)));
+ return view;
+ }
+
+ public static class ListItem {
+ public final String title;
+ public final String subtitle;
+ public final String imageUrl;
+
+ public ListItem(String title, String subtitle, String imageUrl) {
+ this.title = title;
+ this.subtitle = subtitle;
+ this.imageUrl = imageUrl;
+ }
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SpecialThanksFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SpecialThanksFragment.java
new file mode 100644
index 000000000..7e9860036
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/SpecialThanksFragment.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.ui.preferences.screen.about;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.ListFragment;
+import io.reactivex.Single;
+import io.reactivex.SingleOnSubscribe;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class SpecialThanksFragment extends ListFragment {
+ private Disposable translatorsLoader;
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setDivider(null);
+ getListView().setSelector(android.R.color.transparent);
+
+ translatorsLoader = Single.create((SingleOnSubscribe<ArrayList<SimpleIconListAdapter.ListItem>>) emitter -> {
+ ArrayList<SimpleIconListAdapter.ListItem> translators = new ArrayList<>();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ getContext().getAssets().open("special_thanks.csv"), "UTF-8"));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String[] info = line.split(";");
+ translators.add(new SimpleIconListAdapter.ListItem(info[0], info[1], info[2]));
+ }
+ emitter.onSuccess(translators);
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ translators -> setListAdapter(new SimpleIconListAdapter<>(getContext(), translators)),
+ error -> Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show()
+ );
+
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (translatorsLoader != null) {
+ translatorsLoader.dispose();
+ }
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/TranslatorsFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/TranslatorsFragment.java
new file mode 100644
index 000000000..3d2079fce
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/TranslatorsFragment.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.ui.preferences.screen.about;
+
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.ListFragment;
+import io.reactivex.Single;
+import io.reactivex.SingleOnSubscribe;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class TranslatorsFragment extends ListFragment {
+ private Disposable translatorsLoader;
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setDivider(null);
+ getListView().setSelector(android.R.color.transparent);
+
+ translatorsLoader = Single.create((SingleOnSubscribe<ArrayList<SimpleIconListAdapter.ListItem>>) emitter -> {
+ ArrayList<SimpleIconListAdapter.ListItem> translators = new ArrayList<>();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ getContext().getAssets().open("translators.csv"), "UTF-8"));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String[] info = line.split(";");
+ translators.add(new SimpleIconListAdapter.ListItem(info[0], info[1], null));
+ }
+ emitter.onSuccess(translators);
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ translators -> setListAdapter(new SimpleIconListAdapter<>(getContext(), translators)),
+ error -> Toast.makeText(getContext(), error.getMessage(), Toast.LENGTH_LONG).show()
+ );
+
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (translatorsLoader != null) {
+ translatorsLoader.dispose();
+ }
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/ChooseDataFolderDialog.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/ChooseDataFolderDialog.java
new file mode 100644
index 000000000..b43866cc0
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/ChooseDataFolderDialog.java
@@ -0,0 +1,36 @@
+package de.danoeh.antennapod.ui.preferences.screen.downloads;
+
+import android.content.Context;
+
+import android.view.View;
+import androidx.appcompat.app.AlertDialog;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.ui.preferences.R;
+
+public class ChooseDataFolderDialog {
+
+ public static void showDialog(final Context context, Consumer<String> handlerFunc) {
+
+ View content = View.inflate(context, R.layout.choose_data_folder_dialog, null);
+ AlertDialog dialog = new MaterialAlertDialogBuilder(context)
+ .setView(content)
+ .setTitle(R.string.choose_data_directory)
+ .setMessage(R.string.choose_data_directory_message)
+ .setNegativeButton(R.string.cancel_label, null)
+ .create();
+ ((RecyclerView) content.findViewById(R.id.recyclerView)).setLayoutManager(new LinearLayoutManager(context));
+
+ DataFolderAdapter adapter = new DataFolderAdapter(context, path -> {
+ dialog.dismiss();
+ handlerFunc.accept(path);
+ });
+ ((RecyclerView) content.findViewById(R.id.recyclerView)).setAdapter(adapter);
+
+ if (adapter.getItemCount() != 0) {
+ dialog.show();
+ }
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java
new file mode 100644
index 000000000..bd6a75503
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java
@@ -0,0 +1,139 @@
+package de.danoeh.antennapod.ui.preferences.screen.downloads;
+
+import android.content.Context;
+import android.text.format.Formatter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.RadioButton;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.StorageUtils;
+import de.danoeh.antennapod.ui.preferences.R;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+public class DataFolderAdapter extends RecyclerView.Adapter<DataFolderAdapter.ViewHolder> {
+ private final Consumer<String> selectionHandler;
+ private final String currentPath;
+ private final List<StoragePath> entries;
+ private final String freeSpaceString;
+
+ public DataFolderAdapter(Context context, @NonNull Consumer<String> selectionHandler) {
+ this.entries = getStorageEntries(context);
+ this.currentPath = getCurrentPath();
+ this.selectionHandler = selectionHandler;
+ this.freeSpaceString = context.getString(R.string.choose_data_directory_available_space);
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ View entryView = inflater.inflate(R.layout.choose_data_folder_dialog_entry, parent, false);
+ return new ViewHolder(entryView);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ StoragePath storagePath = entries.get(position);
+ Context context = holder.root.getContext();
+ String freeSpace = Formatter.formatShortFileSize(context, storagePath.getAvailableSpace());
+ String totalSpace = Formatter.formatShortFileSize(context, storagePath.getTotalSpace());
+
+ holder.path.setText(storagePath.getShortPath());
+ holder.size.setText(String.format(freeSpaceString, freeSpace, totalSpace));
+ holder.progressBar.setProgress(storagePath.getUsagePercentage());
+ View.OnClickListener selectListener = v -> selectionHandler.accept(storagePath.getFullPath());
+ holder.root.setOnClickListener(selectListener);
+ holder.radioButton.setOnClickListener(selectListener);
+
+ if (storagePath.getFullPath().equals(currentPath)) {
+ holder.radioButton.toggle();
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return entries.size();
+ }
+
+ private String getCurrentPath() {
+ File dataFolder = UserPreferences.getDataFolder(null);
+ if (dataFolder != null) {
+ return dataFolder.getAbsolutePath();
+ }
+ return null;
+ }
+
+ private List<StoragePath> getStorageEntries(Context context) {
+ File[] mediaDirs = context.getExternalFilesDirs(null);
+ final List<StoragePath> entries = new ArrayList<>(mediaDirs.length);
+ for (File dir : mediaDirs) {
+ if (!isWritable(dir)) {
+ continue;
+ }
+ entries.add(new StoragePath(dir.getAbsolutePath()));
+ }
+ if (entries.isEmpty() && isWritable(context.getFilesDir())) {
+ entries.add(new StoragePath(context.getFilesDir().getAbsolutePath()));
+ }
+ return entries;
+ }
+
+ private boolean isWritable(File dir) {
+ return dir != null && dir.exists() && dir.canRead() && dir.canWrite();
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ private final View root;
+ private final TextView path;
+ private final TextView size;
+ private final RadioButton radioButton;
+ private final ProgressBar progressBar;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ root = itemView.findViewById(R.id.root);
+ path = itemView.findViewById(R.id.path);
+ size = itemView.findViewById(R.id.size);
+ radioButton = itemView.findViewById(R.id.radio_button);
+ progressBar = itemView.findViewById(R.id.used_space);
+ }
+ }
+
+ static class StoragePath {
+ private final String path;
+
+ StoragePath(String path) {
+ this.path = path;
+ }
+
+ String getShortPath() {
+ int prefixIndex = path.indexOf("Android");
+ return (prefixIndex > 0) ? path.substring(0, prefixIndex) : path;
+ }
+
+ String getFullPath() {
+ return this.path;
+ }
+
+ long getAvailableSpace() {
+ return StorageUtils.getFreeSpaceAvailable(path);
+ }
+
+ long getTotalSpace() {
+ return StorageUtils.getTotalSpaceAvailable(path);
+ }
+
+ int getUsagePercentage() {
+ return 100 - (int) (100 * getAvailableSpace() / (float) getTotalSpace());
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/AuthenticationDialog.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/AuthenticationDialog.java
new file mode 100644
index 000000000..a91afe78d
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/AuthenticationDialog.java
@@ -0,0 +1,54 @@
+package de.danoeh.antennapod.ui.preferences.screen.synchronization;
+
+import android.content.Context;
+import android.text.method.HideReturnsTransformationMethod;
+import android.text.method.PasswordTransformationMethod;
+import android.view.LayoutInflater;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import de.danoeh.antennapod.ui.preferences.R;
+import de.danoeh.antennapod.ui.preferences.databinding.AuthenticationDialogBinding;
+
+/**
+ * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences.
+ */
+public abstract class AuthenticationDialog extends MaterialAlertDialogBuilder {
+ boolean passwordHidden = true;
+
+ public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField,
+ String usernameInitialValue, String passwordInitialValue) {
+ super(context);
+ setTitle(titleRes);
+ AuthenticationDialogBinding viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context));
+ setView(viewBinding.getRoot());
+
+ viewBinding.usernameEditText.setEnabled(enableUsernameField);
+ if (usernameInitialValue != null) {
+ viewBinding.usernameEditText.setText(usernameInitialValue);
+ }
+ if (passwordInitialValue != null) {
+ viewBinding.passwordEditText.setText(passwordInitialValue);
+ }
+ viewBinding.showPasswordButton.setOnClickListener(v -> {
+ if (passwordHidden) {
+ viewBinding.passwordEditText.setTransformationMethod(HideReturnsTransformationMethod.getInstance());
+ viewBinding.showPasswordButton.setAlpha(1.0f);
+ } else {
+ viewBinding.passwordEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
+ viewBinding.showPasswordButton.setAlpha(0.6f);
+ }
+ passwordHidden = !passwordHidden;
+ });
+
+ setOnCancelListener(dialog -> onCancelled());
+ setNegativeButton(R.string.cancel_label, (dialog, which) -> onCancelled());
+ setPositiveButton(R.string.confirm_label, (dialog, which)
+ -> onConfirmed(viewBinding.usernameEditText.getText().toString(),
+ viewBinding.passwordEditText.getText().toString()));
+ }
+
+ protected void onCancelled() {
+
+ }
+
+ protected abstract void onConfirmed(String username, String password);
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java
new file mode 100644
index 000000000..d28355dad
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java
@@ -0,0 +1,279 @@
+package de.danoeh.antennapod.ui.preferences.screen.synchronization;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.ViewFlipper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import androidx.fragment.app.DialogFragment;
+import com.google.android.material.button.MaterialButton;
+import de.danoeh.antennapod.net.common.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.core.util.FileNameGenerator;
+import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService;
+import de.danoeh.antennapod.net.sync.gpoddernet.model.GpodnetDevice;
+import de.danoeh.antennapod.ui.preferences.R;
+import io.reactivex.Completable;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Guides the user through the authentication process.
+ */
+public class GpodderAuthenticationFragment extends DialogFragment {
+ public static final String TAG = "GpodnetAuthActivity";
+
+ private ViewFlipper viewFlipper;
+
+ private static final int STEP_DEFAULT = -1;
+ private static final int STEP_HOSTNAME = 0;
+ private static final int STEP_LOGIN = 1;
+ private static final int STEP_DEVICE = 2;
+ private static final int STEP_FINISH = 3;
+
+ private int currentStep = -1;
+
+ private GpodnetService service;
+ private volatile String username;
+ private volatile String password;
+ private volatile GpodnetDevice selectedDevice;
+ private List<GpodnetDevice> devices;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(getContext());
+ dialog.setTitle(R.string.gpodnetauth_login_butLabel);
+ dialog.setNegativeButton(R.string.cancel_label, null);
+ dialog.setCancelable(false);
+ this.setCancelable(false);
+
+ View root = View.inflate(getContext(), R.layout.gpodnetauth_dialog, null);
+ viewFlipper = root.findViewById(R.id.viewflipper);
+ advance();
+ dialog.setView(root);
+
+ return dialog.create();
+ }
+
+ private void setupHostView(View view) {
+ final Button selectHost = view.findViewById(R.id.chooseHostButton);
+ final EditText serverUrlText = view.findViewById(R.id.serverUrlText);
+ selectHost.setOnClickListener(v -> {
+ if (serverUrlText.getText().length() == 0) {
+ return;
+ }
+ SynchronizationCredentials.clear(getContext());
+ SynchronizationCredentials.setHosturl(serverUrlText.getText().toString());
+ service = new GpodnetService(AntennapodHttpClient.getHttpClient(),
+ SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceID(),
+ SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword());
+ getDialog().setTitle(SynchronizationCredentials.getHosturl());
+ advance();
+ });
+ }
+
+ private void setupLoginView(View view) {
+ final EditText username = view.findViewById(R.id.etxtUsername);
+ final EditText password = view.findViewById(R.id.etxtPassword);
+ final Button login = view.findViewById(R.id.butLogin);
+ final TextView txtvError = view.findViewById(R.id.credentialsError);
+ final ProgressBar progressBar = view.findViewById(R.id.progBarLogin);
+ final TextView createAccountWarning = view.findViewById(R.id.createAccountWarning);
+
+ if (SynchronizationCredentials.getHosturl().startsWith("http://")) {
+ createAccountWarning.setVisibility(View.VISIBLE);
+ }
+ password.setOnEditorActionListener((v, actionID, event) ->
+ actionID == EditorInfo.IME_ACTION_GO && login.performClick());
+
+ login.setOnClickListener(v -> {
+ final String usernameStr = username.getText().toString();
+ final String passwordStr = password.getText().toString();
+
+ if (usernameHasUnwantedChars(usernameStr)) {
+ txtvError.setText(R.string.gpodnetsync_username_characters_error);
+ txtvError.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ login.setEnabled(false);
+ progressBar.setVisibility(View.VISIBLE);
+ txtvError.setVisibility(View.GONE);
+ InputMethodManager inputManager = (InputMethodManager) getContext()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputManager.hideSoftInputFromWindow(login.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+
+ Completable.fromAction(() -> {
+ service.setCredentials(usernameStr, passwordStr);
+ service.login();
+ devices = service.getDevices();
+ GpodderAuthenticationFragment.this.username = usernameStr;
+ GpodderAuthenticationFragment.this.password = passwordStr;
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ login.setEnabled(true);
+ progressBar.setVisibility(View.GONE);
+ advance();
+ }, error -> {
+ login.setEnabled(true);
+ progressBar.setVisibility(View.GONE);
+ txtvError.setText(error.getCause().getMessage());
+ txtvError.setVisibility(View.VISIBLE);
+ });
+
+ });
+ }
+
+ private void setupDeviceView(View view) {
+ final EditText deviceName = view.findViewById(R.id.deviceName);
+ final LinearLayout devicesContainer = view.findViewById(R.id.devicesContainer);
+ deviceName.setText(generateDeviceName());
+
+ MaterialButton createDeviceButton = view.findViewById(R.id.createDeviceButton);
+ createDeviceButton.setOnClickListener(v -> createDevice(view));
+
+ for (GpodnetDevice device : devices) {
+ View row = View.inflate(getContext(), R.layout.gpodnetauth_device_row, null);
+ Button selectDeviceButton = row.findViewById(R.id.selectDeviceButton);
+ selectDeviceButton.setOnClickListener(v -> {
+ selectedDevice = device;
+ advance();
+ });
+ selectDeviceButton.setText(device.getCaption());
+ devicesContainer.addView(row);
+ }
+ }
+
+ private void createDevice(View view) {
+ final EditText deviceName = view.findViewById(R.id.deviceName);
+ final TextView txtvError = view.findViewById(R.id.deviceSelectError);
+ final ProgressBar progBarCreateDevice = view.findViewById(R.id.progbarCreateDevice);
+
+ String deviceNameStr = deviceName.getText().toString();
+ if (isDeviceInList(deviceNameStr)) {
+ return;
+ }
+ progBarCreateDevice.setVisibility(View.VISIBLE);
+ txtvError.setVisibility(View.GONE);
+ deviceName.setEnabled(false);
+
+ Observable.fromCallable(() -> {
+ String deviceId = generateDeviceId(deviceNameStr);
+ service.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE);
+ return new GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0);
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(device -> {
+ progBarCreateDevice.setVisibility(View.GONE);
+ selectedDevice = device;
+ advance();
+ }, error -> {
+ deviceName.setEnabled(true);
+ progBarCreateDevice.setVisibility(View.GONE);
+ txtvError.setText(error.getMessage());
+ txtvError.setVisibility(View.VISIBLE);
+ });
+ }
+
+ private String generateDeviceName() {
+ String baseName = getString(R.string.gpodnetauth_device_name_default, Build.MODEL);
+ String name = baseName;
+ int num = 1;
+ while (isDeviceInList(name)) {
+ name = baseName + " (" + num + ")";
+ num++;
+ }
+ return name;
+ }
+
+ private String generateDeviceId(String name) {
+ // devices names must be of a certain form:
+ // https://gpoddernet.readthedocs.org/en/latest/api/reference/general.html#devices
+ return FileNameGenerator.generateFileName(name).replaceAll("\\W", "_").toLowerCase(Locale.US);
+ }
+
+ private boolean isDeviceInList(String name) {
+ if (devices == null) {
+ return false;
+ }
+ String id = generateDeviceId(name);
+ for (GpodnetDevice device : devices) {
+ if (device.getId().equals(id) || device.getCaption().equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void setupFinishView(View view) {
+ final Button sync = view.findViewById(R.id.butSyncNow);
+
+ sync.setOnClickListener(v -> {
+ dismiss();
+ SyncService.sync(getContext());
+ });
+ }
+
+ private void advance() {
+ if (currentStep < STEP_FINISH) {
+ View view = viewFlipper.getChildAt(currentStep + 1);
+ if (currentStep == STEP_DEFAULT) {
+ setupHostView(view);
+ } else if (currentStep == STEP_HOSTNAME) {
+ setupLoginView(view);
+ } else if (currentStep == STEP_LOGIN) {
+ if (username == null || password == null) {
+ throw new IllegalStateException("Username and password must not be null here");
+ } else {
+ setupDeviceView(view);
+ }
+ } else if (currentStep == STEP_DEVICE) {
+ if (selectedDevice == null) {
+ throw new IllegalStateException("Device must not be null here");
+ } else {
+ SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.GPODDER_NET);
+ SynchronizationCredentials.setUsername(username);
+ SynchronizationCredentials.setPassword(password);
+ SynchronizationCredentials.setDeviceID(selectedDevice.getId());
+ setupFinishView(view);
+ }
+ }
+ if (currentStep != STEP_DEFAULT) {
+ viewFlipper.showNext();
+ }
+ currentStep++;
+ } else {
+ dismiss();
+ }
+ }
+
+ private boolean usernameHasUnwantedChars(String username) {
+ Pattern special = Pattern.compile("[!@#$%&*()+=|<>?{}\\[\\]~]");
+ Matcher containsUnwantedChars = special.matcher(username);
+ return containsUnwantedChars.find();
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java
new file mode 100644
index 000000000..b73ee2453
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java
@@ -0,0 +1,111 @@
+package de.danoeh.antennapod.ui.preferences.screen.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 com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import androidx.fragment.app.DialogFragment;
+import de.danoeh.antennapod.net.common.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.net.sync.nextcloud.NextcloudLoginFlow;
+import de.danoeh.antennapod.ui.preferences.R;
+import de.danoeh.antennapod.ui.preferences.databinding.NextcloudAuthDialogBinding;
+
+/**
+ * Guides the user through the authentication process.
+ */
+public class NextcloudAuthenticationFragment extends DialogFragment
+ implements NextcloudLoginFlow.AuthenticationCallback {
+ public static final String TAG = "NextcloudAuthenticationFragment";
+ private static final String EXTRA_LOGIN_FLOW = "LoginFlow";
+ private NextcloudAuthDialogBinding viewBinding;
+ private NextcloudLoginFlow nextcloudLoginFlow;
+ private boolean shouldDismiss = false;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ MaterialAlertDialogBuilder dialog = new MaterialAlertDialogBuilder(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.chooseHostButton.setOnClickListener(v -> {
+ nextcloudLoginFlow = new NextcloudLoginFlow(AntennapodHttpClient.getHttpClient(),
+ viewBinding.serverUrlText.getText().toString(), getContext(), this);
+ startLoginFlow();
+ });
+ if (savedInstanceState != null && savedInstanceState.getStringArrayList(EXTRA_LOGIN_FLOW) != null) {
+ nextcloudLoginFlow = NextcloudLoginFlow.fromInstanceState(AntennapodHttpClient.getHttpClient(),
+ getContext(), this, savedInstanceState.getStringArrayList(EXTRA_LOGIN_FLOW));
+ startLoginFlow();
+ }
+ return dialog.create();
+ }
+
+ private void startLoginFlow() {
+ viewBinding.errorText.setVisibility(View.GONE);
+ viewBinding.chooseHostButton.setVisibility(View.GONE);
+ viewBinding.loginProgressContainer.setVisibility(View.VISIBLE);
+ viewBinding.serverUrlText.setEnabled(false);
+ nextcloudLoginFlow.start();
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (nextcloudLoginFlow != null) {
+ outState.putStringArrayList(EXTRA_LOGIN_FLOW, nextcloudLoginFlow.saveInstanceState());
+ }
+ }
+
+ @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 (isResumed()) {
+ 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.chooseHostButton.setVisibility(View.VISIBLE);
+ viewBinding.serverUrlText.setEnabled(true);
+ }
+}
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java
new file mode 100644
index 000000000..3c6461272
--- /dev/null
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java
@@ -0,0 +1,220 @@
+package de.danoeh.antennapod.ui.preferences.screen.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.AppCompatActivity;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import androidx.core.text.HtmlCompat;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import de.danoeh.antennapod.ui.preferences.R;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import de.danoeh.antennapod.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;
+
+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();
+ ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.synchronization_pref);
+ updateScreen();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EventBus.getDefault().unregister(this);
+ ((AppCompatActivity) 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 {
+ ((AppCompatActivity) 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);
+ ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(null);
+ }
+ }
+
+ private void chooseProviderAndLogin() {
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(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();
+ });
+
+ builder.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));
+ ((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle(status);
+ }
+}