Move import/export to its own module (#6986)

Also clean up ImportExportPreferencesFragment a bit.
This commit is contained in:
ByteHamster 2024-03-11 23:10:09 +01:00 committed by GitHub
parent 5c98a33ed2
commit 2f3f1fd186
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 184 additions and 334 deletions

View File

@ -86,6 +86,7 @@ dependencies {
implementation project(':playback:base') implementation project(':playback:base')
implementation project(':playback:cast') implementation project(':playback:cast')
implementation project(':storage:database') implementation project(':storage:database')
implementation project(':storage:importexport')
implementation project(':storage:preferences') implementation project(':storage:preferences')
implementation project(':ui:app-start-intent') implementation project(':ui:app-start-intent')
implementation project(':ui:common') implementation project(':ui:common')

View File

@ -6,13 +6,15 @@ import org.xmlpull.v1.XmlSerializer;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedFunding; import de.danoeh.antennapod.model.feed.FeedFunding;
import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.parser.feed.namespace.PodcastIndex; import de.danoeh.antennapod.parser.feed.namespace.PodcastIndex;
import de.danoeh.antennapod.core.util.DateFormatter;
/** /**
* Creates RSS 2.0 feeds. See FeedGenerator for more information. * Creates RSS 2.0 feeds. See FeedGenerator for more information.
@ -98,7 +100,7 @@ public class Rss2Generator implements FeedGenerator {
} }
if (item.getPubDate() != null) { if (item.getPubDate() != null) {
xml.startTag(null, "pubDate"); xml.startTag(null, "pubDate");
xml.text(DateFormatter.formatRfc822Date(item.getPubDate())); xml.text(formatRfc822Date(item.getPubDate()));
xml.endTag(null, "pubDate"); xml.endTag(null, "pubDate");
} }
if ((flags & FEATURE_WRITE_GUID) != 0) { if ((flags & FEATURE_WRITE_GUID) != 0) {
@ -132,4 +134,9 @@ public class Rss2Generator implements FeedGenerator {
xml.endDocument(); xml.endDocument();
} }
private static String formatRfc822Date(Date date) {
SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US);
return format.format(date);
}
} }

View File

@ -26,14 +26,14 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
import de.danoeh.antennapod.core.export.opml.OpmlReader;
import de.danoeh.antennapod.core.preferences.ThemeSwitcher; import de.danoeh.antennapod.core.preferences.ThemeSwitcher;
import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.core.util.download.FeedUpdateManager;
import de.danoeh.antennapod.databinding.OpmlSelectionBinding; import de.danoeh.antennapod.databinding.OpmlSelectionBinding;
import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.importexport.OpmlElement;
import de.danoeh.antennapod.storage.importexport.OpmlReader;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;

View File

@ -1,68 +0,0 @@
package de.danoeh.antennapod.asynctask;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.documentfile.provider.DocumentFile;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.core.storage.DBReader;
import io.reactivex.Observable;
/**
* Writes an OPML file into the user selected export directory in the background.
*/
public class DocumentFileExportWorker {
private final @NonNull ExportWriter exportWriter;
private @NonNull Context context;
private @NonNull Uri outputFileUri;
public DocumentFileExportWorker(@NonNull ExportWriter exportWriter, @NonNull Context context, @NonNull Uri outputFileUri) {
this.exportWriter = exportWriter;
this.context = context;
this.outputFileUri = outputFileUri;
}
public Observable<DocumentFile> exportObservable() {
DocumentFile output = DocumentFile.fromSingleUri(context, outputFileUri);
return Observable.create(subscriber -> {
OutputStream outputStream = null;
OutputStreamWriter writer = null;
try {
Uri uri = output.getUri();
outputStream = context.getContentResolver().openOutputStream(uri, "wt");
if (outputStream == null) {
throw new IOException();
}
writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"));
exportWriter.writeDocument(DBReader.getFeedList(), writer, context);
subscriber.onNext(output);
} catch (IOException e) {
subscriber.onError(e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
subscriber.onError(e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
subscriber.onError(e);
}
}
subscriber.onComplete();
}
});
}
}

View File

@ -1,68 +0,0 @@
package de.danoeh.antennapod.asynctask;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import io.reactivex.Observable;
/**
* Writes an OPML file into the export directory in the background.
*/
public class ExportWorker {
private static final String EXPORT_DIR = "export/";
private static final String TAG = "ExportWorker";
private static final String DEFAULT_OUTPUT_NAME = "antennapod-feeds";
private final @NonNull ExportWriter exportWriter;
private final @NonNull File output;
private final Context context;
public ExportWorker(@NonNull ExportWriter exportWriter, Context context) {
this(exportWriter, new File(UserPreferences.getDataFolder(EXPORT_DIR),
DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()), context);
}
private ExportWorker(@NonNull ExportWriter exportWriter, @NonNull File output, Context context) {
this.exportWriter = exportWriter;
this.output = output;
this.context = context;
}
public Observable<File> exportObservable() {
if (output.exists()) {
boolean success = output.delete();
Log.w(TAG, "Overwriting previously exported file: " + success);
}
return Observable.create(subscriber -> {
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(output), Charset.forName("UTF-8"));
exportWriter.writeDocument(DBReader.getFeedList(), writer, context);
subscriber.onNext(output);
} catch (IOException e) {
subscriber.onError(e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
subscriber.onError(e);
}
}
subscriber.onComplete();
}
});
}
}

