summaryrefslogtreecommitdiff
path: root/ui/preferences
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
parent376c83d200859ef562d6e3de02602ef18de3e7de (diff)
downloadAntennaPod-f20ce1fc690788273bb779663a4f3211f47a0973.zip
Move first batch of preferences code to :ui:preferences (#7010)
Diffstat (limited to 'ui/preferences')
-rw-r--r--ui/preferences/README.md3
-rw-r--r--ui/preferences/build.gradle47
-rw-r--r--ui/preferences/src/main/AndroidManifest.xml5
-rw-r--r--ui/preferences/src/main/assets/.gitignore4
-rw-r--r--ui/preferences/src/main/assets/LICENSE_APACHE-2.0.txt202
-rw-r--r--ui/preferences/src/main/assets/LICENSE_GLIDE.txt94
-rw-r--r--ui/preferences/src/main/assets/LICENSE_JSOUP.txt21
-rw-r--r--ui/preferences/src/main/assets/LICENSE_OKHTTP.txt13
-rw-r--r--ui/preferences/src/main/assets/LICENSE_PICTOGRAMMERS.txt20
-rw-r--r--ui/preferences/src/main/assets/LICENSE_SEARCHPREFERENCE.txt21
-rw-r--r--ui/preferences/src/main/assets/developers.csv235
-rw-r--r--ui/preferences/src/main/assets/licenses.xml123
-rw-r--r--ui/preferences/src/main/assets/special_thanks.csv6
-rw-r--r--ui/preferences/src/main/assets/translators.csv51
-rw-r--r--ui/preferences/src/main/assets/website-languages.txt7
-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
-rw-r--r--ui/preferences/src/main/res/drawable-nodpi/theme_preview_dark.pngbin0 -> 18079 bytes
-rw-r--r--ui/preferences/src/main/res/drawable-nodpi/theme_preview_light.pngbin0 -> 17760 bytes
-rw-r--r--ui/preferences/src/main/res/drawable-nodpi/theme_preview_system.pngbin0 -> 34076 bytes
-rw-r--r--ui/preferences/src/main/res/layout/about_teaser.xml9
-rw-r--r--ui/preferences/src/main/res/layout/alertdialog_sync_provider_chooser.xml24
-rw-r--r--ui/preferences/src/main/res/layout/authentication_dialog.xml61
-rw-r--r--ui/preferences/src/main/res/layout/choose_data_folder_dialog.xml12
-rw-r--r--ui/preferences/src/main/res/layout/choose_data_folder_dialog_entry.xml46
-rw-r--r--ui/preferences/src/main/res/layout/dialog_switch_preference.xml15
-rw-r--r--ui/preferences/src/main/res/layout/gpodnetauth_credentials.xml83
-rw-r--r--ui/preferences/src/main/res/layout/gpodnetauth_device.xml67
-rw-r--r--ui/preferences/src/main/res/layout/gpodnetauth_device_row.xml13
-rw-r--r--ui/preferences/src/main/res/layout/gpodnetauth_dialog.xml31
-rw-r--r--ui/preferences/src/main/res/layout/gpodnetauth_finish.xml29
-rw-r--r--ui/preferences/src/main/res/layout/gpodnetauth_host.xml38
-rw-r--r--ui/preferences/src/main/res/layout/nextcloud_auth_dialog.xml75
-rw-r--r--ui/preferences/src/main/res/layout/proxy_settings.xml90
-rw-r--r--ui/preferences/src/main/res/layout/simple_icon_list_item.xml41
-rw-r--r--ui/preferences/src/main/res/layout/theme_preference.xml123
-rw-r--r--ui/preferences/src/main/res/menu/bug_report_options.xml7
-rw-r--r--ui/preferences/src/main/res/xml/preferences.xml71
-rw-r--r--ui/preferences/src/main/res/xml/preferences_about.xml28
-rw-r--r--ui/preferences/src/main/res/xml/preferences_autodownload.xml34
-rw-r--r--ui/preferences/src/main/res/xml/preferences_downloads.xml69
-rw-r--r--ui/preferences/src/main/res/xml/preferences_import_export.xml45
-rw-r--r--ui/preferences/src/main/res/xml/preferences_notifications.xml19
-rw-r--r--ui/preferences/src/main/res/xml/preferences_playback.xml108
-rw-r--r--ui/preferences/src/main/res/xml/preferences_swipe.xml28
-rw-r--r--ui/preferences/src/main/res/xml/preferences_synchronization.xml31
-rw-r--r--ui/preferences/src/main/res/xml/preferences_user_interface.xml95
63 files changed, 3835 insertions, 0 deletions
diff --git a/ui/preferences/README.md b/ui/preferences/README.md
new file mode 100644
index 000000000..341e8e52e
--- /dev/null
+++ b/ui/preferences/README.md
@@ -0,0 +1,3 @@
+# :ui:preferences
+
+This module provides the settings screen.
diff --git a/ui/preferences/build.gradle b/ui/preferences/build.gradle
new file mode 100644
index 000000000..b91158fb9
--- /dev/null
+++ b/ui/preferences/build.gradle
@@ -0,0 +1,47 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+android {
+ namespace "de.danoeh.antennapod.ui.preferences"
+}
+
+dependencies {
+ implementation project(":core")
+ implementation project(":event")
+ implementation project(":model")
+ implementation project(":net:common")
+ implementation project(":net:sync:model")
+ implementation project(":net:sync:gpoddernet")
+ implementation project(":storage:preferences")
+ implementation project(":storage:importexport")
+ implementation project(":ui:common")
+ implementation project(":ui:glide")
+ implementation project(":ui:i18n")
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.appcompat:appcompat:$appcompatVersion"
+ implementation "com.google.android.material:material:$googleMaterialVersion"
+ implementation "androidx.preference:preference:$preferenceVersion"
+ implementation "androidx.work:work-runtime:$workManagerVersion"
+
+ implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
+ implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
+ implementation "com.github.bumptech.glide:glide:$glideVersion"
+ implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
+ implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion"
+ implementation "org.greenrobot:eventbus:$eventbusVersion"
+ implementation 'com.github.ByteHamster:SearchPreference:v2.5.0'
+}
+
+tasks.register('copyLicense', Copy) {
+ from "../../LICENSE"
+ into "src/main/assets/"
+ rename { String fileName ->
+ fileName + ".txt"
+ }
+}
+
+preBuild.dependsOn copyLicense
diff --git a/ui/preferences/src/main/AndroidManifest.xml b/ui/preferences/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..7af8da301
--- /dev/null
+++ b/ui/preferences/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:installLocation="auto">
+</manifest>
diff --git a/ui/preferences/src/main/assets/.gitignore b/ui/preferences/src/main/assets/.gitignore
new file mode 100644
index 000000000..f4de63f77
--- /dev/null
+++ b/ui/preferences/src/main/assets/.gitignore
@@ -0,0 +1,4 @@
+# this file is generated automatically
+about.html
+LICENSE.txt
+CONTRIBUTORS.txt
diff --git a/ui/preferences/src/main/assets/LICENSE_APACHE-2.0.txt b/ui/preferences/src/main/assets/LICENSE_APACHE-2.0.txt
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/ui/preferences/src/main/assets/LICENSE_APACHE-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/ui/preferences/src/main/assets/LICENSE_GLIDE.txt b/ui/preferences/src/main/assets/LICENSE_GLIDE.txt
new file mode 100644
index 000000000..f5111eeab
--- /dev/null
+++ b/ui/preferences/src/main/assets/LICENSE_GLIDE.txt
@@ -0,0 +1,94 @@
+License for everything not in third_party and not otherwise marked:
+
+Copyright 2014 Google, Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are
+permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this list of
+ conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list
+ of conditions and the following disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those of the
+authors and should not be interpreted as representing official policies, either expressed
+or implied, of Google, Inc.
+---------------------------------------------------------------------------------------------
+License for third_party/disklrucache:
+
+Copyright 2012 Jake Wharton
+Copyright 2011 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+---------------------------------------------------------------------------------------------
+License for third_party/gif_decoder:
+
+Copyright (c) 2013 Xcellent Creations, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+---------------------------------------------------------------------------------------------
+License for third_party/gif_encoder/AnimatedGifEncoder.java and
+third_party/gif_encoder/LZWEncoder.java:
+
+No copyright asserted on the source code of this class. May be used for any
+purpose, however, refer to the Unisys LZW patent for restrictions on use of
+the associated LZWEncoder class. Please forward any corrections to
+kweiner@fmsware.com.
+
+-----------------------------------------------------------------------------
+License for third_party/gif_encoder/NeuQuant.java
+
+Copyright (c) 1994 Anthony Dekker
+
+NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See
+"Kohonen neural networks for optimal colour quantization" in "Network:
+Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of
+the algorithm.
+
+Any party obtaining a copy of these files from the author, directly or
+indirectly, is granted, free of charge, a full and unrestricted irrevocable,
+world-wide, paid up, royalty-free, nonexclusive right and license to deal in
+this software and documentation files (the "Software"), including without
+limitation the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to permit persons who
+receive copies from any such party to do so, with the only requirement being
+that this copyright notice remain intact.
diff --git a/ui/preferences/src/main/assets/LICENSE_JSOUP.txt b/ui/preferences/src/main/assets/LICENSE_JSOUP.txt
new file mode 100644
index 000000000..fef8197fe
--- /dev/null
+++ b/ui/preferences/src/main/assets/LICENSE_JSOUP.txt
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2009-2022 Jonathan Hedley <https://jsoup.org/>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ui/preferences/src/main/assets/LICENSE_OKHTTP.txt b/ui/preferences/src/main/assets/LICENSE_OKHTTP.txt
new file mode 100644
index 000000000..48164b3fc
--- /dev/null
+++ b/ui/preferences/src/main/assets/LICENSE_OKHTTP.txt
@@ -0,0 +1,13 @@
+Copyright 2019 Square, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/ui/preferences/src/main/assets/LICENSE_PICTOGRAMMERS.txt b/ui/preferences/src/main/assets/LICENSE_PICTOGRAMMERS.txt
new file mode 100644
index 000000000..382f8a138
--- /dev/null
+++ b/ui/preferences/src/main/assets/LICENSE_PICTOGRAMMERS.txt
@@ -0,0 +1,20 @@
+Pictogrammers Free License
+--------------------------
+
+This icon collection is released as free, open source, and GPL friendly by
+the [Pictogrammers](http://pictogrammers.com/) icon group. You may use it
+for commercial projects, open source projects, or anything really.
+
+# Icons: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
+Some of the icons are redistributed under the Apache 2.0 license. All other
+icons are either redistributed under their respective licenses or are
+distributed under the Apache 2.0 license.
+
+# Fonts: Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
+All web and desktop fonts are distributed under the Apache 2.0 license. Web
+and desktop fonts contain some icons that are redistributed under the Apache
+2.0 license. All other icons are either redistributed under their respective
+licenses or are distributed under the Apache 2.0 license.
+
+# Code: MIT (https://opensource.org/licenses/MIT)
+The MIT license applies to all non-font and non-icon files.
diff --git a/ui/preferences/src/main/assets/LICENSE_SEARCHPREFERENCE.txt b/ui/preferences/src/main/assets/LICENSE_SEARCHPREFERENCE.txt
new file mode 100644
index 000000000..2e1603517
--- /dev/null
+++ b/ui/preferences/src/main/assets/LICENSE_SEARCHPREFERENCE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 ByteHamster
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ui/preferences/src/main/assets/developers.csv b/ui/preferences/src/main/assets/developers.csv
new file mode 100644
index 000000000..da0fa6093
--- /dev/null
+++ b/ui/preferences/src/main/assets/developers.csv
@@ -0,0 +1,235 @@
+ByteHamster;5811634;Maintainer
+danieloeh;968613;Original creator of AntennaPod (retired)
+mfietz;6860662;Maintainer (retired)
+TomHennen;5216560;Maintainer (retired)
+orionlee;250644;Contributor
+domingos86;9538859;Contributor
+TacoTheDank;32376686;Contributor
+tonytamsf;149837;Contributor
+damoasda;46045854;Contributor
+andersonvom;69922;Contributor
+ebraminio;833473;Contributor
+shortspider;5712543;Contributor
+spacecowboy;223655;Contributor
+asdoi;36813904;Contributor
+keunes;11229646;Maintainer
+patheticpat;16046;Contributor
+brad;1614;Contributor
+maxbechtold;9162198;Contributor
+Cj-Malone;10121513;Contributor
+gaul;848247;Contributor
+qkolj;6667105;Contributor
+pachecosf;46357909;Contributor
+gerardolgvr;20119298;Contributor
+johnjohndoe;144518;Contributor
+bws9000;262625;Contributor
+hannesa2;3314607;Contributor
+ahangarha;11241315;Contributor
+rharriso;570910;Contributor
+xgouchet;818706;Contributor
+ueen;5067479;Contributor
+peakvalleytech;65185819;Contributor
+vbh;56578479;Contributor
+mueller-ma;22525368;Contributor
+gitstart;1501599;Contributor
+terminalmage;328598;Contributor
+TheRealFalcon;153674;Contributor
+Slinger;75751;Contributor
+udif;809640;Contributor
+malockin;12814657;Contributor
+jas14;569991;Contributor
+jonasburian;15125616;Contributor
+dirkmueller;1029152;Contributor
+jatinkumarg;20503830;Contributor
+peschmae0;4450993;Contributor
+orelogo;15976578;Contributor
+txtd;7108931;Contributor
+ydinath;4193331;Contributor
+two-heart;12869538;Contributor
+binarytoto;75904760;Contributor
+saqura;1935380;Contributor
+drabux;10663142;Contributor
+dethstar;1239177;Contributor
+mchelen;30691;Contributor
+CedricCabessa;365097;Contributor
+matejdro;507922;Contributor
+jhenninger;197274;Contributor
+Xeitor;8825715;Contributor
+ligi;111600;Contributor
+egsavage;126165;Contributor
+cketti;218061;Contributor
+MeirAtIMDDE;4421079;Contributor
+deandreamatias;21011641;Contributor
+hzulla;1705654;Contributor
+bibz;5141956;Contributor
+HaBaLeS;730902;Contributor
+JessieVela;33134794;Contributor
+volhol;11587858;Contributor
+michaelmwhite;28901334;Contributor
+twiceyuan;2619800;Contributor
+thrillfall;15801468;Contributor
+rezanejati;16049370;Contributor
+beijingling;13600573;Contributor
+VishnuSanal;50027064;Contributor
+nereocystis;2257107;Contributor
+liesen;26872;Contributor
+dreiss;4121;Contributor
+Thom-Merrilin;76849828;Contributor
+archibishop;36948493;Contributor
+alroborol;24603829;Contributor
+avirajrsingh;69088913;Contributor
+caoilte;1500358;Contributor
+toggles;14695;Contributor
+connectety;26038710;Contributor
+matdb;48329535;Contributor
+SosoTughushi;19908097;Contributor
+Lukmannudin;32972299;Contributor
+24hours;650407;Contributor
+LatinSuD;451487;Contributor
+katrinleinweber;9948149;Contributor
+jmue;898577;Contributor
+xisberto;1914956;Contributor
+HolgerJeromin;2410353;Contributor
+HrBDev;25826502;Contributor
+CameronBanga;611354;Contributor
+mohitshah3111999;42018918;Contributor
+markamaze;17114678;Contributor
+femmdi;47671383;Contributor
+datavizard;44409076;Contributor
+wseemann;2296196;Contributor
+vinodpatildev;61724808;Contributor
+liutng;8223139;Contributor
+moralesg;14352147;Contributor
+mr-intj;6268767;Contributor
+tamizh143;50977879;Contributor
+tuxayo;2678215;Contributor
+alimemonzx;44647595;Contributor
+dev-darrell;52300159;Contributor
+jmdouglas;10855634;Contributor
+olivoto;15932680;Contributor
+PtilopsisLeucotis;54054883;Contributor
+dsmith47;14109426;Contributor
+kingargyle;177042;Contributor
+FarzanKh;14272565;Contributor
+damlayildiz;56313500;Contributor
+hannesaa2;18496079;Contributor
+myslok;2098329;Contributor
+jhunnius;9149031;Contributor
+Jared234;26669009;Contributor
+skitt;2128935;Contributor
+mamehacker;16738348;Contributor
+raghulrm;5362986;Contributor
+raghulj;57007;Contributor
+Niffler;8172446;Contributor
+JonathanZopf;47294759;Contributor
+a1291762;327162;Contributor
+ShadowIce;59123;Contributor
+victorhaggqvist;1887628;Contributor
+Toover;8531603;Contributor
+atrus6;357881;Contributor
+edent;837136;Contributor
+Carrajaula;25173082;Contributor
+vimsick;20211590;Contributor
+corecode;177979;Contributor
+Pinkolik;26690061;Contributor
+danners;116551;Contributor
+Silverwarriorin;46795935;Contributor
+shombando;42972338;Contributor
+shantanahardy;26757164;Contributor
+sethoscope;534043;Contributor
+sonnayasomnambula;7716779;Contributor
+selivan;1208989;Contributor
+SebiderSushi;23618858;Contributor
+SamWhited;512573;Contributor
+bobrippling;205673;Contributor
+ricardoborgesjr;2378440;Contributor
+rahmatrmdn;43070505;Contributor
+RafaelBod;77226971;Contributor
+panoreak;25068506;Contributor
+ortylp;470439;Contributor
+patrickdemers6;12687723;Contributor
+pganssle;1377457;Contributor
+patrickjkennedy;8617261;Contributor
+zawad2221;32180355;Contributor
+trevortabaka;1552990;Contributor
+thomasdomingos;16108830;Contributor
+tamizh138;26201258;Contributor
+struggggle;19150666;Contributor
+silansuslu;72400543;Contributor
+satis-fy;33289586;Contributor
+s3lph;5564491;Contributor
+hiasr;22374542;Contributor
+quails4Eva;16786857;Contributor
+NWuensche;15856197;Contributor
+minusf;3632883;Contributor
+loucasal;25279797;Contributor
+lightonflux;1377943;Contributor
+gregoryjtom;32783177;Contributor
+sak96;26397224;Contributor
+fossterer;4236021;Contributor
+e-t-l;40775958;Contributor
+cliambrown;17516840;Contributor
+chrk2205;44704035;Contributor
+blairun;1585872;Contributor
+axq;5077221;Contributor
+andrewc1;19559401;Contributor
+amhokies;3124968;Contributor
+agibault;15703733;Contributor
+yarons;406826;Contributor
+waylife;3348620;Contributor
+heyyviv;56256802;Contributor
+oliver;2344;Contributor
+IordanisKokk;72551397;Contributor
+harshad1;1940940;Contributor
+Geist5000;37940313;Contributor
+eerden;277513;Contributor
+eirikv;4076243;Contributor
+edwinhere;19705425;Contributor
+dhruvpatidar359;103873587;Contributor
+cdhiraj40;75211982;Contributor
+brettle;118192;Contributor
+ariedov;958646;Contributor
+danielm5;66779;Contributor
+CWftw;1498303;Contributor
+cszucko;1810383;Contributor
+britiger;2057760;Contributor
+chrissicool;232590;Contributor
+chetan882777;36985543;Contributor
+BoJacobs;25435640;Contributor
+bhaskarblur;85757758;Contributor
+arantius;84729;Contributor
+andweg;30474752;Contributor
+andrey-krutov;1488973;Contributor
+ASGusev;5954975;Contributor
+awbooze;42682253;Contributor
+alexte;7724992;Contributor
+alanorth;191754;Contributor
+adrns;13379985;Contributor
+abhinavg1997;60095795;Contributor
+nproth;48482306;Contributor
+nikhil097;35090769;Contributor
+nicoolasj;63880378;Contributor
+mounirlamouri;573590;Contributor
+MolarAmbiguity;10541979;Contributor
+Mengshi24;58278376;Contributor
+max-wittig;6639323;Contributor
+mschuetz;108637;Contributor
+Gaffen;718125;Contributor
+Mchoi8;45410115;Contributor
+mdeveloper20;2319126;Contributor
+mo;7117;Contributor
+mgborowiec;29843126;Contributor
+M-arcel;56698158;Contributor
+schwedenmut;9077622;Contributor
+mlasson;5814258;Contributor
+mjydv4548;92643506;Contributor
+MStrecke;5202211;Contributor
+LukasBrilla5;114982148;Contributor
+luiscruz;1080714;Contributor
+kvithayathil;1056073;Contributor
+Kaligule;3586246;Contributor
+CreamyCookie;3063858;Contributor
+JonOfUs;11487762;Contributor
+Foso;5015532;Contributor
+jannic;232606;Contributor
+jklippel;8657220;Contributor
diff --git a/ui/preferences/src/main/assets/licenses.xml b/ui/preferences/src/main/assets/licenses.xml
new file mode 100644
index 000000000..e6745e4f5
--- /dev/null
+++ b/ui/preferences/src/main/assets/licenses.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<libraries>
+ <library
+ name="AntennaPod"
+ author="The AntennaPod team"
+ website="https://github.com/AntennaPod/AntennaPod"
+ license="GPL-3.0"
+ licenseText="LICENSE.txt" />
+ <library
+ name="Android Jetpack"
+ author="Google"
+ website="https://developer.android.com/jetpack"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="Apache Commons"
+ author="The Apache Software Foundation"
+ website="https://commons.apache.org/"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="Balloon"
+ author="Jaewoong Eum"
+ website="https://github.com/skydoves/Balloon"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="Conscrypt"
+ author="Google"
+ website="https://github.com/google/conscrypt"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="EventBus"
+ author="greenrobot"
+ website="https://github.com/greenrobot/EventBus"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="ExoPlayer"
+ author="Google"
+ website="https://github.com/google/ExoPlayer"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="Floating Action Button Speed Dial"
+ author="Roberto Leinardi"
+ website="https://github.com/leinardi/FloatingActionButtonSpeedDial"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="fyydlin"
+ author="Martin Fietz"
+ website="https://github.com/mfietz/fyydlin"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="Glide"
+ author="bumptech"
+ website="https://github.com/bumptech/glide"
+ license="Simplified BSD"
+ licenseText="LICENSE_GLIDE.txt" />
+ <library
+ name="jsoup"
+ author="Jonathan Hedley"
+ website="https://jsoup.org/"
+ license="MIT"
+ licenseText="LICENSE_JSOUP.txt" />
+ <library
+ name="Material Components for Android"
+ author="Google"
+ website="https://github.com/material-components/material-components-android"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="Material Design Icons"
+ author="Google"
+ website="https://github.com/google/material-design-icons"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="Material Design Icons"
+ author="Templarian"
+ website="https://github.com/Templarian/MaterialDesign"
+ license="Pictogrammers Free License"
+ licenseText="LICENSE_PICTOGRAMMERS.txt" />
+ <library
+ name="OkHttp"
+ author="Square"
+ website="https://github.com/square/okhttp"
+ license="Apache 2.0"
+ licenseText="LICENSE_OKHTTP.txt" />
+ <library
+ name="RecyclerViewSwipeDecorator"
+ author="Paolo Montalto"
+ website="https://github.com/xabaras/RecyclerViewSwipeDecorator"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="RxAndroid"
+ author="ReactiveX"
+ website="https://github.com/ReactiveX/RxAndroid"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="RxJava"
+ author="ReactiveX"
+ website="https://github.com/ReactiveX/RxJava"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+ <library
+ name="SearchPreference"
+ author="ByteHamster"
+ website="https://github.com/ByteHamster/SearchPreference"
+ license="MIT"
+ licenseText="LICENSE_SEARCHPREFERENCE.txt" />
+ <library
+ name="StackBlur"
+ author="Enrique López Mañas"
+ website="https://github.com/kikoso/android-stackblur"
+ license="Apache 2.0"
+ licenseText="LICENSE_APACHE-2.0.txt" />
+</libraries>
diff --git a/ui/preferences/src/main/assets/special_thanks.csv b/ui/preferences/src/main/assets/special_thanks.csv
new file mode 100644
index 000000000..5758d48c0
--- /dev/null
+++ b/ui/preferences/src/main/assets/special_thanks.csv
@@ -0,0 +1,6 @@
+ByteHamster;Project lead;https://avatars2.githubusercontent.com/u/5811634?s=60&v=4
+Keunes;Project lead;https://avatars2.githubusercontent.com/u/11229646?s=60&v=4
+Femmdi;Translations coordinator;https://avatars2.githubusercontent.com/u/47671383?s=60&v=4
+Ryan Gorley (Freehive);2023 brand design;https://avatars2.githubusercontent.com/u/12849958?s=60&v=4
+221 Pixels;2020 brand design;https://avatars2.githubusercontent.com/u/58243143?s=60&v=4
+Anxhelo Lushka;2020 website design;https://avatars2.githubusercontent.com/u/25004151?s=60&v=4
diff --git a/ui/preferences/src/main/assets/translators.csv b/ui/preferences/src/main/assets/translators.csv
new file mode 100644
index 000000000..28d659c99
--- /dev/null
+++ b/ui/preferences/src/main/assets/translators.csv
@@ -0,0 +1,51 @@
+Arabic;abuzar3.khalid, AhmedHll, Ammar99, badarotti, fake4K, HeshamTB, keunes, mars_amn, Mehyar, mh.abdelhay, mhamade, moftasa, mohmans, MustafaAlgurabi, nabilMaghura, rex07, shubbar, vernandos
+Asturian (ast_ES);enolp, keunes
+Azerbaijani;5NOER227O, xxmn77
+Basque;a_mento, Asier_Iturralde_Sarasola, bipoza, gaztainalde, IngrownMink4, keunes, Osoitz, pospolos
+Bengali;laggybird
+Breton;Belvar, Eorn, EwenKorr, FlorentTroer, Iriep, keunes, technozuzici
+Bulgarian;keunes, ma4ko, mihainov, ppk89, solusitor, x7ype
+Catalan;and_dapo, arseru, badlop, bluegeekgh, carles.llacer, dvd1985, elcamilet, exort12, IvanAmarante, javiercoll, josep2, keunes, Kintu, lambdani, marcmetallextrem, prova, sandandmercury, selmins, xc70
+Chinese (zh_CN);135e2, aihenry2980, Biacke, brnme, claybiockiller, clong289734997, cyril3, Felix2yu, gaohongyuan, Guaidaodl, Huck0, iconteral, jhxie, jxj2zzz79pfp9bpo, JY3, keunes, kyleehee, molisiye, owen8877, RainSlide, RangerNJU, Sak94664, spice2wolf, tupunco, weylinn, whiye.hust, wongsyrone, Xrodo, yangyang, yiqiok
+Chinese (zh_TW);bobchao, BWsix, ijliao, keunes, LNDDYL, mapobi, pggdt, ymhuang0808
+Czech (cs_CZ);anotheranonymoususer, befeleme, Benda, elich, Hanzmeister, jjh, JStrange, kudlav, McLenin666, md.share, ShimonH, svetlemodry, Thomaash, viotalJiplk
+Danish;deusdenton, ERYpTION, Grooty12, JFreak, jhertel, keunes, mikini, petterbejo, SebastianKiwiDk, soelvraeven
+Dutch;daerts, e2jk, fvbommel, keunes, Kleurenregen, mijnheer, oldblue, rwv, twijg, Vistaus, y33per
+Estonian;beez276, Eraser, keunes, mahfiaz, Rots
+Finnish;Ban3, keunes, ktstmu, Kuutar, noppa, Sahtor, scop, teemue
+French;5moufl, 5NOER227O, AX.AGD, ayiniho, ChaoticMind, clombion, Cornegidouille, Daremo, e2jk, ebouaziz, keunes, klintom, Kuscoo, lacouture, LouFex, manuelleduc, Matth78, paolovador, petterbejo, PierreLaville, Poussinou, RomainTT, sterylmreep, teamon, Thoscellen
+Galician;antiparvos, pikamoku, Raichely, Sirgo
+German;23Ba1l598, 5NOER227O, _Er, axre, ByteHamster, Ceekay, ceving, dadosch, datesastick, Delvo, DerSilly, elkangaroo, enz, Erc187, F462, f_grubm, femmdi, finsterwalder, forght, hbilke, HolgerJeromin, JMAN, JoeMcFly, jokap, JoniArida, JonOfUs, kalei, keunes, klyneloud, Kostas_F, L.D.A., Macusercom, MahdiMoradi, max.wittig, mfietz, Michael_Strecke, mkida, muellerma, Nickname, petterbejo, pudeeh, Quiss42, repat, sadfgdf, Sargon_Isa, teamon, thetrash23, thiesrappen, timo.rohwedder, toaskoas, Tobiasff3200, tomte, Tonne11, ttick, tweimer, VfBFan, vrifox, Willhelm, ypid
+Hebrew (he_IL);amir.dafnyman, E1i9, eldaryiftach, mongoose4004, pinkasey, rellieberman, Yaron
+Hindi (hi_IN);Agyat009, itforchange, keunes, PrestigiousBeat6355, purple.coder, siddhusengar, singhrishi245021, sohailmangal72, techiethakkar, thelazyoxymoron
+Hu;hurrikan, keunes, lna91, lomapur, marthynw, mc.transifex, meskobalazs, MMate2007, naren93, Remboo
+Icelandic;keunes, marthjod
+Indonesian;awmpawl, dbrw, justch, keunes, levirs565, liimee, Matyeyev
+Italian (it_IT);aalex70, allin, alvami, atilluF, Bonnee, datesastick, dontknowcris, giulia.iuppa, giuseppep, Guybrush88, ilmanzo, juanjom, keunes, lu.por, m.chinni, marco_pag, mat650, micael_27, mircocau, neonsoftware, niccord, salorock, theloca95
+Japanese;atsukotominaga, ayiniho, giulia.iuppa, guyze, keunes, kirameister, KotaKato, Naofumi, sh3llc4t, tko_cactus, TranslatorG, Xrodo
+Kannada (kn_IN);chethanhs, chiraag.nataraj, deepu2, itforchange, keunes, thejeshgn, yogi
+Ko;changwoo, eshc123, keunes, libliboom, shinwookim
+Latin;nivaca
+Lithuanian;keunes, Sharper
+Macedonian;krisfremen
+Malayalam;joice, keunes, KiranS, rashivkp
+Modern Greek (1453-);AnimaRain, antonist, bufetr, Fotispel, Ioannis_D, keunes, Kostas_F, pavlosv, pcguy23
+Norwegian Bokmål (nb_NO);abstrakct, ahysing, bablecopherye, corkie, forteller, Gauteweb, halibut, heraldo, jakobkg, Jamiera, keunes, kongk, sevenmaster, tc5, timbast, TrymSan, ttick
+Persian;ahangarha, danialbehzadi, ebadi, ebraminio, F7D, hamidrezabayat76, K2latmanesh, keunes, khersi, MahdiMoradi, mmehdishafiee, sinamoghaddas, zarinisalman62
+Polish (pl_PL);ad.szczepanski, befeleme, ewm, Gadzinisko, hiro2020, Iwangelion, kamila.miodek1991, keunes, lomapur, M4SK1N, mandlus, maniexx, Medzik, Mephistofeles, millup, portonus, Rakowy_Manaska, scooby250319888, shark103, TheName, tyle
+Portuguese;emansije, jmelo461, keunes, lecalam, smarquespt, WalkerPt
+Portuguese (pt_BR);alexupits, alysonborges, amalvarenga, andersonvom, aracnus, arua, bandreghetti, brasileiro, caioau, carlo_valente, castrors, denisdl, diecavallax, fnogcps, jmelo461, keunes, lipefire, mbaltar, olivoto, philosp, ricardo_ramos, rogervezaro, RubeensVinicius, SamWilliam, tepadilha, tschertel, Xandefex, ziul123
+Romanian (ro_RO);AdrianMirica, andreh, eRadical, fuzzmz, Hiumee, keunes, mozartro, ralienpp
+Russian (ru_RU);ashed, btimofeev, Duke_Raven, flexagoon, gammja, homocomputeris, IgorPolyakov, keunes, mercutiy, nachoman, null, overmind88, PtilopsisLeucotis, s.chebotar, tepxd, un_logic, Vladryyu, whereisthetea, yako
+Sardinian;prova
+Slovak;ati3, jose1711, keunes, marulinko, McLenin666, real_name, tiborepcek
+Slovenian (sl_SI);asovic, filomena.pzn, keunes, panter23, TheFireFighter, trus2
+Spanish;3argueta3, 5NOER227O, AleksSyntek, andersonvom, andrespelaezp, arseru, Atreyu94, badlop, CaeM0R, carlos.levy, cartojo, deandreamatias, delthia, devarops, dvd1985, elcamilet, elojodepajaro, Fitoschido, frandavid100, Gomerick, hard_ware, Ioannis_D, israelem, javiercoll, keunes, kiekie, LatinSuD, leogrignafini, meanderingDot, Nickname, nivaca, rafael.osuna, technozuzici, tldevelopbit, tres.14159, vfmatzkin, victorzequeida96, wakutiteo, ziul123
+Swahili (macrolanguage);1silvester, keunes, kmtra
+Swedish (sv_SE);aiix, Ainali, bittin, bpnilsson, gustavkj, jrosdahl, keunes, LinAGKar, nilso, TwoD, victorhggqvst
+Tatar;seber
+Telugu;keunes, veeven
+Turkish;AhmedDuran, alianilkocak, alierdogan7, AliGaygisiz, androtuna, archixe, brsata, Erdy, firatsoygul, ibo90p, kabaqtepeli, keunes, overbite, Piryus, samsamsamsam, sismantolga, Slsdem, TZVS, xe1st
+Ukrainian (uk_UA);amatra, aserbovets, balaraz, hishak, keunes, koorool, older, paul_sm, sergiyr, voinovich_vyacheslav, zhenya97
+Uzbek;Usmon
+Vietnamese;abnvolk, bruhwut, keunes, ppanhh
diff --git a/ui/preferences/src/main/assets/website-languages.txt b/ui/preferences/src/main/assets/website-languages.txt
new file mode 100644
index 000000000..64361314b
--- /dev/null
+++ b/ui/preferences/src/main/assets/website-languages.txt
@@ -0,0 +1,7 @@
+en
+fr
+nl
+it
+da
+de
+es
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);
+ }
+}
diff --git a/ui/preferences/src/main/res/drawable-nodpi/theme_preview_dark.png b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_dark.png
new file mode 100644
index 000000000..b4e1e0376
--- /dev/null
+++ b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_dark.png
Binary files differ
diff --git a/ui/preferences/src/main/res/drawable-nodpi/theme_preview_light.png b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_light.png
new file mode 100644
index 000000000..39ef47b4f
--- /dev/null
+++ b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_light.png
Binary files differ
diff --git a/ui/preferences/src/main/res/drawable-nodpi/theme_preview_system.png b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_system.png
new file mode 100644
index 000000000..cc6403a98
--- /dev/null
+++ b/ui/preferences/src/main/res/drawable-nodpi/theme_preview_system.png
Binary files differ
diff --git a/ui/preferences/src/main/res/layout/about_teaser.xml b/ui/preferences/src/main/res/layout/about_teaser.xml
new file mode 100644
index 000000000..4e7f0454f
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/about_teaser.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:adjustViewBounds="true"
+ android:layout_height="wrap_content"
+ app:srcCompat="@drawable/teaser"
+ android:importantForAccessibility="no"/> \ No newline at end of file
diff --git a/ui/preferences/src/main/res/layout/alertdialog_sync_provider_chooser.xml b/ui/preferences/src/main/res/layout/alertdialog_sync_provider_chooser.xml
new file mode 100644
index 000000000..9b4d62804
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/alertdialog_sync_provider_chooser.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="16dp">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginRight="16dip"
+ android:layout_marginEnd="16dip"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:layout_gravity="center" />
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/authentication_dialog.xml b/ui/preferences/src/main/res/layout/authentication_dialog.xml
new file mode 100644
index 000000000..0d54420d4
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/authentication_dialog.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/usernameEditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/username_label"
+ android:lines="1" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/passwordEditText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/password_label"
+ android:inputType="textPassword"
+ android:lines="1" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <ImageView
+ android:id="@+id/showPasswordButton"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:src="@drawable/ic_eye"
+ android:layout_gravity="center_vertical"
+ android:padding="8dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:alpha="0.6"
+ android:textSize="20sp"
+ android:layout_marginLeft="8dp"
+ android:layout_marginStart="8dp" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/choose_data_folder_dialog.xml b/ui/preferences/src/main/res/layout/choose_data_folder_dialog.xml
new file mode 100644
index 000000000..bac14a108
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/choose_data_folder_dialog.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/recyclerView" />
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/choose_data_folder_dialog_entry.xml b/ui/preferences/src/main/res/layout/choose_data_folder_dialog_entry.xml
new file mode 100644
index 000000000..addc63f4d
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/choose_data_folder_dialog_entry.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/root"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="16dp"
+ android:background="?attr/selectableItemBackground">
+
+ <RadioButton
+ android:id="@+id/radio_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:padding="4dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:layout_toEndOf="@+id/radio_button"
+ android:layout_toRightOf="@+id/radio_button"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/path"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:text="/storage/sdcard0" />
+
+ <TextView
+ android:id="@+id/size"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:text="2 GB" />
+
+ <ProgressBar
+ android:id="@+id/used_space"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+
+</RelativeLayout>
diff --git a/ui/preferences/src/main/res/layout/dialog_switch_preference.xml b/ui/preferences/src/main/res/layout/dialog_switch_preference.xml
new file mode 100644
index 000000000..45fe21a90
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/dialog_switch_preference.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="24dp">
+
+ <com.google.android.material.materialswitch.MaterialSwitch
+ android:id="@+id/dialogSwitch"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ tools:text="Switch" />
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/gpodnetauth_credentials.xml b/ui/preferences/src/main/res/layout/gpodnetauth_credentials.xml
new file mode 100644
index 000000000..a5b8c594d
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/gpodnetauth_credentials.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:text="@string/synchronization_credentials_explanation" />
+
+ <TextView
+ android:id="@+id/createAccountWarning"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_encryption_warning"
+ android:textColor="?attr/icon_red"
+ android:textStyle="bold"
+ android:visibility="invisible" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/etxtUsername"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/username_label"
+ android:inputType="textNoSuggestions"
+ android:lines="1"
+ android:imeOptions="actionNext|flagNoFullscreen" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/etxtPassword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/password_label"
+ android:inputType="textPassword"
+ android:lines="1"
+ android:imeOptions="actionNext|flagNoFullscreen"
+ android:imeActionLabel="@string/synchronization_login_butLabel" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <ProgressBar
+ android:id="@+id/progBarLogin"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:layout_gravity="right" />
+
+ <TextView
+ android:id="@+id/credentialsError"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="?attr/icon_red"
+ android:textSize="@dimen/text_size_small"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:gravity="center"
+ android:visibility="gone"
+ tools:text=" message" />
+
+ <Button
+ android:id="@+id/butLogin"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/synchronization_login_butLabel" />
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/gpodnetauth_device.xml b/ui/preferences/src/main/res/layout/gpodnetauth_device.xml
new file mode 100644
index 000000000..6e051ecf3
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/gpodnetauth_device.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:text="@string/synchronization_selectDevice_explanation" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/deviceName"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/gpodnetauth_device_name"
+ android:lines="1"
+ android:imeOptions="actionNext|flagNoFullscreen" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <Button
+ android:id="@+id/createDeviceButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right|end"
+ android:text="@string/gpodnetauth_create_device" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/gpodnetauth_existing_devices"
+ style="@style/AntennaPod.TextView.Heading" />
+
+ <TextView
+ android:id="@+id/deviceSelectError"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textColor="?attr/icon_red"
+ android:textSize="@dimen/text_size_small"
+ android:visibility="gone"
+ tools:text="Error message"
+ tools:background="@android:color/holo_green_dark" />
+
+ <LinearLayout
+ android:id="@+id/devicesContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+
+ <ProgressBar
+ android:id="@+id/progbarCreateDevice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?attr/icon_red"
+ android:visibility="gone" />
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/gpodnetauth_device_row.xml b/ui/preferences/src/main/res/layout/gpodnetauth_device_row.xml
new file mode 100644
index 000000000..d39c00571
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/gpodnetauth_device_row.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp">
+
+ <Button
+ android:id="@+id/selectDeviceButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="?attr/materialButtonOutlinedStyle" />
+</FrameLayout> \ No newline at end of file
diff --git a/ui/preferences/src/main/res/layout/gpodnetauth_dialog.xml b/ui/preferences/src/main/res/layout/gpodnetauth_dialog.xml
new file mode 100644
index 000000000..b5814a4e5
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/gpodnetauth_dialog.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:clipToPadding="false">
+
+ <ViewFlipper
+ android:id="@+id/viewflipper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:measureAllChildren="false"
+ android:inAnimation="@anim/slide_right_in"
+ android:outAnimation="@anim/slide_left_out">
+
+ <include
+ layout="@layout/gpodnetauth_host" />
+
+ <include
+ layout="@layout/gpodnetauth_credentials" />
+
+ <include
+ layout="@layout/gpodnetauth_device" />
+
+ <include
+ layout="@layout/gpodnetauth_finish" />
+
+ </ViewFlipper>
+
+</ScrollView>
diff --git a/ui/preferences/src/main/res/layout/gpodnetauth_finish.xml b/ui/preferences/src/main/res/layout/gpodnetauth_finish.xml
new file mode 100644
index 000000000..8eced7304
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/gpodnetauth_finish.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@id/icon"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ app:srcCompat="@drawable/gpodder_icon" />
+
+ <TextView
+ android:id="@+id/txtvDescription"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/gpodnetauth_finish_descr"
+ android:textColor="?android:attr/textColorPrimary" />
+
+ <Button
+ android:id="@+id/butSyncNow"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/gpodnetauth_finish_butsyncnow"/>
+
+</LinearLayout> \ No newline at end of file
diff --git a/ui/preferences/src/main/res/layout/gpodnetauth_host.xml b/ui/preferences/src/main/res/layout/gpodnetauth_host.xml
new file mode 100644
index 000000000..7f2d16f11
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/gpodnetauth_host.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:clipToPadding="false">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:text="@string/synchronization_host_explanation" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/serverUrlText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/synchronization_host_label"
+ android:inputType="textNoSuggestions"
+ android:lines="1"
+ android:imeOptions="actionNext|flagNoFullscreen" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <Button
+ android:id="@+id/chooseHostButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/proceed_to_login_butLabel" />
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/nextcloud_auth_dialog.xml b/ui/preferences/src/main/res/layout/nextcloud_auth_dialog.xml
new file mode 100644
index 000000000..0f25a271f
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/nextcloud_auth_dialog.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="16dp"
+ android:orientation="vertical"
+ android:clipToPadding="false">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:text="@string/synchronization_host_explanation" />
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/serverUrlTextInput"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
+
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/serverUrlText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/synchronization_host_label"
+ android:inputType="textNoSuggestions"
+ android:lines="1"
+ android:imeOptions="actionNext|flagNoFullscreen" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <LinearLayout
+ android:id="@+id/loginProgressContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:orientation="horizontal"
+ android:layout_gravity="center_vertical">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:layout_marginRight="8dp" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/synchronization_nextcloud_authenticate_browser" />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/errorText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:textColor="?attr/icon_red"
+ android:layout_marginBottom="16dp" />
+
+ <Button
+ android:id="@+id/chooseHostButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/proceed_to_login_butLabel" />
+
+ </LinearLayout>
+
+</ScrollView>
diff --git a/ui/preferences/src/main/res/layout/proxy_settings.xml b/ui/preferences/src/main/res/layout/proxy_settings.xml
new file mode 100644
index 000000000..3467291eb
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/proxy_settings.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <TextView
+ android:id="@+id/txtvType"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/proxy_type_label"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <Spinner
+ android:id="@+id/spType"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:id="@+id/txtvHost"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/host_label"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <EditText
+ android:id="@+id/etHost"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:inputType="textUri"
+ android:hint="www.example.com" />
+
+ <TextView
+ android:id="@+id/txtvPort"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/port_label"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <EditText
+ android:id="@+id/etPort"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="8080"
+ android:inputType="number" />
+
+ <TextView
+ android:id="@+id/txtvUsername"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:singleLine="true"
+ android:text="@string/username_label"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <EditText
+ android:id="@+id/etUsername"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/optional_hint" />
+
+ <TextView
+ android:id="@+id/txtvPassword"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/password_label"
+ android:textColor="?android:attr/textColorSecondary" />
+
+ <EditText
+ android:id="@+id/etPassword"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/optional_hint"
+ android:inputType="textPassword" />
+
+ <TextView
+ android:id="@+id/txtvMessage"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:visibility="invisible"
+ android:gravity="center" />
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/simple_icon_list_item.xml b/ui/preferences/src/main/res/layout/simple_icon_list_item.xml
new file mode 100644
index 000000000..7ed129204
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/simple_icon_list_item.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="16dp">
+
+ <ImageView
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:id="@+id/icon"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginLeft="16dp"
+ android:layout_gravity="center_vertical"
+ android:layout_marginStart="16dp">
+
+ <TextView
+ tools:text="Title"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/title"/>
+
+ <TextView
+ tools:text="Subtitle"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="14sp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/subtitle"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/layout/theme_preference.xml b/ui/preferences/src/main/res/layout/theme_preference.xml
new file mode 100644
index 000000000..27335fbd3
--- /dev/null
+++ b/ui/preferences/src/main/res/layout/theme_preference.xml
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:background="?android:attr/colorBackground"
+ android:gravity="top"
+ android:padding="8dp">
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/themeSystemCard"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:clickable="true"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:layout_weight="1"
+ app:cardElevation="0dp"
+ app:cardCornerRadius="16dp"
+ app:contentPadding="16dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"
+ android:src="@drawable/theme_preview_system" />
+
+ <TextView
+ android:id="@+id/themeSystemRadio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:text="@string/pref_theme_title_automatic"
+ android:clickable="false" />
+
+ </LinearLayout>
+
+ </androidx.cardview.widget.CardView>
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/themeLightCard"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:clickable="true"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:layout_weight="1"
+ app:cardElevation="0dp"
+ app:cardCornerRadius="16dp"
+ app:contentPadding="16dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"
+ android:src="@drawable/theme_preview_light" />
+
+ <TextView
+ android:id="@+id/themeLightRadio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:text="@string/pref_theme_title_light"
+ android:clickable="false" />
+
+ </LinearLayout>
+
+ </androidx.cardview.widget.CardView>
+
+ <androidx.cardview.widget.CardView
+ android:id="@+id/themeDarkCard"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="4dp"
+ android:clickable="true"
+ android:foreground="?android:attr/selectableItemBackground"
+ android:layout_weight="1"
+ app:cardElevation="0dp"
+ app:cardCornerRadius="16dp"
+ app:contentPadding="16dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center">
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:adjustViewBounds="true"
+ android:src="@drawable/theme_preview_dark" />
+
+ <TextView
+ android:id="@+id/themeDarkCardRadio"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:text="@string/pref_theme_title_dark"
+ android:clickable="false" />
+
+ </LinearLayout>
+
+ </androidx.cardview.widget.CardView>
+
+</LinearLayout>
diff --git a/ui/preferences/src/main/res/menu/bug_report_options.xml b/ui/preferences/src/main/res/menu/bug_report_options.xml
new file mode 100644
index 000000000..62963210c
--- /dev/null
+++ b/ui/preferences/src/main/res/menu/bug_report_options.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:id="@+id/export_logcat"
+ android:title="@string/export_logs_menu_title" />
+
+</menu> \ No newline at end of file
diff --git a/ui/preferences/src/main/res/xml/preferences.xml b/ui/preferences/src/main/res/xml/preferences.xml
new file mode 100644
index 000000000..8f3851c09
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:search="http://schemas.android.com/apk/res-auto">
+
+ <com.bytehamster.lib.preferencesearch.SearchPreference
+ android:key="searchPreference"
+ search:textHint="@string/preference_search_hint"
+ search:textNoResults="@string/preference_search_no_results"
+ search:textClearHistory="@string/preference_search_clear_history" />
+
+ <Preference
+ android:key="prefScreenInterface"
+ android:title="@string/user_interface_label"
+ android:summary="@string/user_interface_sum"
+ android:icon="@drawable/ic_appearance" />
+
+ <Preference
+ android:key="prefScreenPlayback"
+ android:title="@string/playback_pref"
+ android:summary="@string/playback_pref_sum"
+ android:icon="@drawable/ic_play_24dp" />
+
+ <Preference
+ android:key="prefScreenDownloads"
+ android:title="@string/downloads_pref"
+ android:summary="@string/downloads_pref_sum"
+ android:icon="@drawable/ic_download" />
+
+ <Preference
+ android:key="prefScreenSynchronization"
+ android:title="@string/synchronization_pref"
+ android:summary="@string/synchronization_sum"
+ android:icon="@drawable/ic_cloud" />
+
+ <Preference
+ android:key="prefScreenImportExport"
+ android:title="@string/import_export_pref"
+ android:summary="@string/import_export_summary"
+ android:icon="@drawable/ic_storage" />
+
+ <Preference
+ android:key="notifications"
+ android:title="@string/notification_pref_fragment"
+ android:icon="@drawable/ic_notifications"/>
+
+ <PreferenceCategory
+ android:key="project"
+ android:title="@string/project_pref">
+ <Preference
+ android:key="prefDocumentation"
+ android:title="@string/documentation_support"
+ android:icon="@drawable/ic_questionmark" />
+ <Preference
+ android:key="prefViewForum"
+ android:title="@string/visit_user_forum"
+ android:icon="@drawable/ic_chat" />
+ <Preference
+ android:key="prefContribute"
+ android:title="@string/pref_contribute"
+ android:icon="@drawable/ic_contribute" />
+ <Preference
+ android:key="prefSendBugReport"
+ android:title="@string/bug_report_title"
+ android:icon="@drawable/ic_bug" />
+ <Preference
+ android:key="prefAbout"
+ android:title="@string/about_pref"
+ android:icon="@drawable/ic_info" />
+ </PreferenceCategory>
+</PreferenceScreen>
diff --git a/ui/preferences/src/main/res/xml/preferences_about.xml b/ui/preferences/src/main/res/xml/preferences_about.xml
new file mode 100644
index 000000000..1312d5466
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_about.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <Preference
+ android:layout="@layout/about_teaser"/>
+ <Preference
+ android:key="about_version"
+ android:title="@string/antennapod_version"
+ android:icon="@drawable/ic_star"
+ android:summary="1.7.2 (asd8qs)"/>
+ <Preference
+ android:key="about_contributors"
+ android:icon="@drawable/ic_settings"
+ android:summary="@string/contributors_summary"
+ android:title="@string/contributors"/>
+ <Preference
+ android:key="about_privacy_policy"
+ android:icon="@drawable/ic_questionmark"
+ android:summary="www.antennapod.org/privacy"
+ android:title="@string/privacy_policy"/>
+ <Preference
+ android:key="about_licenses"
+ android:icon="@drawable/ic_info"
+ android:summary="@string/licenses_summary"
+ android:title="@string/licenses"/>
+
+</PreferenceScreen>
diff --git a/ui/preferences/src/main/res/xml/preferences_autodownload.xml b/ui/preferences/src/main/res/xml/preferences_autodownload.xml
new file mode 100644
index 000000000..339ddc9af
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_autodownload.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
+
+ <de.danoeh.antennapod.ui.preferences.preference.MasterSwitchPreference
+ android:key="prefEnableAutoDl"
+ android:title="@string/pref_automatic_download_title"
+ search:summary="@string/pref_automatic_download_sum"
+ android:defaultValue="false"/>
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:defaultValue="25"
+ android:entries="@array/episode_cache_size_entries"
+ android:key="prefEpisodeCacheSize"
+ android:title="@string/pref_episode_cache_title"
+ android:summary="@string/pref_episode_cache_summary"
+ android:entryValues="@array/episode_cache_size_values"/>
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:defaultValue="-1"
+ android:entries="@array/episode_cleanup_entries"
+ android:key="prefEpisodeCleanup"
+ android:title="@string/pref_episode_cleanup_title"
+ android:summary="@string/pref_episode_cleanup_summary"
+ android:entryValues="@array/episode_cleanup_values"/>
+ <SwitchPreferenceCompat
+ android:key="prefEnableAutoDownloadOnBattery"
+ android:title="@string/pref_automatic_download_on_battery_title"
+ android:summary="@string/pref_automatic_download_on_battery_sum"
+ android:defaultValue="true"/>
+ <SwitchPreferenceCompat
+ android:key="prefEnableAutoDownloadWifiFilter"
+ android:title="@string/pref_autodl_wifi_filter_title"
+ android:summary="@string/pref_autodl_wifi_filter_sum"/>
+</PreferenceScreen>
diff --git a/ui/preferences/src/main/res/xml/preferences_downloads.xml b/ui/preferences/src/main/res/xml/preferences_downloads.xml
new file mode 100644
index 000000000..8a0c2efbe
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_downloads.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
+
+ <Preference
+ android:title="@string/choose_data_directory"
+ android:key="prefChooseDataDir"/>
+
+ <PreferenceCategory android:title="@string/automation">
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:entryValues="@array/feed_refresh_interval_values"
+ android:entries="@array/feed_refresh_interval_entries"
+ android:key="prefAutoUpdateIntervall"
+ android:title="@string/feed_refresh_title"
+ android:summary="@string/feed_refresh_sum"
+ android:defaultValue="12"/>
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:entryValues="@array/globalNewEpisodesActionValues"
+ android:entries="@array/globalNewEpisodesActionItems"
+ android:key="prefNewEpisodesAction"
+ android:title="@string/pref_new_episodes_action_title"
+ android:summary="@string/pref_new_episodes_action_sum"
+ android:defaultValue="1"/>
+ <Preference
+ android:summary="@string/pref_automatic_download_sum"
+ android:key="prefAutoDownloadSettings"
+ android:title="@string/pref_automatic_download_title"
+ search:ignore="true" />
+ <SwitchPreferenceCompat
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefAutoDelete"
+ android:summary="@string/pref_auto_delete_sum"
+ android:title="@string/pref_auto_delete_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefAutoDeleteLocal"
+ android:summary="@string/pref_auto_local_delete_sum"
+ android:title="@string/pref_auto_local_delete_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefFavoriteKeepsEpisode"
+ android:summary="@string/pref_favorite_keeps_episodes_sum"
+ android:title="@string/pref_favorite_keeps_episodes_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefDeleteRemovesFromQueue"
+ android:summary="@string/pref_delete_removes_from_queue_sum"
+ android:title="@string/pref_delete_removes_from_queue_title"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/download_pref_details">
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialMultiSelectListPreference
+ android:defaultValue="@array/mobile_update_default_value"
+ android:entries="@array/mobile_update_entries"
+ android:entryValues="@array/mobile_update_values"
+ android:key="prefMobileUpdateTypes"
+ android:summary="@string/pref_mobileUpdate_sum"
+ android:title="@string/pref_mobileUpdate_title"/>
+ <Preference
+ android:key="prefProxy"
+ android:summary="@string/pref_proxy_sum"
+ android:title="@string/pref_proxy_title"/>
+ </PreferenceCategory>
+</PreferenceScreen>
diff --git a/ui/preferences/src/main/res/xml/preferences_import_export.xml b/ui/preferences/src/main/res/xml/preferences_import_export.xml
new file mode 100644
index 000000000..789c8c216
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_import_export.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
+
+ <PreferenceCategory android:title="@string/database">
+ <Preference
+ android:key="prefDatabaseExport"
+ search:keywords="@string/import_export_search_keywords"
+ android:title="@string/database_export_label"
+ android:summary="@string/database_export_summary"/>
+ <SwitchPreferenceCompat
+ android:key="prefAutomaticDatabaseExport"
+ android:title="@string/automatic_database_export_label"
+ android:summary="@string/automatic_database_export_summary"
+ android:defaultValue="false" />
+ <Preference
+ android:key="prefDatabaseImport"
+ search:keywords="@string/import_export_search_keywords"
+ android:title="@string/database_import_label"
+ android:summary="@string/database_import_summary"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/opml">
+ <Preference
+ android:key="prefOpmlExport"
+ android:title="@string/opml_export_label"
+ android:summary="@string/opml_export_summary"/>
+ <Preference
+ android:key="prefOpmlImport"
+ android:title="@string/opml_import_label"
+ android:summary="@string/opml_import_summary"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/html">
+ <Preference
+ android:key="prefHtmlExport"
+ android:title="@string/html_export_label"
+ android:summary="@string/html_export_summary"/>
+ <Preference
+ android:key="prefFavoritesExport"
+ android:title="@string/favorites_export_label"
+ android:summary="@string/favorites_export_summary"/>
+ </PreferenceCategory>
+</PreferenceScreen>
diff --git a/ui/preferences/src/main/res/xml/preferences_notifications.xml b/ui/preferences/src/main/res/xml/preferences_notifications.xml
new file mode 100644
index 000000000..34d327340
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_notifications.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <PreferenceCategory
+ android:title="@string/notification_group_errors">
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefShowDownloadReport"
+ android:summary="@string/notification_channel_download_error_description"
+ android:title="@string/notification_channel_download_error" />
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:key="pref_gpodnet_notifications"
+ android:summary="@string/notification_channel_sync_error_description"
+ android:title="@string/notification_channel_sync_error" />
+ </PreferenceCategory>
+</PreferenceScreen> \ No newline at end of file
diff --git a/ui/preferences/src/main/res/xml/preferences_playback.xml b/ui/preferences/src/main/res/xml/preferences_playback.xml
new file mode 100644
index 000000000..9fff85eba
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_playback.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <PreferenceCategory android:title="@string/interruptions">
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefPauseOnHeadsetDisconnect"
+ android:summary="@string/pref_pauseOnDisconnect_sum"
+ android:title="@string/pref_pauseOnHeadsetDisconnect_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:dependency="prefPauseOnHeadsetDisconnect"
+ android:key="prefUnpauseOnHeadsetReconnect"
+ android:summary="@string/pref_unpauseOnHeadsetReconnect_sum"
+ android:title="@string/pref_unpauseOnHeadsetReconnect_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="false"
+ android:enabled="true"
+ android:dependency="prefPauseOnHeadsetDisconnect"
+ android:key="prefUnpauseOnBluetoothReconnect"
+ android:summary="@string/pref_unpauseOnBluetoothReconnect_sum"
+ android:title="@string/pref_unpauseOnBluetoothReconnect_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefPauseForFocusLoss"
+ android:summary="@string/pref_pausePlaybackForFocusLoss_sum"
+ android:title="@string/pref_pausePlaybackForFocusLoss_title"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/playback_control">
+ <Preference
+ android:key="prefPlaybackFastForwardDeltaLauncher"
+ android:summary="@string/pref_fast_forward_sum"
+ android:title="@string/pref_fast_forward"/>
+ <Preference
+ android:key="prefPlaybackRewindDeltaLauncher"
+ android:summary="@string/pref_rewind_sum"
+ android:title="@string/pref_rewind"/>
+ <Preference
+ android:key="prefPlaybackSpeedLauncher"
+ android:summary="@string/pref_playback_speed_sum"
+ android:title="@string/playback_speed"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="false"
+ android:key="prefPlaybackTimeRespectsSpeed"
+ android:summary="@string/pref_playback_time_respects_speed_sum"
+ android:title="@string/pref_playback_time_respects_speed_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="false"
+ android:key="prefStreamOverDownload"
+ android:summary="@string/pref_stream_over_download_sum"
+ android:title="@string/pref_stream_over_download_title"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/reassign_hardware_buttons">
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:defaultValue="@string/keycode_media_fast_forward"
+ android:entries="@array/button_action_options"
+ android:entryValues="@array/button_action_values"
+ android:key="prefHardwareForwardButton"
+ android:title="@string/pref_hardware_forward_button_title"
+ android:summary="@string/pref_hardware_forward_button_summary"/>
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:defaultValue="@string/keycode_media_rewind"
+ android:entries="@array/button_action_options"
+ android:entryValues="@array/button_action_values"
+ android:key="prefHardwarePreviousButton"
+ android:title="@string/pref_hardware_previous_button_title"
+ android:summary="@string/pref_hardware_previous_button_summary"/>
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/queue_label">
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefEnqueueDownloaded"
+ android:summary="@string/pref_enqueue_downloaded_summary"
+ android:title="@string/pref_enqueue_downloaded_title" />
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:defaultValue="BACK"
+ android:entries="@array/enqueue_location_options"
+ android:entryValues="@array/enqueue_location_values"
+ android:key="prefEnqueueLocation"
+ android:title="@string/pref_enqueue_location_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefFollowQueue"
+ android:summary="@string/pref_followQueue_sum"
+ android:title="@string/pref_followQueue_title"/>
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:defaultValue="30"
+ android:entries="@array/smart_mark_as_played_values"
+ android:entryValues="@array/smart_mark_as_played_values"
+ android:key="prefSmartMarkAsPlayedSecs"
+ android:summary="@string/pref_smart_mark_as_played_sum"
+ android:title="@string/pref_smart_mark_as_played_title"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefSkipKeepsEpisode"
+ android:summary="@string/pref_skip_keeps_episodes_sum"
+ android:title="@string/pref_skip_keeps_episodes_title"/>
+ </PreferenceCategory>
+</PreferenceScreen>
diff --git a/ui/preferences/src/main/res/xml/preferences_swipe.xml b/ui/preferences/src/main/res/xml/preferences_swipe.xml
new file mode 100644
index 000000000..10ac102dd
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_swipe.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <Preference
+ android:key="prefSwipeQueue"
+ android:title="@string/queue_label"/>
+
+ <Preference
+ android:key="prefSwipeInbox"
+ android:title="@string/inbox_label"/>
+
+ <Preference
+ android:key="prefSwipeEpisodes"
+ android:title="@string/episodes_label"/>
+
+ <Preference
+ android:key="prefSwipeDownloads"
+ android:title="@string/downloads_label"/>
+
+ <Preference
+ android:key="prefSwipeHistory"
+ android:title="@string/playback_history_label"/>
+
+ <Preference
+ android:key="prefSwipeFeed"
+ android:title="@string/individual_subscription"/>
+
+</PreferenceScreen>
diff --git a/ui/preferences/src/main/res/xml/preferences_synchronization.xml b/ui/preferences/src/main/res/xml/preferences_synchronization.xml
new file mode 100644
index 000000000..fbd4ccc79
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_synchronization.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <Preference
+ android:key="preference_synchronization_description"
+ android:icon="@drawable/ic_notification_sync"
+ android:summary="@string/synchronization_summary_unchoosen"/>
+
+ <Preference
+ android:key="pref_gpodnet_setlogin_information"
+ android:title="@string/pref_gpodnet_setlogin_information_title"
+ android:summary="@string/pref_gpodnet_setlogin_information_sum"
+ app:isPreferenceVisible="false"/>
+
+ <Preference
+ android:key="pref_synchronization_sync"
+ android:title="@string/synchronization_sync_changes_title"
+ android:summary="@string/synchronization_sync_summary"/>
+
+ <Preference
+ android:key="pref_synchronization_force_full_sync"
+ android:title="@string/synchronization_full_sync_title"
+ android:summary="@string/synchronization_force_sync_summary"/>
+
+ <Preference
+ android:key="pref_synchronization_logout"
+ android:title="@string/synchronization_logout"/>
+
+</PreferenceScreen>
diff --git a/ui/preferences/src/main/res/xml/preferences_user_interface.xml b/ui/preferences/src/main/res/xml/preferences_user_interface.xml
new file mode 100644
index 000000000..3730ca828
--- /dev/null
+++ b/ui/preferences/src/main/res/xml/preferences_user_interface.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:search="http://schemas.android.com/apk/com.bytehamster.lib.preferencesearch">
+
+ <PreferenceCategory android:title="@string/appearance">
+ <de.danoeh.antennapod.ui.preferences.preference.ThemePreference
+ android:key="prefTheme" />
+ <SwitchPreferenceCompat
+ android:title="@string/pref_black_theme_title"
+ android:key="prefThemeBlack"
+ android:summary="@string/pref_black_theme_message"
+ android:defaultValue="false" />
+ <SwitchPreferenceCompat
+ android:title="@string/pref_tinted_theme_title"
+ android:key="prefTintedColors"
+ android:summary="@string/pref_tinted_theme_message"
+ android:defaultValue="false" />
+ <Preference
+ android:key="prefHiddenDrawerItems"
+ android:summary="@string/pref_nav_drawer_items_sum"
+ android:title="@string/pref_nav_drawer_items_title"/>
+ <SwitchPreferenceCompat
+ android:title="@string/pref_episode_cover_title"
+ android:key="prefEpisodeCover"
+ android:summary="@string/pref_episode_cover_summary"
+ android:defaultValue="true"
+ android:enabled="true"/>
+ <SwitchPreferenceCompat
+ android:title="@string/pref_show_remain_time_title"
+ android:key="showTimeLeft"
+ android:summary="@string/pref_show_remain_time_summary"
+ android:defaultValue="false"
+ android:enabled="true"/>
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/subscriptions_label">
+ <Preference
+ android:title="@string/pref_nav_drawer_feed_order_title"
+ android:key="prefDrawerFeedOrder"
+ android:summary="@string/pref_nav_drawer_feed_order_sum"/>
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:entryValues="@array/nav_drawer_feed_counter_values"
+ android:entries="@array/nav_drawer_feed_counter_options"
+ android:title="@string/pref_nav_drawer_feed_counter_title"
+ android:key="prefDrawerFeedIndicator"
+ android:summary="@string/pref_nav_drawer_feed_counter_sum"
+ android:defaultValue="1"/>
+ <Preference
+ android:title="@string/pref_filter_feed_title"
+ android:key="prefSubscriptionsFilter"
+ android:summary="@string/pref_filter_feed_sum" />
+ <SwitchPreferenceCompat
+ android:title="@string/pref_show_subscription_title"
+ android:key="prefSubscriptionTitle"
+ android:summary="@string/pref_show_subscription_title_summary"
+ android:defaultValue="false" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/external_elements">
+ <SwitchPreferenceCompat
+ android:defaultValue="false"
+ android:enabled="true"
+ android:key="prefExpandNotify"
+ android:summary="@string/pref_expandNotify_sum"
+ android:title="@string/pref_expandNotify_title"
+ search:ignore="true"/>
+ <SwitchPreferenceCompat
+ android:defaultValue="true"
+ android:enabled="true"
+ android:key="prefPersistNotify"
+ android:summary="@string/pref_persistNotify_sum"
+ android:title="@string/pref_persistNotify_title"/>
+ <Preference
+ android:key="prefFullNotificationButtons"
+ android:summary="@string/pref_full_notification_buttons_sum"
+ android:title="@string/pref_full_notification_buttons_title"/>
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/behavior">
+ <de.danoeh.antennapod.ui.preferences.preference.MaterialListPreference
+ android:entryValues="@array/default_page_values"
+ android:entries="@array/default_page_titles"
+ android:key="prefDefaultPage"
+ android:title="@string/pref_default_page"
+ android:summary="@string/pref_default_page_sum"
+ android:defaultValue="HomeFragment"/>
+ <SwitchPreferenceCompat
+ android:key="prefBackButtonOpensDrawer"
+ android:title="@string/pref_back_button_opens_drawer"
+ android:summary="@string/pref_back_button_opens_drawer_summary"
+ android:defaultValue="false"/>
+ <Preference
+ android:key="prefSwipe"
+ android:summary="@string/swipeactions_summary"
+ android:title="@string/swipeactions_label"/>
+ </PreferenceCategory>
+</PreferenceScreen>