summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java51
-rw-r--r--app/src/main/res/xml/preferences_import_export.xml5
-rw-r--r--storage/importexport/build.gradle8
-rw-r--r--storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/AutomaticDatabaseExportWorker.java127
-rw-r--r--storage/importexport/src/main/res/values/ids.xml5
-rw-r--r--storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java10
-rw-r--r--ui/i18n/src/main/res/values/strings.xml3
8 files changed, 205 insertions, 6 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
index fe1db360b..d38d4e3ad 100644
--- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
+++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java
@@ -59,6 +59,7 @@ import de.danoeh.antennapod.fragment.TransitionEffect;
import de.danoeh.antennapod.model.download.DownloadStatus;
import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
import de.danoeh.antennapod.playback.cast.CastEnabledActivity;
+import de.danoeh.antennapod.storage.importexport.AutomaticDatabaseExportWorker;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.common.ThemeUtils;
@@ -167,6 +168,7 @@ public class MainActivity extends CastEnabledActivity {
FeedUpdateManager.restartUpdateAlarm(this, false);
SynchronizationQueueSink.syncNowIfNotSyncedRecently();
+ AutomaticDatabaseExportWorker.enqueueIfNeeded(this, false);
WorkManager.getInstance(this)
.getWorkInfosByTagLiveData(FeedUpdateManager.WORK_TAG_FEED_UPDATE)
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java
index 9191825aa..6c0e0d456 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/ImportExportPreferencesFragment.java
@@ -15,8 +15,10 @@ import androidx.activity.result.contract.ActivityResultContracts;
import androidx.activity.result.contract.ActivityResultContracts.GetContent;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.documentfile.provider.DocumentFile;
+import androidx.preference.SwitchPreferenceCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
@@ -30,6 +32,7 @@ import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.importexport.AutomaticDatabaseExportWorker;
import de.danoeh.antennapod.storage.importexport.DatabaseExporter;
import de.danoeh.antennapod.storage.importexport.FavoritesWriter;
import de.danoeh.antennapod.storage.importexport.HtmlWriter;
@@ -59,6 +62,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
private static final String PREF_HTML_EXPORT = "prefHtmlExport";
private static final String PREF_DATABASE_IMPORT = "prefDatabaseImport";
private static final String PREF_DATABASE_EXPORT = "prefDatabaseExport";
+ private static final String PREF_AUTOMATIC_DATABASE_EXPORT = "prefAutomaticDatabaseExport";
private static final String PREF_FAVORITE_EXPORT = "prefFavoritesExport";
private static final String DEFAULT_OPML_OUTPUT_NAME = "antennapod-feeds-%s.opml";
private static final String CONTENT_TYPE_OPML = "text/x-opml";
@@ -88,6 +92,8 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
startActivity(intent);
}
});
+ private final ActivityResultLauncher<Uri> automaticBackupLauncher =
+ registerForActivityResult(new PickWritableFolder(), this::setupAutomaticBackup);
private Disposable disposable;
private ProgressDialog progressDialog;
@@ -143,7 +149,26 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
});
findPreference(PREF_DATABASE_EXPORT).setOnPreferenceClickListener(
preference -> {
- exportDatabase();
+ backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME));
+ return true;
+ });
+ ((SwitchPreferenceCompat) findPreference(PREF_AUTOMATIC_DATABASE_EXPORT))
+ .setChecked(UserPreferences.getAutomaticExportFolder() != null);
+ findPreference(PREF_AUTOMATIC_DATABASE_EXPORT).setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ if (Boolean.TRUE.equals(newValue)) {
+ try {
+ automaticBackupLauncher.launch(null);
+ } catch (ActivityNotFoundException e) {
+ e.printStackTrace();
+ Snackbar.make(getView(), R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
+ .show();
+ }
+ return false;
+ } else {
+ UserPreferences.setAutomaticExportFolder(null);
+ AutomaticDatabaseExportWorker.enqueueIfNeeded(getContext(), false);
+ }
return true;
});
findPreference(PREF_FAVORITE_EXPORT).setOnPreferenceClickListener(
@@ -157,10 +182,6 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
return String.format(fname, new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date()));
}
- private void exportDatabase() {
- backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME));
- }
-
private void importDatabase() {
// setup the alert builder
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
@@ -330,6 +351,17 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
}
}
+ private void setupAutomaticBackup(Uri uri) {
+ if (uri == null) {
+ return;
+ }
+ getActivity().getContentResolver().takePersistableUriPermission(uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ UserPreferences.setAutomaticExportFolder(uri.toString());
+ AutomaticDatabaseExportWorker.enqueueIfNeeded(getContext(), true);
+ ((SwitchPreferenceCompat) findPreference(PREF_AUTOMATIC_DATABASE_EXPORT)).setChecked(true);
+ }
+
private static class BackupDatabase extends ActivityResultContracts.CreateDocument {
BackupDatabase() {
@@ -345,6 +377,15 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
}
}
+ private static class PickWritableFolder extends ActivityResultContracts.OpenDocumentTree {
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull final Context context, @Nullable final Uri input) {
+ return super.createIntent(context, input)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ }
+ }
+
private enum Export {
OPML(CONTENT_TYPE_OPML, DEFAULT_OPML_OUTPUT_NAME, R.string.opml_export_label),
HTML(CONTENT_TYPE_HTML, DEFAULT_HTML_OUTPUT_NAME, R.string.html_export_label),
diff --git a/app/src/main/res/xml/preferences_import_export.xml b/app/src/main/res/xml/preferences_import_export.xml
index 383bff117..789c8c216 100644
--- a/app/src/main/res/xml/preferences_import_export.xml
+++ b/app/src/main/res/xml/preferences_import_export.xml
@@ -9,6 +9,11 @@
search:keywords="@string/import_export_search_keywords"
android:title="@string/database_export_label"
android:summary="@string/database_export_summary"/>
+ <SwitchPreferenceCompat
+ android:key="prefAutomaticDatabaseExport"
+ android:title="@string/automatic_database_export_label"
+ android:summary="@string/automatic_database_export_summary"
+ android:defaultValue="false" />
<Preference
android:key="prefDatabaseImport"
search:keywords="@string/import_export_search_keywords"
diff --git a/storage/importexport/build.gradle b/storage/importexport/build.gradle
index ddbbd1951..ae1faa284 100644
--- a/storage/importexport/build.gradle
+++ b/storage/importexport/build.gradle
@@ -8,14 +8,20 @@ android {
}
dependencies {
+ implementation project(':event')
implementation project(':storage:database')
implementation project(':storage:preferences')
implementation project(':ui:i18n')
+ implementation project(':ui:notifications')
implementation project(':model')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
+ implementation "androidx.core:core:$coreVersion"
+ implementation 'androidx.documentfile:documentfile:1.0.1'
+ implementation "androidx.work:work-runtime:$workManagerVersion"
+
implementation "commons-io:commons-io:$commonsioVersion"
implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
- implementation 'androidx.documentfile:documentfile:1.0.1'
+ implementation "org.greenrobot:eventbus:$eventbusVersion"
}
diff --git a/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/AutomaticDatabaseExportWorker.java b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/AutomaticDatabaseExportWorker.java
new file mode 100644
index 000000000..6002c3bba
--- /dev/null
+++ b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/AutomaticDatabaseExportWorker.java
@@ -0,0 +1,127 @@
+package de.danoeh.antennapod.storage.importexport;
+
+import android.Manifest;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.core.app.NotificationCompat;
+import androidx.core.content.ContextCompat;
+import androidx.documentfile.provider.DocumentFile;
+import androidx.work.ExistingPeriodicWorkPolicy;
+import androidx.work.PeriodicWorkRequest;
+import androidx.work.WorkManager;
+import androidx.work.Worker;
+import androidx.work.WorkerParameters;
+import de.danoeh.antennapod.event.MessageEvent;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.ui.notifications.NotificationUtils;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+import org.greenrobot.eventbus.EventBus;
+
+public class AutomaticDatabaseExportWorker extends Worker {
+ private static final String WORK_ID_AUTOMATIC_DATABASE_EXPORT = "de.danoeh.antennapod.AutomaticDbExport";
+
+ public static void enqueueIfNeeded(Context context, boolean replace) {
+ if (UserPreferences.getAutomaticExportFolder() == null) {
+ WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_AUTOMATIC_DATABASE_EXPORT);
+ } else {
+ PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(
+ AutomaticDatabaseExportWorker.class, 1, TimeUnit.DAYS)
+ .build();
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_AUTOMATIC_DATABASE_EXPORT,
+ replace ? ExistingPeriodicWorkPolicy.REPLACE : ExistingPeriodicWorkPolicy.KEEP, workRequest);
+ }
+ }
+
+ public AutomaticDatabaseExportWorker(@NonNull Context context, @NonNull WorkerParameters params) {
+ super(context, params);
+ }
+
+ @Override
+ @NonNull
+ public Result doWork() {
+ String folderUri = UserPreferences.getAutomaticExportFolder();
+ if (folderUri == null) {
+ return Result.success();
+ }
+ try {
+ export(folderUri);
+ return Result.success();
+ } catch (IOException e) {
+ showErrorNotification(e);
+ return Result.failure();
+ }
+ }
+
+ private void export(String folderUri) throws IOException {
+ DocumentFile documentFolder = DocumentFile.fromTreeUri(getApplicationContext(), Uri.parse(folderUri));
+ if (documentFolder == null || !documentFolder.exists() || !documentFolder.canWrite()) {
+ throw new IOException("Unable to open export folder");
+ }
+ String filename = String.format("AntennaPodBackup-%s.db",
+ new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date()));
+ DocumentFile exportFile = documentFolder.createFile("application/x-sqlite3", filename);
+ if (exportFile == null || !exportFile.canWrite()) {
+ throw new IOException("Unable to create export file");
+ }
+ DatabaseExporter.exportToDocument(exportFile.getUri(), getApplicationContext());
+ List<DocumentFile> files = new ArrayList<>(Arrays.asList(documentFolder.listFiles()));
+ Iterator<DocumentFile> itr = files.iterator();
+ while (itr.hasNext()) {
+ DocumentFile file = itr.next();
+ if (!file.getName().startsWith("AntennaPod")) {
+ itr.remove();
+ }
+ }
+ Collections.sort(files, (o1, o2) -> Long.compare(o2.lastModified(), o1.lastModified()));
+ for (int i = 5; i < files.size(); i++) {
+ files.get(i).delete();
+ }
+ }
+
+ private void showErrorNotification(Exception exception) {
+ final String description = getApplicationContext().getString(R.string.automatic_database_export_error)
+ + " " + exception.getMessage();
+ if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) {
+ EventBus.getDefault().post(new MessageEvent(description));
+ return;
+ }
+
+ Intent intent = getApplicationContext().getPackageManager().getLaunchIntentForPackage(
+ getApplicationContext().getPackageName());
+ PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(),
+ R.id.pending_intent_backup_error, intent, PendingIntent.FLAG_UPDATE_CURRENT
+ | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0));
+ Notification notification = new NotificationCompat.Builder(getApplicationContext(),
+ NotificationUtils.CHANNEL_ID_SYNC_ERROR)
+ .setContentTitle(getApplicationContext().getString(R.string.automatic_database_export_error))
+ .setContentText(exception.getMessage())
+ .setStyle(new NotificationCompat.BigTextStyle().bigText(description))
+ .setContentIntent(pendingIntent)
+ .setSmallIcon(R.drawable.ic_notification_sync_error)
+ .setAutoCancel(true)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .build();
+ NotificationManager nm = (NotificationManager) getApplicationContext()
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS)
+ == PackageManager.PERMISSION_GRANTED) {
+ nm.notify(R.id.notification_id_backup_error, notification);
+ }
+ }
+}
diff --git a/storage/importexport/src/main/res/values/ids.xml b/storage/importexport/src/main/res/values/ids.xml
new file mode 100644
index 000000000..7e973d82a
--- /dev/null
+++ b/storage/importexport/src/main/res/values/ids.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="pending_intent_backup_error" type="id" />
+ <item name="notification_id_backup_error" type="id" />
+</resources>
diff --git a/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java b/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java
index a42493692..f2baf1242 100644
--- a/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java
+++ b/storage/preferences/src/main/java/de/danoeh/antennapod/storage/preferences/UserPreferences.java
@@ -114,6 +114,7 @@ public class UserPreferences {
// Other
private static final String PREF_DATA_FOLDER = "prefDataFolder";
public static final String PREF_DELETE_REMOVES_FROM_QUEUE = "prefDeleteRemovesFromQueue";
+ private static final String PREF_AUTOMATIC_EXPORT_FOLDER = "prefAutomaticExportFolder";
// Mediaplayer
private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed";
@@ -282,6 +283,15 @@ public class UserPreferences {
prefs.edit().putBoolean(PREF_SHOW_TIME_LEFT, showRemain).apply();
}
+ @Nullable
+ public static String getAutomaticExportFolder() {
+ return prefs.getString(PREF_AUTOMATIC_EXPORT_FOLDER, null);
+ }
+
+ public static void setAutomaticExportFolder(@Nullable String folder) {
+ prefs.edit().putString(PREF_AUTOMATIC_EXPORT_FOLDER, folder).apply();
+ }
+
/**
* Returns notification priority.
*
diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml
index b07d6c45f..964b85c77 100644
--- a/ui/i18n/src/main/res/values/strings.xml
+++ b/ui/i18n/src/main/res/values/strings.xml
@@ -579,6 +579,9 @@
<string name="opml_export_label">OPML export</string>
<string name="html_export_label">HTML export</string>
<string name="database_export_label">Database export</string>
+ <string name="automatic_database_export_label">Automatic database export</string>
+ <string name="automatic_database_export_summary">Automatically create daily backups of the AntennaPod database</string>
+ <string name="automatic_database_export_error">Error during automatic database backup</string>
<string name="database_import_label">Database import</string>
<string name="database_import_warning">Importing a database will replace all of your current subscriptions and playing history. You should export your current database as a backup. Do you want to replace?</string>
<string name="please_wait">Please wait&#8230;</string>