mirror of
https://github.com/AntennaPod/AntennaPod.git
synced 2025-12-01 12:31:45 +00:00
Delete core module (#7060)
This commit is contained in:
3
ui/chapters/README.md
Normal file
3
ui/chapters/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# :ui:chapters
|
||||
|
||||
This module provides chapter loading and merging for display, but not the actual UI to display them.
|
||||
21
ui/chapters/build.gradle
Normal file
21
ui/chapters/build.gradle
Normal file
@ -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"
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
10
ui/common/src/main/res/drawable/bg_blue_gradient.xml
Normal file
10
ui/common/src/main/res/drawable/bg_blue_gradient.xml
Normal file
@ -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>
|
||||
6
ui/common/src/main/res/drawable/bg_circle.xml
Normal file
6
ui/common/src/main/res/drawable/bg_circle.xml
Normal file
@ -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>
|
||||
20
ui/common/src/main/res/drawable/bg_drawer_item.xml
Normal file
20
ui/common/src/main/res/drawable/bg_drawer_item.xml
Normal file
@ -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>
|
||||
10
ui/common/src/main/res/drawable/bg_gradient.xml
Normal file
10
ui/common/src/main/res/drawable/bg_gradient.xml
Normal file
@ -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>
|
||||
7
ui/common/src/main/res/drawable/bg_pill.xml
Normal file
7
ui/common/src/main/res/drawable/bg_pill.xml
Normal file
@ -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>
|
||||
6
ui/common/src/main/res/drawable/bg_rounded_corners.xml
Normal file
6
ui/common/src/main/res/drawable/bg_rounded_corners.xml
Normal file
@ -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>
|
||||
@ -23,7 +23,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":core")
|
||||
implementation project(":event")
|
||||
implementation project(":net:common")
|
||||
implementation project(":net:sync:model")
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() {
|
||||
|
||||
277
ui/preferences/src/main/res/values/arrays.xml
Normal file
277
ui/preferences/src/main/res/values/arrays.xml
Normal file
@ -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>
|
||||
9
ui/preferences/src/main/res/values/keycodes.xml
Normal file
9
ui/preferences/src/main/res/values/keycodes.xml
Normal file
@ -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>
|
||||
@ -9,7 +9,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":core")
|
||||
implementation project(":event")
|
||||
implementation project(":model")
|
||||
implementation project(':storage:database')
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user