summaryrefslogtreecommitdiff
path: root/ui
diff options
context:
space:
mode:
Diffstat (limited to 'ui')
-rw-r--r--ui/chapters/README.md3
-rw-r--r--ui/chapters/build.gradle21
-rw-r--r--ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterMerger.java70
-rw-r--r--ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterUtils.java228
-rw-r--r--ui/common/build.gradle2
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/ConfirmationDialog.java54
-rw-r--r--ui/common/src/main/java/de/danoeh/antennapod/ui/common/IntentUtils.java67
-rw-r--r--ui/common/src/main/res/drawable/bg_blue_gradient.xml10
-rw-r--r--ui/common/src/main/res/drawable/bg_circle.xml6
-rw-r--r--ui/common/src/main/res/drawable/bg_drawer_item.xml20
-rw-r--r--ui/common/src/main/res/drawable/bg_gradient.xml10
-rw-r--r--ui/common/src/main/res/drawable/bg_pill.xml7
-rw-r--r--ui/common/src/main/res/drawable/bg_rounded_corners.xml6
-rw-r--r--ui/common/website-languages.txt (renamed from ui/preferences/src/main/assets/website-languages.txt)0
-rw-r--r--ui/preferences/build.gradle1
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/AboutFragment.java2
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/LicensesFragment.java2
-rw-r--r--ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/downloads/DataFolderAdapter.java12
-rw-r--r--ui/preferences/src/main/res/values/arrays.xml277
-rw-r--r--ui/preferences/src/main/res/values/keycodes.xml9
-rw-r--r--ui/statistics/build.gradle1
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java2
-rw-r--r--ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsFragment.java2
23 files changed, 803 insertions, 9 deletions
diff --git a/ui/chapters/README.md b/ui/chapters/README.md
new file mode 100644
index 000000000..68136b436
--- /dev/null
+++ b/ui/chapters/README.md
@@ -0,0 +1,3 @@
+# :ui:chapters
+
+This module provides chapter loading and merging for display, but not the actual UI to display them.
diff --git a/ui/chapters/build.gradle b/ui/chapters/build.gradle
new file mode 100644
index 000000000..a3cb1b677
--- /dev/null
+++ b/ui/chapters/build.gradle
@@ -0,0 +1,21 @@
+plugins {
+ id("com.android.library")
+}
+apply from: "../../common.gradle"
+apply from: "../../playFlavor.gradle"
+
+android {
+ namespace "de.danoeh.antennapod.ui.chapters"
+}
+
+dependencies {
+ implementation project(':model')
+ implementation project(':net:common')
+ implementation project(':parser:media')
+ implementation project(':parser:feed')
+ implementation project(':storage:database')
+
+ annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "commons-io:commons-io:$commonsioVersion"
+ implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
+}
diff --git a/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterMerger.java b/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterMerger.java
new file mode 100644
index 000000000..7ec11b566
--- /dev/null
+++ b/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterMerger.java
@@ -0,0 +1,70 @@
+package de.danoeh.antennapod.ui.chapters;
+
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import de.danoeh.antennapod.model.feed.Chapter;
+
+import java.util.List;
+
+public class ChapterMerger {
+ private static final String TAG = "ChapterMerger";
+
+ private ChapterMerger() {
+
+ }
+
+ /**
+ * This method might modify the input data.
+ */
+ @Nullable
+ public static List<Chapter> merge(@Nullable List<Chapter> chapters1, @Nullable List<Chapter> chapters2) {
+ Log.d(TAG, "Merging chapters");
+ if (chapters1 == null) {
+ return chapters2;
+ } else if (chapters2 == null) {
+ return chapters1;
+ } else if (chapters2.size() > chapters1.size()) {
+ return chapters2;
+ } else if (chapters2.size() < chapters1.size()) {
+ return chapters1;
+ } else {
+ // Merge chapter lists of same length. Store in chapters2 array.
+ // In case the lists can not be merged, return chapters1 array.
+ for (int i = 0; i < chapters2.size(); i++) {
+ Chapter chapterTarget = chapters2.get(i);
+ Chapter chapterOther = chapters1.get(i);
+
+ if (Math.abs(chapterTarget.getStart() - chapterOther.getStart()) > 1000) {
+ Log.e(TAG, "Chapter lists are too different. Cancelling merge.");
+ return score(chapters1) > score(chapters2) ? chapters1 : chapters2;
+ }
+
+ if (TextUtils.isEmpty(chapterTarget.getImageUrl())) {
+ chapterTarget.setImageUrl(chapterOther.getImageUrl());
+ }
+ if (TextUtils.isEmpty(chapterTarget.getLink())) {
+ chapterTarget.setLink(chapterOther.getLink());
+ }
+ if (TextUtils.isEmpty(chapterTarget.getTitle())) {
+ chapterTarget.setTitle(chapterOther.getTitle());
+ }
+ }
+ return chapters2;
+ }
+ }
+
+ /**
+ * Tries to give a score that can determine which list of chapters a user might want to see.
+ */
+ private static int score(List<Chapter> chapters) {
+ int score = 0;
+ for (Chapter chapter : chapters) {
+ score = score
+ + (TextUtils.isEmpty(chapter.getTitle()) ? 0 : 1)
+ + (TextUtils.isEmpty(chapter.getLink()) ? 0 : 1)
+ + (TextUtils.isEmpty(chapter.getImageUrl()) ? 0 : 1);
+ }
+ return score;
+ }
+}
diff --git a/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterUtils.java b/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterUtils.java
new file mode 100644
index 000000000..5554890ed
--- /dev/null
+++ b/ui/chapters/src/main/java/de/danoeh/antennapod/ui/chapters/ChapterUtils.java
@@ -0,0 +1,228 @@
+package de.danoeh.antennapod.ui.chapters;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import de.danoeh.antennapod.model.feed.Chapter;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.net.common.AntennapodHttpClient;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.parser.feed.PodcastIndexChapterParser;
+import de.danoeh.antennapod.parser.media.id3.ChapterReader;
+import de.danoeh.antennapod.parser.media.id3.ID3ReaderException;
+import de.danoeh.antennapod.model.playback.Playable;
+import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentChapterReader;
+import de.danoeh.antennapod.parser.media.vorbis.VorbisCommentReaderException;
+import okhttp3.CacheControl;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.commons.io.input.CountingInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Utility class for getting chapter data from media files.
+ */
+public class ChapterUtils {
+
+ private static final String TAG = "ChapterUtils";
+
+ private ChapterUtils() {
+ }
+
+ public static void loadChapters(Playable playable, Context context, boolean forceRefresh) {
+ if (playable.getChapters() != null && !forceRefresh) {
+ // Already loaded
+ return;
+ }
+
+ try {
+ List<Chapter> chaptersFromDatabase = null;
+ List<Chapter> chaptersFromPodcastIndex = null;
+ if (playable instanceof FeedMedia) {
+ FeedMedia feedMedia = (FeedMedia) playable;
+ if (feedMedia.getItem() == null) {
+ feedMedia.setItem(DBReader.getFeedItem(feedMedia.getItemId()));
+ }
+ if (feedMedia.getItem().hasChapters()) {
+ chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(feedMedia.getItem());
+ }
+
+ if (!TextUtils.isEmpty(feedMedia.getItem().getPodcastIndexChapterUrl())) {
+ chaptersFromPodcastIndex = ChapterUtils.loadChaptersFromUrl(
+ feedMedia.getItem().getPodcastIndexChapterUrl(), forceRefresh);
+ }
+
+ }
+
+ List<Chapter> chaptersFromMediaFile = ChapterUtils.loadChaptersFromMediaFile(playable, context);
+ List<Chapter> chaptersMergePhase1 = ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile);
+ List<Chapter> chapters = ChapterMerger.merge(chaptersMergePhase1, chaptersFromPodcastIndex);
+ if (chapters == null) {
+ // Do not try loading again. There are no chapters or parsing failed.
+ playable.setChapters(Collections.emptyList());
+ } else {
+ playable.setChapters(chapters);
+ }
+ } catch (InterruptedIOException e) {
+ Log.d(TAG, "Chapter loading interrupted");
+ playable.setChapters(null); // Allow later retry
+ }
+ }
+
+ public static List<Chapter> loadChaptersFromMediaFile(Playable playable, Context context)
+ throws InterruptedIOException {
+ try (CountingInputStream in = openStream(playable, context)) {
+ List<Chapter> chapters = readId3ChaptersFrom(in);
+ if (!chapters.isEmpty()) {
+ Log.i(TAG, "Chapters loaded");
+ return chapters;
+ }
+ } catch (InterruptedIOException e) {
+ throw e;
+ } catch (IOException | ID3ReaderException e) {
+ Log.e(TAG, "Unable to load ID3 chapters: " + e.getMessage());
+ }
+
+ try (CountingInputStream in = openStream(playable, context)) {
+ List<Chapter> chapters = readOggChaptersFromInputStream(in);
+ if (!chapters.isEmpty()) {
+ Log.i(TAG, "Chapters loaded");
+ return chapters;
+ }
+ } catch (InterruptedIOException e) {
+ throw e;
+ } catch (IOException | VorbisCommentReaderException e) {
+ Log.e(TAG, "Unable to load vorbis chapters: " + e.getMessage());
+ }
+ return null;
+ }
+
+ private static CountingInputStream openStream(Playable playable, Context context) throws IOException {
+ if (playable.localFileAvailable()) {
+ if (playable.getLocalFileUrl() == null) {
+ throw new IOException("No local url");
+ }
+ File source = new File(playable.getLocalFileUrl());
+ if (!source.exists()) {
+ throw new IOException("Local file does not exist");
+ }
+ return new CountingInputStream(new BufferedInputStream(new FileInputStream(source)));
+ } else if (playable.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) {
+ Uri uri = Uri.parse(playable.getStreamUrl());
+ return new CountingInputStream(new BufferedInputStream(context.getContentResolver().openInputStream(uri)));
+ } else {
+ Request request = new Request.Builder().url(playable.getStreamUrl()).build();
+ Response response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
+ if (response.body() == null) {
+ throw new IOException("Body is null");
+ }
+ return new CountingInputStream(new BufferedInputStream(response.body().byteStream()));
+ }
+ }
+
+ public static List<Chapter> loadChaptersFromUrl(String url, boolean forceRefresh) throws InterruptedIOException {
+ if (forceRefresh) {
+ return loadChaptersFromUrl(url, CacheControl.FORCE_NETWORK);
+ }
+ List<Chapter> cachedChapters = loadChaptersFromUrl(url, CacheControl.FORCE_CACHE);
+ if (cachedChapters == null || cachedChapters.size() <= 1) {
+ // Some publishers use one dummy chapter before actual chapters are available
+ return loadChaptersFromUrl(url, CacheControl.FORCE_NETWORK);
+ }
+ return cachedChapters;
+ }
+
+ private static List<Chapter> loadChaptersFromUrl(String url, CacheControl cacheControl)
+ throws InterruptedIOException {
+ Response response = null;
+ try {
+ Request request = new Request.Builder().url(url).cacheControl(cacheControl).build();
+ response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
+ if (response.isSuccessful() && response.body() != null) {
+ return PodcastIndexChapterParser.parse(response.body().string());
+ }
+ } catch (InterruptedIOException e) {
+ throw e;
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (response != null) {
+ response.close();
+ }
+ }
+ return null;
+ }
+
+ @NonNull
+ private static List<Chapter> readId3ChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
+ ChapterReader reader = new ChapterReader(in);
+ reader.readInputStream();
+ List<Chapter> chapters = reader.getChapters();
+ Collections.sort(chapters, new ChapterStartTimeComparator());
+ enumerateEmptyChapterTitles(chapters);
+ if (!chaptersValid(chapters)) {
+ Log.e(TAG, "Chapter data was invalid");
+ return Collections.emptyList();
+ }
+ return chapters;
+ }
+
+ @NonNull
+ private static List<Chapter> readOggChaptersFromInputStream(InputStream input) throws VorbisCommentReaderException {
+ VorbisCommentChapterReader reader = new VorbisCommentChapterReader(new BufferedInputStream(input));
+ reader.readInputStream();
+ List<Chapter> chapters = reader.getChapters();
+ if (chapters == null) {
+ return Collections.emptyList();
+ }
+ Collections.sort(chapters, new ChapterStartTimeComparator());
+ enumerateEmptyChapterTitles(chapters);
+ if (chaptersValid(chapters)) {
+ return chapters;
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Makes sure that chapter does a title and an item attribute.
+ */
+ private static void enumerateEmptyChapterTitles(List<Chapter> chapters) {
+ for (int i = 0; i < chapters.size(); i++) {
+ Chapter c = chapters.get(i);
+ if (c.getTitle() == null) {
+ c.setTitle(Integer.toString(i));
+ }
+ }
+ }
+
+ private static boolean chaptersValid(List<Chapter> chapters) {
+ if (chapters.isEmpty()) {
+ return false;
+ }
+ for (Chapter c : chapters) {
+ if (c.getStart() < 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static class ChapterStartTimeComparator implements Comparator<Chapter> {
+ @Override
+ public int compare(Chapter lhs, Chapter rhs) {
+ return Long.compare(lhs.getStart(), rhs.getStart());
+ }
+ }
+}
diff --git a/ui/common/build.gradle b/ui/common/build.gradle
index 1325761d3..f2916593a 100644
--- a/ui/common/build.gradle
+++ b/ui/common/build.gradle
@@ -16,6 +16,8 @@ dependencies {
implementation "androidx.viewpager2:viewpager2:$viewPager2Version"
implementation "com.google.android.material:material:$googleMaterialVersion"
implementation "androidx.core:core-splashscreen:1.0.0"
+ implementation "org.apache.commons:commons-lang3:$commonslangVersion"
+ implementation "commons-io:commons-io:$commonsioVersion"
testImplementation "junit:junit:$junitVersion"
}
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ConfirmationDialog.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ConfirmationDialog.java
new file mode 100644
index 000000000..8acedc7d3
--- /dev/null
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/ConfirmationDialog.java
@@ -0,0 +1,54 @@
+package de.danoeh.antennapod.ui.common;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import androidx.appcompat.app.AlertDialog;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import android.util.Log;
+
+/**
+ * Creates an AlertDialog which asks the user to confirm something. Other
+ * classes can handle events like confirmation or cancellation.
+ */
+public abstract class ConfirmationDialog {
+
+ private static final String TAG = ConfirmationDialog.class.getSimpleName();
+
+ private final Context context;
+ private final int titleId;
+ private final String message;
+
+ private int positiveText;
+
+ public ConfirmationDialog(Context context, int titleId, int messageId) {
+ this(context, titleId, context.getString(messageId));
+ }
+
+ public ConfirmationDialog(Context context, int titleId, String message) {
+ this.context = context;
+ this.titleId = titleId;
+ this.message = message;
+ }
+
+ private void onCancelButtonPressed(DialogInterface dialog) {
+ Log.d(TAG, "Dialog was cancelled");
+ dialog.dismiss();
+ }
+
+ public void setPositiveText(int id) {
+ this.positiveText = id;
+ }
+
+ public abstract void onConfirmButtonPressed(DialogInterface dialog);
+
+ public final AlertDialog createNewDialog() {
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
+ builder.setTitle(titleId);
+ builder.setMessage(message);
+ builder.setPositiveButton(positiveText != 0 ? positiveText : R.string.confirm_label,
+ (dialog, which) -> onConfirmButtonPressed(dialog));
+ builder.setNegativeButton(R.string.cancel_label, (dialog, which) -> onCancelButtonPressed(dialog));
+ builder.setOnCancelListener(ConfirmationDialog.this::onCancelButtonPressed);
+ return builder.create();
+ }
+}
diff --git a/ui/common/src/main/java/de/danoeh/antennapod/ui/common/IntentUtils.java b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/IntentUtils.java
new file mode 100644
index 000000000..26c703e7b
--- /dev/null
+++ b/ui/common/src/main/java/de/danoeh/antennapod/ui/common/IntentUtils.java
@@ -0,0 +1,67 @@
+package de.danoeh.antennapod.ui.common;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.Toast;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Locale;
+
+public class IntentUtils {
+ private static final String TAG = "IntentUtils";
+
+ private IntentUtils(){}
+
+ /*
+ * Checks if there is at least one exported activity that can be performed for the intent
+ */
+ public static boolean isCallable(final Context context, final Intent intent) {
+ List<ResolveInfo> list = context.getPackageManager().queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ for(ResolveInfo info : list) {
+ if(info.activityInfo.exported) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static void sendLocalBroadcast(Context context, String action) {
+ context.sendBroadcast(new Intent(action).setPackage(context.getPackageName()));
+ }
+
+ public static void openInBrowser(Context context, String url) {
+ try {
+ Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(myIntent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(context, R.string.pref_no_browser_found, Toast.LENGTH_LONG).show();
+ Log.e(TAG, Log.getStackTraceString(e));
+ }
+ }
+
+ public static String getLocalizedWebsiteLink(Context context) {
+ try (InputStream is = context.getAssets().open("website-languages.txt")) {
+ String[] languages = IOUtils.toString(is, StandardCharsets.UTF_8.name()).split("\n");
+ String deviceLanguage = Locale.getDefault().getLanguage();
+ if (ArrayUtils.contains(languages, deviceLanguage) && !"en".equals(deviceLanguage)) {
+ return "https://antennapod.org/" + deviceLanguage;
+ } else {
+ return "https://antennapod.org";
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/ui/common/src/main/res/drawable/bg_blue_gradient.xml b/ui/common/src/main/res/drawable/bg_blue_gradient.xml
new file mode 100644
index 000000000..8ae045b6d
--- /dev/null
+++ b/ui/common/src/main/res/drawable/bg_blue_gradient.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+ <gradient
+ android:angle="90"
+ android:endColor="@color/gradient_025"
+ android:startColor="@color/gradient_075"
+ android:type="linear" />
+ <corners
+ android:radius="0dp"/>
+</shape>
diff --git a/ui/common/src/main/res/drawable/bg_circle.xml b/ui/common/src/main/res/drawable/bg_circle.xml
new file mode 100644
index 000000000..0957db5e4
--- /dev/null
+++ b/ui/common/src/main/res/drawable/bg_circle.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="?attr/colorPrimary" />
+ <corners android:radius="30dp" />
+ <size android:width="60dp" android:height="60dp"/>
+</shape>
diff --git a/ui/common/src/main/res/drawable/bg_drawer_item.xml b/ui/common/src/main/res/drawable/bg_drawer_item.xml
new file mode 100644
index 000000000..40727bf50
--- /dev/null
+++ b/ui/common/src/main/res/drawable/bg_drawer_item.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?attr/colorSurfaceVariant">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/black"/>
+ <corners android:radius="32dp"/>
+ </shape>
+ </item>
+ <item>
+ <selector>
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <solid android:color="?attr/colorSurfaceVariant"/>
+ <corners android:radius="32dp"/>
+ </shape>
+ </item>
+ <item android:drawable="@android:color/transparent" />
+ </selector>
+ </item>
+</ripple>
diff --git a/ui/common/src/main/res/drawable/bg_gradient.xml b/ui/common/src/main/res/drawable/bg_gradient.xml
new file mode 100644
index 000000000..5022240b3
--- /dev/null
+++ b/ui/common/src/main/res/drawable/bg_gradient.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
+ <gradient
+ android:angle="90"
+ android:endColor="#00ffffff"
+ android:startColor="#ffffffff"
+ android:type="linear" />
+ <corners
+ android:radius="0dp"/>
+</shape> \ No newline at end of file
diff --git a/ui/common/src/main/res/drawable/bg_pill.xml b/ui/common/src/main/res/drawable/bg_pill.xml
new file mode 100644
index 000000000..f5865ccff
--- /dev/null
+++ b/ui/common/src/main/res/drawable/bg_pill.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <stroke
+ android:width="1dp"
+ android:color="?attr/colorPrimary" />
+ <corners android:radius="20dp" />
+</shape> \ No newline at end of file
diff --git a/ui/common/src/main/res/drawable/bg_rounded_corners.xml b/ui/common/src/main/res/drawable/bg_rounded_corners.xml
new file mode 100644
index 000000000..11b7710c4
--- /dev/null
+++ b/ui/common/src/main/res/drawable/bg_rounded_corners.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="8dp" />
+</shape>
diff --git a/ui/preferences/src/main/assets/website-languages.txt b/ui/common/website-languages.txt
index 64361314b..64361314b 100644
--- a/ui/preferences/src/main/assets/website-languages.txt
+++ b/ui/common/website-languages.txt
diff --git a/ui/preferences/build.gradle b/ui/preferences/build.gradle
index a777b4332..eb595da58 100644
--- a/ui/preferences/build.gradle
+++ b/ui/preferences/build.gradle
@@ -23,7 +23,6 @@ android {
}
dependencies {
- implementation project(":core")
implementation project(":event")
implementation project(":net:common")
implementation project(":net:sync:model")
diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/AboutFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/AboutFragment.java
index ad956ed71..3e30b44dd 100644
--- a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/AboutFragment.java
+++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/about/AboutFragment.java
@@ -10,7 +10,7 @@ import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceFragmentCompat;
import com.google.android.material.snackbar.Snackbar;
-import de.danoeh.antennapod.core.util.IntentUtils;
+import de.danoeh.antennapod.ui.common.IntentUtils;
import de.danoeh.antennapod.ui.preferences.BuildConfig;
import de.danoeh.antennapod.ui.preferences.R;
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
index 85badcefc..1a60d03ce 100644
--- 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
@@ -9,7 +9,7 @@ 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.common.IntentUtils;
import de.danoeh.antennapod.ui.preferences.R;
import io.reactivex.Single;
import io.reactivex.SingleOnSubscribe;
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
index bd6a75503..99f63156c 100644
--- 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
@@ -1,6 +1,7 @@
package de.danoeh.antennapod.ui.preferences.screen.downloads;
import android.content.Context;
+import android.os.StatFs;
import android.text.format.Formatter;
import android.view.LayoutInflater;
import android.view.View;
@@ -12,7 +13,6 @@ 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;
@@ -125,11 +125,17 @@ public class DataFolderAdapter extends RecyclerView.Adapter<DataFolderAdapter.Vi
}
long getAvailableSpace() {
- return StorageUtils.getFreeSpaceAvailable(path);
+ StatFs stat = new StatFs(path);
+ long availableBlocks = stat.getAvailableBlocksLong();
+ long blockSize = stat.getBlockSizeLong();
+ return availableBlocks * blockSize;
}
long getTotalSpace() {
- return StorageUtils.getTotalSpaceAvailable(path);
+ StatFs stat = new StatFs(path);
+ long blockCount = stat.getBlockCountLong();
+ long blockSize = stat.getBlockSizeLong();
+ return blockCount * blockSize;
}
int getUsagePercentage() {
diff --git a/ui/preferences/src/main/res/values/arrays.xml b/ui/preferences/src/main/res/values/arrays.xml
new file mode 100644
index 000000000..a4f5d7f38
--- /dev/null
+++ b/ui/preferences/src/main/res/values/arrays.xml
@@ -0,0 +1,277 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string-array name="spnAutoDeleteItems">
+ <item>@string/global_default</item>
+ <item>@string/feed_auto_download_always</item>
+ <item>@string/feed_auto_download_never</item>
+ </string-array>
+
+ <string-array name="spnAutoDeleteValues">
+ <item>global</item>
+ <item>always</item>
+ <item>never</item>
+ </string-array>
+
+ <string-array name="spnVolumeAdaptationItems">
+ <item>@string/feed_volume_reduction_heavy</item>
+ <item>@string/feed_volume_reduction_light</item>
+ <item>@string/feed_volume_reduction_off</item>
+ <item>@string/feed_volume_boost_light</item>
+ <item>@string/feed_volume_boost_medium</item>
+ <item>@string/feed_volume_boost_heavy</item>
+ </string-array>
+
+ <string-array name="spnVolumeAdaptationValues">
+ <item>heavy</item>
+ <item>light</item>
+ <item>off</item>
+ <item>light_boost</item>
+ <item>medium_boost</item>
+ <item>heavy_boost</item>
+ </string-array>
+
+ <string-array name="feed_refresh_interval_entries">
+ <item>@string/feed_refresh_never</item>
+ <item>@string/feed_every_hour</item>
+ <item>@string/feed_every_2_hours</item>
+ <item>@string/feed_every_4_hours</item>
+ <item>@string/feed_every_8_hours</item>
+ <item>@string/feed_every_12_hours</item>
+ <item>@string/feed_every_24_hours</item>
+ <item>@string/feed_every_72_hours</item>
+ </string-array>
+
+ <string-array name="feed_refresh_interval_values">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>4</item>
+ <item>8</item>
+ <item>12</item>
+ <item>24</item>
+ <item>72</item>
+ </string-array>
+
+ <string-array name="globalNewEpisodesActionItems">
+ <item>@string/feed_new_episodes_action_add_to_inbox</item>
+ <item>@string/feed_new_episodes_action_add_to_queue</item>
+ <item>@string/feed_new_episodes_action_nothing</item>
+ </string-array>
+
+ <string-array name="globalNewEpisodesActionValues">
+ <item>1</item>
+ <item>3</item>
+ <item>2</item>
+ </string-array>
+
+ <string-array name="feedNewEpisodesActionItems">
+ <item>@string/global_default</item>
+ <item>@string/feed_new_episodes_action_add_to_inbox</item>
+ <item>@string/feed_new_episodes_action_add_to_queue</item>
+ <item>@string/feed_new_episodes_action_nothing</item>
+ </string-array>
+
+ <string-array name="feedNewEpisodesActionValues">
+ <item>0</item>
+ <item>1</item>
+ <item>3</item>
+ <item>2</item>
+ </string-array>
+
+ <string-array name="smart_mark_as_played_values">
+ <item>0</item>
+ <item>15</item>
+ <item>30</item>
+ <item>60</item>
+ <item>120</item>
+ <item>300</item>
+ </string-array>
+
+
+ <integer-array name="seek_delta_values">
+ <item>5</item>
+ <item>10</item>
+ <item>15</item>
+ <item>20</item>
+ <item>30</item>
+ <item>45</item>
+ <item>60</item>
+ </integer-array>
+
+ <string-array name="episode_cache_size_entries">
+ <item>5</item>
+ <item>10</item>
+ <item>25</item>
+ <item>50</item>
+ <item>100</item>
+ <item>500</item>
+ <item>@string/pref_episode_cache_unlimited</item>
+ </string-array>
+
+ <string-array name="episode_cache_size_values">
+ <item>5</item>
+ <item>10</item>
+ <item>25</item>
+ <item>50</item>
+ <item>100</item>
+ <item>500</item>
+ <item>-1</item>
+ </string-array>
+
+ <string-array name="mobile_update_entries">
+ <item>@string/pref_mobileUpdate_refresh</item>
+ <item>@string/pref_mobileUpdate_episode_download</item>
+ <item>@string/pref_mobileUpdate_auto_download</item>
+ <item>@string/pref_mobileUpdate_streaming</item>
+ <item>@string/pref_mobileUpdate_images</item>
+ <item>@string/synchronization_pref</item>
+ </string-array>
+
+ <string-array name="mobile_update_values">
+ <item>feed_refresh</item>
+ <item>episode_download</item>
+ <item>auto_download</item>
+ <item>streaming</item>
+ <item>images</item>
+ <item>sync</item>
+ </string-array>
+
+ <string-array name="mobile_update_default_value">
+ <item>images</item>
+ <item>sync</item>
+ </string-array>
+
+ <string-array name="episode_cleanup_entries">
+ <item>@string/episode_cleanup_except_favorite_removal</item>
+ <item>@string/episode_cleanup_queue_removal</item>
+ <item>0</item>
+ <item>1</item>
+ <item>3</item>
+ <item>5</item>
+ <item>7</item>
+ <item>@string/episode_cleanup_never</item>
+ </string-array>
+
+ <string-array name="button_action_options">
+ <item>@string/button_action_fast_forward</item>
+ <item>@string/button_action_rewind</item>
+ <item>@string/button_action_skip_episode</item>
+ <item>@string/button_action_restart_episode</item>
+ </string-array>
+
+ <string-array name="button_action_values">
+ <item>@string/keycode_media_fast_forward</item>
+ <item>@string/keycode_media_rewind</item>
+ <item>@string/keycode_media_next</item>
+ <item>@string/keycode_media_previous</item>
+ </string-array>
+
+ <string-array name="enqueue_location_options">
+ <item>@string/enqueue_location_back</item>
+ <item>@string/enqueue_location_front</item>
+ <item>@string/enqueue_location_after_current</item>
+ <item>@string/enqueue_location_random</item>
+ </string-array>
+
+ <string-array name="enqueue_location_values">
+ <!-- MUST be the same as UserPreferences.EnqueueLocation enum -->
+ <item>BACK</item>
+ <item>FRONT</item>
+ <item>AFTER_CURRENTLY_PLAYING</item>
+ <item>RANDOM</item>
+ </string-array>
+
+ <string-array name="episode_cleanup_values">
+ <item>-3</item>
+ <item>-1</item>
+ <item>0</item>
+ <item>12</item>
+ <item>24</item>
+ <item>72</item>
+ <item>120</item>
+ <item>168</item>
+ <item>-2</item>
+ </string-array>
+
+ <string-array name="nav_drawer_titles">
+ <item>@string/home_label</item>
+ <item>@string/queue_label</item>
+ <item>@string/inbox_label</item>
+ <item>@string/episodes_label</item>
+ <item>@string/subscriptions_label</item>
+ <item>@string/downloads_label</item>
+ <item>@string/playback_history_label</item>
+ <item>@string/add_feed_label</item>
+ <item>@string/subscriptions_list_label</item>
+ </string-array>
+
+ <string-array name="nav_drawer_feed_order_options">
+ <item>@string/drawer_feed_order_unplayed_episodes</item>
+ <item>@string/drawer_feed_order_alphabetical</item>
+ <item>@string/drawer_feed_order_last_update</item>
+ <item>@string/drawer_feed_order_most_played</item>
+ </string-array>
+ <string-array name="nav_drawer_feed_order_values">
+ <item>0</item>
+ <item>1</item>
+ <item>2</item>
+ <item>3</item>
+ </string-array>
+
+ <string-array name="nav_drawer_feed_counter_options">
+ <item>@string/drawer_feed_counter_inbox</item>
+ <item>@string/drawer_feed_counter_unplayed</item>
+ <item>@string/drawer_feed_counter_downloaded</item>
+ <item>@string/drawer_feed_counter_downloaded_unplayed</item>
+ <item>@string/drawer_feed_counter_none</item>
+ </string-array>
+ <string-array name="nav_drawer_feed_counter_values">
+ <item>1</item>
+ <item>2</item>
+ <item>4</item>
+ <item>5</item>
+ <item>3</item>
+ </string-array>
+
+ <string-array name="home_section_titles">
+ <item>@string/home_continue_title</item>
+ <item>@string/home_new_title</item>
+ <item>@string/home_surprise_title</item>
+ <item>@string/home_classics_title</item>
+ <item>@string/home_downloads_title</item>
+ </string-array>
+
+ <string-array name="home_section_tags">
+ <item>QueueSection</item>
+ <item>InboxSection</item>
+ <item>EpisodesSurpriseSection</item>
+ <item>SubscriptionsSection</item>
+ <item>DownloadsSection</item>
+ </string-array>
+
+ <string-array name="full_notification_buttons_options">
+ <item>@string/skip_episode_label</item>
+ <item>@string/next_chapter</item>
+ <item>@string/playback_speed</item>
+ <item>@string/sleep_timer_label</item>
+ </string-array>
+
+ <string-array name="default_page_values">
+ <item>HomeFragment</item>
+ <item>QueueFragment</item>
+ <item>NewEpisodesFragment</item>
+ <item>EpisodesFragment</item>
+ <item>SubscriptionFragment</item>
+ <item>remember</item>
+ </string-array>
+
+ <string-array name="default_page_titles">
+ <item>@string/home_label</item>
+ <item>@string/queue_label</item>
+ <item>@string/inbox_label</item>
+ <item>@string/episodes_label</item>
+ <item>@string/subscriptions_label</item>
+ <item>@string/remember_last_page</item>
+ </string-array>
+</resources>
diff --git a/ui/preferences/src/main/res/values/keycodes.xml b/ui/preferences/src/main/res/values/keycodes.xml
new file mode 100644
index 000000000..e0d44ce04
--- /dev/null
+++ b/ui/preferences/src/main/res/values/keycodes.xml
@@ -0,0 +1,9 @@
+<resources
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:ignore="MissingTranslation">
+
+ <string name="keycode_media_next">87</string>
+ <string name="keycode_media_previous">88</string>
+ <string name="keycode_media_rewind">89</string>
+ <string name="keycode_media_fast_forward">90</string>
+</resources>
diff --git a/ui/statistics/build.gradle b/ui/statistics/build.gradle
index 1e33a0f0a..be86665a7 100644
--- a/ui/statistics/build.gradle
+++ b/ui/statistics/build.gradle
@@ -9,7 +9,6 @@ android {
}
dependencies {
- implementation project(":core")
implementation project(":event")
implementation project(":model")
implementation project(':storage:database')
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java
index a2f9e8146..74a0eaa5f 100644
--- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java
@@ -18,7 +18,7 @@ import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
-import de.danoeh.antennapod.core.util.ConfirmationDialog;
+import de.danoeh.antennapod.ui.common.ConfirmationDialog;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.event.StatisticsEvent;
import de.danoeh.antennapod.ui.common.PagedToolbarFragment;
diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsFragment.java
index 6a9e80740..aacd1294d 100644
--- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsFragment.java
+++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/feed/FeedStatisticsFragment.java
@@ -13,7 +13,7 @@ import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.StatisticsItem;
import de.danoeh.antennapod.ui.common.Converter;
import de.danoeh.antennapod.ui.common.DateFormatter;
-import de.danoeh.antennapod.core.util.ReleaseScheduleGuesser;
+import de.danoeh.antennapod.storage.database.ReleaseScheduleGuesser;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.SortOrder;