Optional automatic daily database backup (#6994)
This commit is contained in:
parent
d40b9ef59b
commit
55845c46a1
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
5
storage/importexport/src/main/res/values/ids.xml
Normal file
5
storage/importexport/src/main/res/values/ids.xml
Normal file
@ -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>
|
@ -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.
|
||||
*
|
||||
|
@ -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…</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user