View File

@ -16,6 +16,7 @@ import androidx.activity.result.contract.ActivityResultContracts.GetContent;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.documentfile.provider.DocumentFile;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import androidx.core.app.ShareCompat; import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider; import androidx.core.content.FileProvider;
@ -25,13 +26,15 @@ import de.danoeh.antennapod.PodcastApp;
import de.danoeh.antennapod.R; import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.OpmlImportActivity; import de.danoeh.antennapod.activity.OpmlImportActivity;
import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.asynctask.DocumentFileExportWorker; import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.asynctask.ExportWorker; import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.core.export.favorites.FavoritesWriter; import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.core.export.html.HtmlWriter; import de.danoeh.antennapod.storage.importexport.DatabaseExporter;
import de.danoeh.antennapod.core.export.opml.OpmlWriter; import de.danoeh.antennapod.storage.importexport.FavoritesWriter;
import de.danoeh.antennapod.core.storage.DatabaseExporter; import de.danoeh.antennapod.storage.importexport.HtmlWriter;
import de.danoeh.antennapod.storage.importexport.OpmlWriter;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
@ -39,8 +42,14 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Locale; import java.util.Locale;
public class ImportExportPreferencesFragment extends PreferenceFragmentCompat { public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
@ -57,18 +66,29 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
private static final String CONTENT_TYPE_HTML = "text/html"; private static final String CONTENT_TYPE_HTML = "text/html";
private static final String DEFAULT_FAVORITES_OUTPUT_NAME = "antennapod-favorites-%s.html"; private static final String DEFAULT_FAVORITES_OUTPUT_NAME = "antennapod-favorites-%s.html";
private static final String DATABASE_EXPORT_FILENAME = "AntennaPodBackup-%s.db"; private static final String DATABASE_EXPORT_FILENAME = "AntennaPodBackup-%s.db";
private final ActivityResultLauncher<Intent> chooseOpmlExportPathLauncher = private final ActivityResultLauncher<Intent> chooseOpmlExportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::chooseOpmlExportPathResult); registerForActivityResult(new StartActivityForResult(),
result -> exportToDocument(result, Export.OPML));
private final ActivityResultLauncher<Intent> chooseHtmlExportPathLauncher = private final ActivityResultLauncher<Intent> chooseHtmlExportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::chooseHtmlExportPathResult); registerForActivityResult(new StartActivityForResult(),
result -> exportToDocument(result, Export.HTML));
private final ActivityResultLauncher<Intent> chooseFavoritesExportPathLauncher = private final ActivityResultLauncher<Intent> chooseFavoritesExportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::chooseFavoritesExportPathResult); registerForActivityResult(new StartActivityForResult(),
result -> exportToDocument(result, Export.FAVORITES));
private final ActivityResultLauncher<Intent> restoreDatabaseLauncher = private final ActivityResultLauncher<Intent> restoreDatabaseLauncher =
registerForActivityResult(new StartActivityForResult(), this::restoreDatabaseResult); registerForActivityResult(new StartActivityForResult(), this::restoreDatabaseResult);
private final ActivityResultLauncher<String> backupDatabaseLauncher = private final ActivityResultLauncher<String> backupDatabaseLauncher =
registerForActivityResult(new BackupDatabase(), this::backupDatabaseResult); registerForActivityResult(new BackupDatabase(), this::backupDatabaseResult);
private final ActivityResultLauncher<String> chooseOpmlImportPathLauncher = private final ActivityResultLauncher<String> chooseOpmlImportPathLauncher =
registerForActivityResult(new GetContent(), this::chooseOpmlImportPathResult); registerForActivityResult(new GetContent(), uri -> {
if (uri != null) {
final Intent intent = new Intent(getContext(), OpmlImportActivity.class);
intent.setData(uri);
startActivity(intent);
}
});
private Disposable disposable; private Disposable disposable;
private ProgressDialog progressDialog; private ProgressDialog progressDialog;
@ -95,20 +115,16 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
} }
} }
private String dateStampFilename(String fname) {
return String.format(fname, new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date()));
}
private void setupStorageScreen() { private void setupStorageScreen() {
findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener(
preference -> { preference -> {
openExportPathPicker(Export.OPML, chooseOpmlExportPathLauncher, new OpmlWriter()); openExportPathPicker(Export.OPML, chooseOpmlExportPathLauncher);
return true; return true;
} }
); );
findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener( findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener(
preference -> { preference -> {
openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher, new HtmlWriter()); openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher);
return true; return true;
}); });
findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener( findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener(
@ -132,32 +148,13 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
}); });
findPreference(PREF_FAVORITE_EXPORT).setOnPreferenceClickListener( findPreference(PREF_FAVORITE_EXPORT).setOnPreferenceClickListener(
preference -> { preference -> {
openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, new FavoritesWriter()); openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher);
return true; return true;
}); });
} }
private void exportWithWriter(ExportWriter exportWriter, Uri uri, Export exportType) { private String dateStampFilename(String fname) {
Context context = getActivity(); return String.format(fname, new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date()));
progressDialog.show();
if (uri == null) {
Observable<File> observable = new ExportWorker(exportWriter, getContext()).exportObservable();
disposable = observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(output -> {
Uri fileUri = FileProvider.getUriForFile(context.getApplicationContext(),
context.getString(R.string.provider_authority), output);
showExportSuccessSnackbar(fileUri, exportType.contentType);
}, this::showExportErrorDialog, progressDialog::dismiss);
} else {
DocumentFileExportWorker worker = new DocumentFileExportWorker(exportWriter, context, uri);
disposable = worker.exportObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(output ->
showExportSuccessSnackbar(output.getUri(), exportType.contentType),
this::showExportErrorDialog, progressDialog::dismiss);
}
} }
private void exportDatabase() { private void exportDatabase() {
@ -211,30 +208,6 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
alert.show(); alert.show();
} }
private void chooseOpmlExportPathResult(final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
return;
}
final Uri uri = result.getData().getData();
exportWithWriter(new OpmlWriter(), uri, Export.OPML);
}
private void chooseHtmlExportPathResult(final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
return;
}
final Uri uri = result.getData().getData();
exportWithWriter(new HtmlWriter(), uri, Export.HTML);
}
private void chooseFavoritesExportPathResult(final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
return;
}
final Uri uri = result.getData().getData();
exportWithWriter(new FavoritesWriter(), uri, Export.FAVORITES);
}
private void restoreDatabaseResult(final ActivityResult result) { private void restoreDatabaseResult(final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) { if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
return; return;
@ -264,16 +237,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
}, this::showExportErrorDialog); }, this::showExportErrorDialog);
} }
private void chooseOpmlImportPathResult(final Uri uri) { private void openExportPathPicker(Export exportType, ActivityResultLauncher<Intent> result) {
if (uri == null) {
return;
}
final Intent intent = new Intent(getContext(), OpmlImportActivity.class);
intent.setData(uri);
startActivity(intent);
}
private void openExportPathPicker(Export exportType, ActivityResultLauncher<Intent> result, ExportWriter writer) {
String title = dateStampFilename(exportType.outputNameTemplate); String title = dateStampFilename(exportType.outputNameTemplate);
Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT)
@ -292,7 +256,78 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat {
// If we are using a SDK lower than API 21 or the implicit intent failed // If we are using a SDK lower than API 21 or the implicit intent failed
// fallback to the legacy export process // fallback to the legacy export process
exportWithWriter(writer, null, exportType); File output = new File(UserPreferences.getDataFolder("export/"), title);
exportToFile(exportType, output);
}
private void exportToFile(Export exportType, File output) {
progressDialog.show();
disposable = Observable.create(
subscriber -> {
if (output.exists()) {
boolean success = output.delete();
Log.w(TAG, "Overwriting previously exported file: " + success);
}
try (FileOutputStream fileOutputStream = new FileOutputStream(output)) {
writeToStream(fileOutputStream, exportType);
subscriber.onNext(output);
} catch (IOException e) {
subscriber.onError(e);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(outputFile -> {
progressDialog.dismiss();
Uri fileUri = FileProvider.getUriForFile(getActivity().getApplicationContext(),
getString(R.string.provider_authority), output);
showExportSuccessSnackbar(fileUri, exportType.contentType);
}, this::showExportErrorDialog, progressDialog::dismiss);
}
private void exportToDocument(final ActivityResult result, Export exportType) {
if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) {
return;
}
progressDialog.show();
DocumentFile output = DocumentFile.fromSingleUri(getContext(), result.getData().getData());
disposable = Observable.create(
subscriber -> {
try (OutputStream outputStream = getContext().getContentResolver()
.openOutputStream(output.getUri(), "wt")) {
writeToStream(outputStream, exportType);
subscriber.onNext(output);
} catch (IOException e) {
subscriber.onError(e);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(ignore -> {
progressDialog.dismiss();
showExportSuccessSnackbar(output.getUri(), exportType.contentType);
}, this::showExportErrorDialog, progressDialog::dismiss);
}
private void writeToStream(OutputStream outputStream, Export type) throws IOException {
try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, Charset.forName("UTF-8"))) {
switch (type) {
case HTML:
HtmlWriter.writeDocument(DBReader.getFeedList(), writer, getContext());
break;
case OPML:
OpmlWriter.writeDocument(DBReader.getFeedList(), writer);
break;
case FAVORITES:
List<FeedItem> allFavorites = DBReader.getEpisodes(0, Integer.MAX_VALUE,
new FeedItemFilter(FeedItemFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD);
FavoritesWriter.writeDocument(allFavorites, writer, getContext());
break;
default:
showExportErrorDialog(new Exception("Invalid export type"));
break;
}
}
} }
private static class BackupDatabase extends ActivityResultContracts.CreateDocument { private static class BackupDatabase extends ActivityResultContracts.CreateDocument {

View File

@ -35,6 +35,7 @@ dependencies {
implementation project(':playback:base') implementation project(':playback:base')
implementation project(':playback:cast') implementation project(':playback:cast')
implementation project(':storage:database') implementation project(':storage:database')
implementation project(':storage:importexport')
implementation project(':storage:preferences') implementation project(':storage:preferences')
implementation project(':ui:app-start-intent') implementation project(':ui:app-start-intent')
implementation project(':ui:common') implementation project(':ui:common')

View File

@ -10,6 +10,9 @@ import android.util.Log;
import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBTasks;
import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.core.util.download.FeedUpdateManager;
import de.danoeh.antennapod.storage.importexport.OpmlElement;
import de.danoeh.antennapod.storage.importexport.OpmlReader;
import de.danoeh.antennapod.storage.importexport.OpmlWriter;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
@ -31,9 +34,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import de.danoeh.antennapod.core.export.opml.OpmlElement;
import de.danoeh.antennapod.core.export.opml.OpmlReader;
import de.danoeh.antennapod.core.export.opml.OpmlWriter;
import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBReader;
@ -81,7 +81,7 @@ public class OpmlBackupAgent extends BackupAgentHelper {
try { try {
// Write OPML // Write OPML
new OpmlWriter().writeDocument(DBReader.getFeedList(), writer, mContext); OpmlWriter.writeDocument(DBReader.getFeedList(), writer);
// Compare checksum of new and old file to see if we need to perform a backup at all // Compare checksum of new and old file to see if we need to perform a backup at all
if (digester != null) { if (digester != null) {

View File

@ -1,25 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.danoeh.antennapod.core.export;
public class CommonSymbols {
public static final String HEAD = "head";
public static final String BODY = "body";
public static final String TITLE = "title";
public static final String XML_FEATURE_INDENT_OUTPUT = "http://xmlpull.org/v1/doc/features.html#indent-output";
}

View File

@ -1,31 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.danoeh.antennapod.core.export;
import android.content.Context;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import de.danoeh.antennapod.model.feed.Feed;
public interface ExportWriter {
void writeDocument(List<Feed> feeds, Writer writer, Context context)
throws IllegalArgumentException, IllegalStateException, IOException;
String fileExtension();
}

View File

@ -3,11 +3,9 @@ package de.danoeh.antennapod.core.util;
import android.content.Context; import android.content.Context;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.Locale;
/** /**
* Formats dates. * Formats dates.
@ -17,11 +15,6 @@ public class DateFormatter {
} }
public static String formatRfc822Date(Date date) {
SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US);
return format.format(date);
}
public static String formatAbbrev(final Context context, final Date date) { public static String formatAbbrev(final Context context, final Date date) {
if (date == null) { if (date == null) {
return ""; return "";

View File

@ -34,6 +34,7 @@ include ':playback:base'
include ':playback:cast' include ':playback:cast'
include ':storage:database' include ':storage:database'
include ':storage:importexport'
include ':storage:preferences' include ':storage:preferences'
include ':ui:app-start-intent' include ':ui:app-start-intent'

View File

@ -0,0 +1,3 @@
# :storage:importexport
Import/Export of the AntennaPod database.

View File

@ -0,0 +1,21 @@
plugins {
id("com.android.library")
}
apply from: "../../common.gradle"
android {
namespace "de.danoeh.antennapod.storage.importexport"
}
dependencies {
implementation project(':storage:database')
implementation project(':storage:preferences')
implementation project(':ui:i18n')
implementation project(':model')
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
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'
}

View File

@ -16,6 +16,7 @@
background-image: linear-gradient(180deg, #0f9cff, #0682ff); background-image: linear-gradient(180deg, #0f9cff, #0682ff);
text-align: center; text-align: center;
padding: 10px; padding: 10px;
min-height: 100%;
} }
h1 { h1 {
color: #fff; color: #fff;

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.storage; package de.danoeh.antennapod.storage.importexport;
import android.content.Context; import android.content.Context;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
@ -7,7 +7,6 @@ import android.net.Uri;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.text.format.Formatter; import android.text.format.Formatter;
import android.util.Log; import android.util.Log;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.storage.database.PodDBAdapter; import de.danoeh.antennapod.storage.database.PodDBAdapter;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;

View File

@ -1,9 +1,8 @@
package de.danoeh.antennapod.core.export.favorites; package de.danoeh.antennapod.storage.importexport;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import java.io.IOException; import java.io.IOException;
@ -14,21 +13,17 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.model.feed.SortOrder;
/** Writes saved favorites to file. */ /** Writes saved favorites to file. */
public class FavoritesWriter implements ExportWriter { public class FavoritesWriter {
private static final String TAG = "FavoritesWriter"; private static final String TAG = "FavoritesWriter";
private static final String FAVORITE_TEMPLATE = "html-export-favorites-item-template.html"; private static final String FAVORITE_TEMPLATE = "html-export-favorites-item-template.html";
private static final String FEED_TEMPLATE = "html-export-feed-template.html"; private static final String FEED_TEMPLATE = "html-export-feed-template.html";
private static final String UTF_8 = "UTF-8"; private static final String UTF_8 = "UTF-8";
@Override public static void writeDocument(List<FeedItem> allFavorites, Writer writer, Context context)
public void writeDocument(List<Feed> feeds, Writer writer, Context context)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
Log.d(TAG, "Starting to write document"); Log.d(TAG, "Starting to write document");
@ -43,8 +38,6 @@ public class FavoritesWriter implements ExportWriter {
InputStream feedTemplateStream = context.getAssets().open(FEED_TEMPLATE); InputStream feedTemplateStream = context.getAssets().open(FEED_TEMPLATE);
String feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8); String feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8);
List<FeedItem> allFavorites = DBReader.getEpisodes(0, Integer.MAX_VALUE,
new FeedItemFilter(FeedItemFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD);
Map<Long, List<FeedItem>> favoriteByFeed = getFeedMap(allFavorites); Map<Long, List<FeedItem>> favoriteByFeed = getFeedMap(allFavorites);
writer.append(templateParts[0]); writer.append(templateParts[0]);
@ -72,7 +65,7 @@ public class FavoritesWriter implements ExportWriter {
* @param favoritesList {@code List} of all favorite episodes. * @param favoritesList {@code List} of all favorite episodes.
* @return A {@code Map} favorite episodes, keyed by feed ID. * @return A {@code Map} favorite episodes, keyed by feed ID.
*/ */
private Map<Long, List<FeedItem>> getFeedMap(List<FeedItem> favoritesList) { private static Map<Long, List<FeedItem>> getFeedMap(List<FeedItem> favoritesList) {
Map<Long, List<FeedItem>> feedMap = new TreeMap<>(); Map<Long, List<FeedItem>> feedMap = new TreeMap<>();
for (FeedItem item : favoritesList) { for (FeedItem item : favoritesList) {
@ -89,7 +82,7 @@ public class FavoritesWriter implements ExportWriter {
return feedMap; return feedMap;
} }
private void writeFeed(Writer writer, Feed feed, String feedTemplate) throws IOException { private static void writeFeed(Writer writer, Feed feed, String feedTemplate) throws IOException {
String feedInfo = feedTemplate String feedInfo = feedTemplate
.replace("{FEED_IMG}", feed.getImageUrl()) .replace("{FEED_IMG}", feed.getImageUrl())
.replace("{FEED_TITLE}", feed.getTitle()) .replace("{FEED_TITLE}", feed.getTitle())
@ -99,7 +92,7 @@ public class FavoritesWriter implements ExportWriter {
writer.append(feedInfo); writer.append(feedInfo);
} }
private void writeFavoriteItem(Writer writer, FeedItem item, String favoriteTemplate) throws IOException { private static void writeFavoriteItem(Writer writer, FeedItem item, String favoriteTemplate) throws IOException {
String favItem = favoriteTemplate.replace("{FAV_TITLE}", item.getTitle().trim()); String favItem = favoriteTemplate.replace("{FAV_TITLE}", item.getTitle().trim());
if (item.getLink() != null) { if (item.getLink() != null) {
favItem = favItem.replace("{FAV_WEBSITE}", item.getLink()); favItem = favItem.replace("{FAV_WEBSITE}", item.getLink());
@ -114,9 +107,4 @@ public class FavoritesWriter implements ExportWriter {
writer.append(favItem); writer.append(favItem);
} }
@Override
public String fileExtension() {
return "html";
}
} }

View File

@ -1,8 +1,7 @@
package de.danoeh.antennapod.core.export.html; package de.danoeh.antennapod.storage.importexport;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.Feed;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -11,15 +10,14 @@ import java.util.List;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
/** Writes HTML documents. */ /** Writes HTML documents. */
public class HtmlWriter implements ExportWriter { public class HtmlWriter {
private static final String TAG = "HtmlWriter"; private static final String TAG = "HtmlWriter";
/** /**
* Takes a list of feeds and a writer and writes those into an HTML * Takes a list of feeds and a writer and writes those into an HTML
* document. * document.
*/ */
@Override public static void writeDocument(List<Feed> feeds, Writer writer, Context context)
public void writeDocument(List<Feed> feeds, Writer writer, Context context)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
Log.d(TAG, "Starting to write document"); Log.d(TAG, "Starting to write document");
@ -43,9 +41,4 @@ public class HtmlWriter implements ExportWriter {
writer.append(templateParts[1]); writer.append(templateParts[1]);
Log.d(TAG, "Finished writing document"); Log.d(TAG, "Finished writing document");
} }
public String fileExtension() {
return "html";
}
} }

View File

@ -1,4 +1,4 @@
package de.danoeh.antennapod.core.export.opml; package de.danoeh.antennapod.storage.importexport;
/** /**
* Represents a single feed in an OPML file. * Represents a single feed in an OPML file.

View File

@ -1,7 +1,9 @@
package de.danoeh.antennapod.core.export.opml; package de.danoeh.antennapod.storage.importexport;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import de.danoeh.antennapod.storage.preferences.BuildConfig;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlPullParserFactory;
@ -10,8 +12,6 @@ import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import java.util.ArrayList; import java.util.ArrayList;
import de.danoeh.antennapod.core.BuildConfig;
/** /**
* Reads OPML documents. * Reads OPML documents.
*/ */
@ -53,7 +53,7 @@ public class OpmlReader {
OpmlElement element = new OpmlElement(); OpmlElement element = new OpmlElement();
final String title = xpp.getAttributeValue(null, OpmlSymbols.TITLE); final String title = xpp.getAttributeValue(null, OpmlSymbols.TITLE);
if (title != null) { if (!TextUtils.isEmpty(title)) {
Log.i(TAG, "Using title: " + title); Log.i(TAG, "Using title: " + title);
element.setText(title); element.setText(title);
} else { } else {
@ -63,8 +63,8 @@ public class OpmlReader {
element.setXmlUrl(xpp.getAttributeValue(null, OpmlSymbols.XMLURL)); element.setXmlUrl(xpp.getAttributeValue(null, OpmlSymbols.XMLURL));
element.setHtmlUrl(xpp.getAttributeValue(null, OpmlSymbols.HTMLURL)); element.setHtmlUrl(xpp.getAttributeValue(null, OpmlSymbols.HTMLURL));
element.setType(xpp.getAttributeValue(null, OpmlSymbols.TYPE)); element.setType(xpp.getAttributeValue(null, OpmlSymbols.TYPE));
if (element.getXmlUrl() != null) { if (!TextUtils.isEmpty(element.getXmlUrl())) {
if (element.getText() == null) { if (TextUtils.isEmpty(element.getText())) {
Log.i(TAG, "Opml element has no text attribute."); Log.i(TAG, "Opml element has no text attribute.");
element.setText(element.getXmlUrl()); element.setText(element.getXmlUrl());
} }

View File

@ -1,12 +1,14 @@
package de.danoeh.antennapod.core.export.opml; package de.danoeh.antennapod.storage.importexport;
import de.danoeh.antennapod.core.export.CommonSymbols;
/** /**
* Contains symbols for reading and writing OPML documents. * Contains symbols for reading and writing OPML documents.
*/ */
final class OpmlSymbols extends CommonSymbols { final class OpmlSymbols {
public static final String XML_FEATURE_INDENT_OUTPUT = "http://xmlpull.org/v1/doc/features.html#indent-output";
public static final String HEAD = "head";
public static final String BODY = "body";
public static final String TITLE = "title";
public static final String OPML = "opml"; public static final String OPML = "opml";
static final String OUTLINE = "outline"; static final String OUTLINE = "outline";
static final String TEXT = "text"; static final String TEXT = "text";
@ -19,5 +21,4 @@ final class OpmlSymbols extends CommonSymbols {
private OpmlSymbols() { private OpmlSymbols() {
} }
} }

View File

@ -1,22 +1,21 @@
package de.danoeh.antennapod.core.export.opml; package de.danoeh.antennapod.storage.importexport;
import android.content.Context;
import android.util.Log; import android.util.Log;
import android.util.Xml; import android.util.Xml;
import de.danoeh.antennapod.core.util.DateFormatter;
import org.xmlpull.v1.XmlSerializer; import org.xmlpull.v1.XmlSerializer;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
import de.danoeh.antennapod.core.export.ExportWriter;
import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.Feed;
/** Writes OPML documents. */ /** Writes OPML documents. */
public class OpmlWriter implements ExportWriter { public class OpmlWriter {
private static final String TAG = "OpmlWriter"; private static final String TAG = "OpmlWriter";
private static final String ENCODING = "UTF-8"; private static final String ENCODING = "UTF-8";
@ -27,8 +26,7 @@ public class OpmlWriter implements ExportWriter {
* Takes a list of feeds and a writer and writes those into an OPML * Takes a list of feeds and a writer and writes those into an OPML
* document. * document.
*/ */
@Override public static void writeDocument(List<Feed> feeds, Writer writer)
public void writeDocument(List<Feed> feeds, Writer writer, Context context)
throws IllegalArgumentException, IllegalStateException, IOException { throws IllegalArgumentException, IllegalStateException, IOException {
Log.d(TAG, "Starting to write document"); Log.d(TAG, "Starting to write document");
XmlSerializer xs = Xml.newSerializer(); XmlSerializer xs = Xml.newSerializer();
@ -44,7 +42,7 @@ public class OpmlWriter implements ExportWriter {
xs.text(OPML_TITLE); xs.text(OPML_TITLE);
xs.endTag(null, OpmlSymbols.TITLE); xs.endTag(null, OpmlSymbols.TITLE);
xs.startTag(null, OpmlSymbols.DATE_CREATED); xs.startTag(null, OpmlSymbols.DATE_CREATED);
xs.text(DateFormatter.formatRfc822Date(new Date())); xs.text(formatRfc822Date(new Date()));
xs.endTag(null, OpmlSymbols.DATE_CREATED); xs.endTag(null, OpmlSymbols.DATE_CREATED);
xs.endTag(null, OpmlSymbols.HEAD); xs.endTag(null, OpmlSymbols.HEAD);
@ -68,8 +66,8 @@ public class OpmlWriter implements ExportWriter {
Log.d(TAG, "Finished writing document"); Log.d(TAG, "Finished writing document");
} }
public String fileExtension() { private static String formatRfc822Date(Date date) {
return "opml"; SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US);
return format.format(date);
} }
} }