diff options
Diffstat (limited to 'ui')
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; |