diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2024-03-23 09:40:40 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-23 09:40:40 +0100 |
commit | f20ce1fc690788273bb779663a4f3211f47a0973 (patch) | |
tree | a1192a00bc1b5153d143fa579b8c6f977111c847 /ui/preferences/src/main/java | |
parent | 376c83d200859ef562d6e3de02602ef18de3e7de (diff) | |
download | AntennaPod-f20ce1fc690788273bb779663a4f3211f47a0973.zip |
Move first batch of preferences code to :ui:preferences (#7010)
Diffstat (limited to 'ui/preferences/src/main/java')
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); + } +} |