diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ad68fcfe3..c17e2120a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -140,13 +140,6 @@
-
-
-
backup());
- findViewById(R.id.button_import).setOnClickListener(view -> restore());
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- finish();
- return true;
- } else {
- return super.onOptionsItemSelected(item);
- }
- }
-
- private void backup() {
- if (Build.VERSION.SDK_INT >= 19) {
- Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
- .addCategory(Intent.CATEGORY_OPENABLE)
- .setType("application/x-sqlite3")
- .putExtra(Intent.EXTRA_TITLE, EXPORT_FILENAME);
-
- startActivityForResult(intent, REQUEST_CODE_BACKUP_DOCUMENT);
- } else {
- try {
- File sd = Environment.getExternalStorageDirectory();
- File backupDB = new File(sd, EXPORT_FILENAME);
- writeBackupTo(new FileOutputStream(backupDB));
- } catch (IOException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- Snackbar.make(findViewById(R.id.import_export_layout), e.getLocalizedMessage(), Snackbar.LENGTH_SHORT).show();
- }
- }
- }
-
- private void restore() {
- if (Build.VERSION.SDK_INT >= 19) {
- Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
- intent.setType("*/*");
- startActivityForResult(intent, REQUEST_CODE_RESTORE);
- } else {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.setType("*/*");
- startActivityForResult(Intent.createChooser(intent,
- getString(R.string.import_select_file)), REQUEST_CODE_RESTORE);
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
- if (resultCode != RESULT_OK || resultData == null) {
- return;
- }
- Uri uri = resultData.getData();
-
- if (requestCode == REQUEST_CODE_RESTORE) {
- restoreFrom(uri);
- } else if (requestCode == REQUEST_CODE_BACKUP_DOCUMENT) {
- backupToDocument(uri);
- }
- }
-
- private void restoreFrom(Uri inputUri) {
- InputStream inputStream = null;
- try {
- if (!validateDB(inputUri)) {
- displayBadFileDialog();
- return;
- }
-
- File currentDB = getDatabasePath(PodDBAdapter.DATABASE_NAME);
- inputStream = getContentResolver().openInputStream(inputUri);
- FileUtils.copyInputStreamToFile(inputStream, currentDB);
- displayImportSuccessDialog();
- } catch (IOException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- Snackbar.make(findViewById(R.id.import_export_layout), e.getLocalizedMessage(), Snackbar.LENGTH_SHORT).show();
- } finally {
- IOUtils.closeQuietly(inputStream);
- }
- }
-
- private static final byte[] SQLITE3_MAGIC = "SQLite format 3\0".getBytes();
- private boolean validateDB(Uri inputUri) throws IOException {
- try (InputStream inputStream = getContentResolver().openInputStream(inputUri)) {
- byte[] magicBuf = new byte[SQLITE3_MAGIC.length];
- if (inputStream.read(magicBuf) == magicBuf.length) {
- return Arrays.equals(SQLITE3_MAGIC, magicBuf);
- }
- }
-
- return false;
- }
-
- private void displayBadFileDialog() {
- AlertDialog.Builder d = new AlertDialog.Builder(ImportExportActivity.this);
- d.setMessage(R.string.import_bad_file)
- .setCancelable(false)
- .setPositiveButton(android.R.string.ok, ((dialogInterface, i) -> {
- // do nothing
- }))
- .show();
- }
-
- private void displayImportSuccessDialog() {
- AlertDialog.Builder d = new AlertDialog.Builder(ImportExportActivity.this);
- d.setMessage(R.string.import_ok);
- d.setCancelable(false);
- d.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
- Intent intent = new Intent(getApplicationContext(), SplashActivity.class);
- ComponentName cn = intent.getComponent();
- Intent mainIntent = Intent.makeRestartActivityTask(cn);
- startActivity(mainIntent);
- });
- d.show();
- }
-
- private void backupToDocument(Uri uri) {
- ParcelFileDescriptor pfd = null;
- FileOutputStream fileOutputStream = null;
- try {
- pfd = getContentResolver().openFileDescriptor(uri, "w");
- fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
- writeBackupTo(fileOutputStream);
-
- Snackbar.make(findViewById(R.id.import_export_layout),
- R.string.export_ok, Snackbar.LENGTH_SHORT).show();
- } catch (IOException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- Snackbar.make(findViewById(R.id.import_export_layout), e.getLocalizedMessage(), Snackbar.LENGTH_SHORT).show();
- } finally {
- IOUtils.closeQuietly(fileOutputStream);
-
- if (pfd != null) {
- try {
- pfd.close();
- } catch (IOException e) {
- Log.d(TAG, "Unable to close ParcelFileDescriptor");
- }
- }
- }
- }
-
- private void writeBackupTo(FileOutputStream outFileStream) {
- FileChannel src = null;
- FileChannel dst = null;
- try {
- File currentDB = getDatabasePath(PodDBAdapter.DATABASE_NAME);
-
- if (currentDB.exists()) {
- src = new FileInputStream(currentDB).getChannel();
- dst = outFileStream.getChannel();
- dst.transferFrom(src, 0, src.size());
-
- Snackbar.make(findViewById(R.id.import_export_layout),
- R.string.export_ok, Snackbar.LENGTH_SHORT).show();
- } else {
- Snackbar.make(findViewById(R.id.import_export_layout),
- "Can not access current database", Snackbar.LENGTH_SHORT).show();
- }
- } catch (IOException e) {
- Log.e(TAG, Log.getStackTraceString(e));
- Snackbar.make(findViewById(R.id.import_export_layout), e.getLocalizedMessage(), Snackbar.LENGTH_SHORT).show();
- } finally {
- IOUtils.closeQuietly(src);
- IOUtils.closeQuietly(dst);
- }
- }
-}
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 ca69414f1..9803e072c 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
@@ -1,9 +1,9 @@
package de.danoeh.antennapod.fragment.preferences;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -11,26 +11,30 @@ import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.os.Environment;
import android.util.Log;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.FileProvider;
-import androidx.documentfile.provider.DocumentFile;
import androidx.preference.PreferenceFragmentCompat;
+import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
-import de.danoeh.antennapod.activity.ImportExportActivity;
import de.danoeh.antennapod.activity.OpmlImportFromPathActivity;
import de.danoeh.antennapod.activity.PreferenceActivity;
+import de.danoeh.antennapod.activity.SplashActivity;
import de.danoeh.antennapod.asynctask.DocumentFileExportWorker;
import de.danoeh.antennapod.asynctask.ExportWorker;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.core.export.html.HtmlWriter;
import de.danoeh.antennapod.core.export.opml.OpmlWriter;
+import de.danoeh.antennapod.core.storage.DatabaseExporter;
+import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
+import java.io.FileOutputStream;
import java.util.List;
public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
@@ -38,19 +42,27 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
private static final String PREF_OPML_EXPORT = "prefOpmlExport";
private static final String PREF_OPML_IMPORT = "prefOpmlImport";
private static final String PREF_HTML_EXPORT = "prefHtmlExport";
- private static final String IMPORT_EXPORT = "importExport";
- private static final int CHOOSE_OPML_EXPORT_PATH = 1;
+ private static final String PREF_DATABASE_IMPORT = "prefDatabaseImport";
+ private static final String PREF_DATABASE_EXPORT = "prefDatabaseExport";
+ private static final int REQUEST_CODE_CHOOSE_OPML_EXPORT_PATH = 1;
private static final String DEFAULT_OPML_OUTPUT_NAME = "antennapod-feeds.opml";
private static final String CONTENT_TYPE_OPML = "text/x-opml";
- private static final int CHOOSE_HTML_EXPORT_PATH = 2;
+ private static final int REQUEST_CODE_CHOOSE_HTML_EXPORT_PATH = 2;
private static final String DEFAULT_HTML_OUTPUT_NAME = "antennapod-feeds.html";
private static final String CONTENT_TYPE_HTML = "text/html";
+ private static final int REQUEST_CODE_RESTORE_DATABASE = 3;
+ private static final int REQUEST_CODE_BACKUP_DATABASE = 4;
+ private static final String DATABASE_EXPORT_FILENAME = "AntennaPodBackup.db";
private Disposable disposable;
+ private ProgressDialog progressDialog;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences_import_export);
setupStorageScreen();
+ progressDialog = new ProgressDialog(getContext());
+ progressDialog.setIndeterminate(true);
+ progressDialog.setMessage(getContext().getString(R.string.please_wait));
}
@Override
@@ -69,24 +81,17 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
private void setupStorageScreen() {
final Activity activity = getActivity();
-
- findPreference(IMPORT_EXPORT).setOnPreferenceClickListener(
- preference -> {
- activity.startActivity(new Intent(activity, ImportExportActivity.class));
- return true;
- }
- );
findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener(
preference -> {
openExportPathPicker(CONTENT_TYPE_OPML, DEFAULT_OPML_OUTPUT_NAME,
- CHOOSE_OPML_EXPORT_PATH, new OpmlWriter());
+ REQUEST_CODE_CHOOSE_OPML_EXPORT_PATH, new OpmlWriter());
return true;
}
);
findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener(
preference -> {
openExportPathPicker(CONTENT_TYPE_HTML, DEFAULT_HTML_OUTPUT_NAME,
- CHOOSE_HTML_EXPORT_PATH, new HtmlWriter());
+ REQUEST_CODE_CHOOSE_HTML_EXPORT_PATH, new HtmlWriter());
return true;
});
findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener(
@@ -94,13 +99,20 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
activity.startActivity(new Intent(activity, OpmlImportFromPathActivity.class));
return true;
});
+ findPreference(PREF_DATABASE_IMPORT).setOnPreferenceClickListener(
+ preference -> {
+ importDatabase();
+ return true;
+ });
+ findPreference(PREF_DATABASE_EXPORT).setOnPreferenceClickListener(
+ preference -> {
+ exportDatabase();
+ return true;
+ });
}
- private boolean export(ExportWriter exportWriter, final Uri uri) {
+ private void exportWithWriter(ExportWriter exportWriter, final Uri uri) {
Context context = getActivity();
- final ProgressDialog progressDialog = new ProgressDialog(context);
- progressDialog.setMessage(context.getString(R.string.exporting_label));
- progressDialog.setIndeterminate(true);
progressDialog.show();
if (uri == null) {
Observable observable = new ExportWorker(exportWriter, getContext()).exportObservable();
@@ -109,24 +121,73 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
.subscribe(output -> {
Uri fileUri = FileProvider.getUriForFile(context.getApplicationContext(),
context.getString(R.string.provider_authority), output);
- showExportSuccessDialog(context.getString(R.string.export_success_sum, output.toString()), fileUri);
+ showExportSuccessDialog(output.toString(), fileUri);
}, this::showExportErrorDialog, progressDialog::dismiss);
} else {
- Observable observable = new DocumentFileExportWorker(exportWriter, context, uri).exportObservable();
- disposable = observable.subscribeOn(Schedulers.io())
+ DocumentFileExportWorker worker = new DocumentFileExportWorker(exportWriter, context, uri);
+ disposable = worker.exportObservable()
+ .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(output -> {
- showExportSuccessDialog(context.getString(R.string.export_success_sum, output.getUri()), output.getUri());
- }, this::showExportErrorDialog, progressDialog::dismiss);
+ .subscribe(output ->
+ showExportSuccessDialog(output.getUri().toString(), output.getUri()),
+ this::showExportErrorDialog, progressDialog::dismiss);
}
- return true;
}
- private void showExportSuccessDialog(final String message, final Uri streamUri) {
- final AlertDialog.Builder alert = new AlertDialog.Builder(getContext())
- .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
+ private void exportDatabase() {
+ if (Build.VERSION.SDK_INT >= 19) {
+ Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("application/x-sqlite3")
+ .putExtra(Intent.EXTRA_TITLE, DATABASE_EXPORT_FILENAME);
+
+ startActivityForResult(intent, REQUEST_CODE_BACKUP_DATABASE);
+ } else {
+ File sd = Environment.getExternalStorageDirectory();
+ File backupDB = new File(sd, DATABASE_EXPORT_FILENAME);
+ progressDialog.show();
+ disposable = Completable.fromAction(() ->
+ DatabaseExporter.exportToStream(new FileOutputStream(backupDB), getContext()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ Snackbar.make(getView(), R.string.export_ok, Snackbar.LENGTH_LONG).show();
+ progressDialog.dismiss();
+ }, this::showExportErrorDialog);
+ }
+ }
+
+ private void importDatabase() {
+ if (Build.VERSION.SDK_INT >= 19) {
+ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+ intent.setType("*/*");
+ startActivityForResult(intent, REQUEST_CODE_RESTORE_DATABASE);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("*/*");
+ startActivityForResult(Intent.createChooser(intent,
+ getString(R.string.import_select_file)), REQUEST_CODE_RESTORE_DATABASE);
+ }
+ }
+
+ private void showDatabaseImportSuccessDialog() {
+ AlertDialog.Builder d = new AlertDialog.Builder(getContext());
+ d.setMessage(R.string.import_ok);
+ d.setCancelable(false);
+ d.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
+ Intent intent = new Intent(getContext(), SplashActivity.class);
+ ComponentName cn = intent.getComponent();
+ Intent mainIntent = Intent.makeRestartActivityTask(cn);
+ startActivity(mainIntent);
+ });
+ d.show();
+ }
+
+ private void showExportSuccessDialog(final String path, final Uri streamUri) {
+ final AlertDialog.Builder alert = new AlertDialog.Builder(getContext());
+ alert.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
alert.setTitle(R.string.export_success_title);
- alert.setMessage(message);
+ alert.setMessage(getContext().getString(R.string.export_success_sum, path));
alert.setPositiveButton(R.string.send_label, (dialog, which) -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.opml_export_label));
@@ -147,6 +208,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
}
private void showExportErrorDialog(final Throwable error) {
+ progressDialog.dismiss();
final AlertDialog.Builder alert = new AlertDialog.Builder(getContext())
.setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
alert.setTitle(R.string.export_error_label);
@@ -154,16 +216,35 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
alert.show();
}
- @SuppressLint("NewApi")
+ @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_OPML_EXPORT_PATH) {
- Uri uri = data.getData();
- export(new OpmlWriter(), uri);
+ if (resultCode != Activity.RESULT_OK || data == null) {
+ return;
}
+ Uri uri = data.getData();
- if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_HTML_EXPORT_PATH) {
- Uri uri = data.getData();
- export(new HtmlWriter(), uri);
+ if (requestCode == REQUEST_CODE_CHOOSE_OPML_EXPORT_PATH) {
+ exportWithWriter(new OpmlWriter(), uri);
+ } else if (requestCode == REQUEST_CODE_CHOOSE_HTML_EXPORT_PATH) {
+ exportWithWriter(new HtmlWriter(), uri);
+ } else if (requestCode == REQUEST_CODE_RESTORE_DATABASE) {
+ progressDialog.show();
+ disposable = Completable.fromAction(() -> DatabaseExporter.importBackup(uri, getContext()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ showDatabaseImportSuccessDialog();
+ progressDialog.dismiss();
+ }, this::showExportErrorDialog);
+ } else if (requestCode == REQUEST_CODE_BACKUP_DATABASE) {
+ progressDialog.show();
+ disposable = Completable.fromAction(() -> DatabaseExporter.exportToDocument(uri, getContext()))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(() -> {
+ Snackbar.make(getView(), R.string.export_ok, Snackbar.LENGTH_LONG).show();
+ progressDialog.dismiss();
+ }, this::showExportErrorDialog);
}
}
@@ -186,6 +267,6 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
// If we are using a SDK lower than API 21 or the implicit intent failed
// fallback to the legacy export process
- export(writer, null);
+ exportWithWriter(writer, null);
}
}
diff --git a/app/src/main/res/xml/preferences_import_export.xml b/app/src/main/res/xml/preferences_import_export.xml
index 6489ca493..7c576d194 100644
--- a/app/src/main/res/xml/preferences_import_export.xml
+++ b/app/src/main/res/xml/preferences_import_export.xml
@@ -14,18 +14,23 @@
android:summary="@string/opml_import_summary"/>
+
+
+
+
+
-
-
-
-
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java
new file mode 100644
index 000000000..af3d1206c
--- /dev/null
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java
@@ -0,0 +1,95 @@
+package de.danoeh.antennapod.core.storage;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import de.danoeh.antennapod.core.R;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+
+public class DatabaseExporter {
+ private static final String TAG = "DatabaseExporter";
+ private static final byte[] SQLITE3_MAGIC = "SQLite format 3\0".getBytes();
+
+ public static boolean validateDB(Uri inputUri, Context context) throws IOException {
+ try (InputStream inputStream = context.getContentResolver().openInputStream(inputUri)) {
+ byte[] magicBuf = new byte[SQLITE3_MAGIC.length];
+ if (inputStream.read(magicBuf) == magicBuf.length) {
+ return Arrays.equals(SQLITE3_MAGIC, magicBuf);
+ }
+ }
+ return false;
+ }
+
+ public static void exportToDocument(Uri uri, Context context) throws IOException {
+ ParcelFileDescriptor pfd = null;
+ FileOutputStream fileOutputStream = null;
+ try {
+ pfd = context.getContentResolver().openFileDescriptor(uri, "w");
+ fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
+ exportToStream(fileOutputStream, context);
+ } catch (IOException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw e;
+ } finally {
+ IOUtils.closeQuietly(fileOutputStream);
+
+ if (pfd != null) {
+ try {
+ pfd.close();
+ } catch (IOException e) {
+ Log.d(TAG, "Unable to close ParcelFileDescriptor");
+ }
+ }
+ }
+ }
+
+ public static void exportToStream(FileOutputStream outFileStream, Context context) throws IOException {
+ FileChannel src = null;
+ FileChannel dst = null;
+ try {
+ File currentDB = context.getDatabasePath(PodDBAdapter.DATABASE_NAME);
+
+ if (currentDB.exists()) {
+ src = new FileInputStream(currentDB).getChannel();
+ dst = outFileStream.getChannel();
+ dst.transferFrom(src, 0, src.size());
+ } else {
+ throw new IOException("Can not access current database");
+ }
+ } catch (IOException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw e;
+ } finally {
+ IOUtils.closeQuietly(src);
+ IOUtils.closeQuietly(dst);
+ }
+ }
+
+ public static void importBackup(Uri inputUri, Context context) throws IOException {
+ InputStream inputStream = null;
+ try {
+ if (!validateDB(inputUri, context)) {
+ throw new IOException(context.getString(R.string.import_bad_file));
+ }
+
+ File currentDB = context.getDatabasePath(PodDBAdapter.DATABASE_NAME);
+ inputStream = context.getContentResolver().openInputStream(inputUri);
+ FileUtils.copyInputStreamToFile(inputStream, currentDB);
+ } catch (IOException e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw e;
+ } finally {
+ IOUtils.closeQuietly(inputStream);
+ }
+ }
+}
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index 26ed87537..554e1489f 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -566,7 +566,9 @@
From local filesystem
OPML export
HTML export
- Exporting…
+ Database export
+ Database import
+ Please wait…
Export error
Export successful
The exported file was written to:\n\n%1$s