diff --git a/app/build.gradle b/app/build.gradle index 06b387e7b..5553e174b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,6 +86,7 @@ dependencies { implementation project(':playback:base') implementation project(':playback:cast') implementation project(':storage:database') + implementation project(':storage:importexport') implementation project(':storage:preferences') implementation project(':ui:app-start-intent') implementation project(':ui:common') diff --git a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java index 6b85f3bf8..ec4a58e33 100644 --- a/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java +++ b/app/src/androidTest/java/de/test/antennapod/util/syndication/feedgenerator/Rss2Generator.java @@ -6,13 +6,15 @@ import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.OutputStream; +import java.text.SimpleDateFormat; 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.FeedFunding; import de.danoeh.antennapod.model.feed.FeedItem; 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. @@ -98,7 +100,7 @@ public class Rss2Generator implements FeedGenerator { } if (item.getPubDate() != null) { xml.startTag(null, "pubDate"); - xml.text(DateFormatter.formatRfc822Date(item.getPubDate())); + xml.text(formatRfc822Date(item.getPubDate())); xml.endTag(null, "pubDate"); } if ((flags & FEATURE_WRITE_GUID) != 0) { @@ -132,4 +134,9 @@ public class Rss2Generator implements FeedGenerator { 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); + } } diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java index 3f1c17cdc..caafe989d 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java @@ -26,14 +26,14 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; 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.storage.DBTasks; import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.databinding.OpmlSelectionBinding; 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.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java deleted file mode 100644 index 8acfcb58f..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java +++ /dev/null @@ -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 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(); - } - }); - } - -} diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java deleted file mode 100644 index 97a5f157b..000000000 --- a/app/src/main/java/de/danoeh/antennapod/asynctask/ExportWorker.java +++ /dev/null @@ -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 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(); - } - }); - } - -} 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 71ba326dd..9191825aa 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 @@ -16,6 +16,7 @@ import androidx.activity.result.contract.ActivityResultContracts.GetContent; import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import androidx.annotation.NonNull; import androidx.annotation.StringRes; +import androidx.documentfile.provider.DocumentFile; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.core.app.ShareCompat; import androidx.core.content.FileProvider; @@ -25,13 +26,15 @@ import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.OpmlImportActivity; import de.danoeh.antennapod.activity.PreferenceActivity; -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.favorites.FavoritesWriter; -import de.danoeh.antennapod.core.export.html.HtmlWriter; -import de.danoeh.antennapod.core.export.opml.OpmlWriter; -import de.danoeh.antennapod.core.storage.DatabaseExporter; +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.DatabaseExporter; +import de.danoeh.antennapod.storage.importexport.FavoritesWriter; +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.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -39,8 +42,14 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; 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.util.Date; +import java.util.List; import java.util.Locale; 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 DEFAULT_FAVORITES_OUTPUT_NAME = "antennapod-favorites-%s.html"; private static final String DATABASE_EXPORT_FILENAME = "AntennaPodBackup-%s.db"; + private final ActivityResultLauncher chooseOpmlExportPathLauncher = - registerForActivityResult(new StartActivityForResult(), this::chooseOpmlExportPathResult); + registerForActivityResult(new StartActivityForResult(), + result -> exportToDocument(result, Export.OPML)); private final ActivityResultLauncher chooseHtmlExportPathLauncher = - registerForActivityResult(new StartActivityForResult(), this::chooseHtmlExportPathResult); + registerForActivityResult(new StartActivityForResult(), + result -> exportToDocument(result, Export.HTML)); private final ActivityResultLauncher chooseFavoritesExportPathLauncher = - registerForActivityResult(new StartActivityForResult(), this::chooseFavoritesExportPathResult); + registerForActivityResult(new StartActivityForResult(), + result -> exportToDocument(result, Export.FAVORITES)); private final ActivityResultLauncher restoreDatabaseLauncher = registerForActivityResult(new StartActivityForResult(), this::restoreDatabaseResult); private final ActivityResultLauncher backupDatabaseLauncher = registerForActivityResult(new BackupDatabase(), this::backupDatabaseResult); private final ActivityResultLauncher 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 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() { findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( preference -> { - openExportPathPicker(Export.OPML, chooseOpmlExportPathLauncher, new OpmlWriter()); + openExportPathPicker(Export.OPML, chooseOpmlExportPathLauncher); return true; } ); findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener( preference -> { - openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher, new HtmlWriter()); + openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher); return true; }); findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener( @@ -132,32 +148,13 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat { }); findPreference(PREF_FAVORITE_EXPORT).setOnPreferenceClickListener( preference -> { - openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, new FavoritesWriter()); + openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher); return true; }); } - private void exportWithWriter(ExportWriter exportWriter, Uri uri, Export exportType) { - Context context = getActivity(); - progressDialog.show(); - if (uri == null) { - Observable 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 String dateStampFilename(String fname) { + return String.format(fname, new SimpleDateFormat("yyyy-MM-dd", Locale.US).format(new Date())); } private void exportDatabase() { @@ -211,30 +208,6 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat { 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) { if (result.getResultCode() != Activity.RESULT_OK || result.getData() == null) { return; @@ -264,16 +237,7 @@ public class ImportExportPreferencesFragment extends PreferenceFragmentCompat { }, this::showExportErrorDialog); } - private void chooseOpmlImportPathResult(final Uri uri) { - if (uri == null) { - return; - } - final Intent intent = new Intent(getContext(), OpmlImportActivity.class); - intent.setData(uri); - startActivity(intent); - } - - private void openExportPathPicker(Export exportType, ActivityResultLauncher result, ExportWriter writer) { + private void openExportPathPicker(Export exportType, ActivityResultLauncher result) { String title = dateStampFilename(exportType.outputNameTemplate); 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 // 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 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 { diff --git a/core/build.gradle b/core/build.gradle index b36d87129..4385b7cd6 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation project(':playback:base') implementation project(':playback:cast') implementation project(':storage:database') + implementation project(':storage:importexport') implementation project(':storage:preferences') implementation project(':ui:app-start-intent') implementation project(':ui:common') diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java index 2058d5b2f..a78ae71a3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -10,6 +10,9 @@ import android.util.Log; import de.danoeh.antennapod.core.storage.DBTasks; 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.xmlpull.v1.XmlPullParserException; @@ -31,9 +34,6 @@ import java.util.ArrayList; import java.util.Arrays; 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.core.storage.DBReader; @@ -81,7 +81,7 @@ public class OpmlBackupAgent extends BackupAgentHelper { try { // 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 if (digester != null) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/CommonSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/export/CommonSymbols.java deleted file mode 100644 index 2ce340328..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/export/CommonSymbols.java +++ /dev/null @@ -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"; - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/ExportWriter.java b/core/src/main/java/de/danoeh/antennapod/core/export/ExportWriter.java deleted file mode 100644 index e60609569..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/export/ExportWriter.java +++ /dev/null @@ -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 feeds, Writer writer, Context context) - throws IllegalArgumentException, IllegalStateException, IOException; - - String fileExtension(); - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java index dc7ed4508..c67e13db3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateFormatter.java @@ -3,11 +3,9 @@ package de.danoeh.antennapod.core.util; import android.content.Context; import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; -import java.util.Locale; /** * 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) { if (date == null) { return ""; diff --git a/settings.gradle b/settings.gradle index ed7f5d8d2..5e7e973f4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,6 +34,7 @@ include ':playback:base' include ':playback:cast' include ':storage:database' +include ':storage:importexport' include ':storage:preferences' include ':ui:app-start-intent' diff --git a/storage/importexport/README.md b/storage/importexport/README.md new file mode 100644 index 000000000..925d537d3 --- /dev/null +++ b/storage/importexport/README.md @@ -0,0 +1,3 @@ +# :storage:importexport + +Import/Export of the AntennaPod database. diff --git a/storage/importexport/build.gradle b/storage/importexport/build.gradle new file mode 100644 index 000000000..ddbbd1951 --- /dev/null +++ b/storage/importexport/build.gradle @@ -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' +} diff --git a/core/src/main/assets/html-export-favorites-item-template.html b/storage/importexport/src/main/assets/html-export-favorites-item-template.html similarity index 100% rename from core/src/main/assets/html-export-favorites-item-template.html rename to storage/importexport/src/main/assets/html-export-favorites-item-template.html diff --git a/core/src/main/assets/html-export-feed-template.html b/storage/importexport/src/main/assets/html-export-feed-template.html similarity index 100% rename from core/src/main/assets/html-export-feed-template.html rename to storage/importexport/src/main/assets/html-export-feed-template.html diff --git a/core/src/main/assets/html-export-template.html b/storage/importexport/src/main/assets/html-export-template.html similarity index 95% rename from core/src/main/assets/html-export-template.html rename to storage/importexport/src/main/assets/html-export-template.html index e4d3ffd31..9137ccd65 100644 --- a/core/src/main/assets/html-export-template.html +++ b/storage/importexport/src/main/assets/html-export-template.html @@ -16,6 +16,7 @@ background-image: linear-gradient(180deg, #0f9cff, #0682ff); text-align: center; padding: 10px; + min-height: 100%; } h1 { color: #fff; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/DatabaseExporter.java similarity index 98% rename from core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java rename to storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/DatabaseExporter.java index d4a863b8b..d152ba34a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java +++ b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/DatabaseExporter.java @@ -1,4 +1,4 @@ -package de.danoeh.antennapod.core.storage; +package de.danoeh.antennapod.storage.importexport; import android.content.Context; import android.database.sqlite.SQLiteDatabase; @@ -7,7 +7,6 @@ import android.net.Uri; import android.os.ParcelFileDescriptor; import android.text.format.Formatter; import android.util.Log; -import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.storage.database.PodDBAdapter; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/FavoritesWriter.java similarity index 79% rename from core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java rename to storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/FavoritesWriter.java index 649ec815a..280cd1028 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/favorites/FavoritesWriter.java +++ b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/FavoritesWriter.java @@ -1,9 +1,8 @@ -package de.danoeh.antennapod.core.export.favorites; +package de.danoeh.antennapod.storage.importexport; import android.content.Context; import android.util.Log; -import de.danoeh.antennapod.model.feed.FeedItemFilter; import org.apache.commons.io.IOUtils; import java.io.IOException; @@ -14,21 +13,17 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.model.feed.Feed; 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. */ -public class FavoritesWriter implements ExportWriter { +public class FavoritesWriter { private static final String TAG = "FavoritesWriter"; 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 UTF_8 = "UTF-8"; - @Override - public void writeDocument(List feeds, Writer writer, Context context) + public static void writeDocument(List allFavorites, Writer writer, Context context) throws IllegalArgumentException, IllegalStateException, IOException { Log.d(TAG, "Starting to write document"); @@ -43,8 +38,6 @@ public class FavoritesWriter implements ExportWriter { InputStream feedTemplateStream = context.getAssets().open(FEED_TEMPLATE); String feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8); - List allFavorites = DBReader.getEpisodes(0, Integer.MAX_VALUE, - new FeedItemFilter(FeedItemFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD); Map> favoriteByFeed = getFeedMap(allFavorites); writer.append(templateParts[0]); @@ -72,7 +65,7 @@ public class FavoritesWriter implements ExportWriter { * @param favoritesList {@code List} of all favorite episodes. * @return A {@code Map} favorite episodes, keyed by feed ID. */ - private Map> getFeedMap(List favoritesList) { + private static Map> getFeedMap(List favoritesList) { Map> feedMap = new TreeMap<>(); for (FeedItem item : favoritesList) { @@ -89,7 +82,7 @@ public class FavoritesWriter implements ExportWriter { 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 .replace("{FEED_IMG}", feed.getImageUrl()) .replace("{FEED_TITLE}", feed.getTitle()) @@ -99,7 +92,7 @@ public class FavoritesWriter implements ExportWriter { 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()); if (item.getLink() != null) { favItem = favItem.replace("{FAV_WEBSITE}", item.getLink()); @@ -114,9 +107,4 @@ public class FavoritesWriter implements ExportWriter { writer.append(favItem); } - - @Override - public String fileExtension() { - return "html"; - } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlWriter.java b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/HtmlWriter.java similarity index 82% rename from core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlWriter.java rename to storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/HtmlWriter.java index 8a660600f..6ad1feb3d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/html/HtmlWriter.java +++ b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/HtmlWriter.java @@ -1,8 +1,7 @@ -package de.danoeh.antennapod.core.export.html; +package de.danoeh.antennapod.storage.importexport; import android.content.Context; import android.util.Log; -import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.model.feed.Feed; import java.io.IOException; import java.io.InputStream; @@ -11,15 +10,14 @@ import java.util.List; import org.apache.commons.io.IOUtils; /** Writes HTML documents. */ -public class HtmlWriter implements ExportWriter { +public class HtmlWriter { private static final String TAG = "HtmlWriter"; /** * Takes a list of feeds and a writer and writes those into an HTML * document. */ - @Override - public void writeDocument(List feeds, Writer writer, Context context) + public static void writeDocument(List feeds, Writer writer, Context context) throws IllegalArgumentException, IllegalStateException, IOException { Log.d(TAG, "Starting to write document"); @@ -43,9 +41,4 @@ public class HtmlWriter implements ExportWriter { writer.append(templateParts[1]); Log.d(TAG, "Finished writing document"); } - - public String fileExtension() { - return "html"; - } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlElement.java b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlElement.java similarity index 93% rename from core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlElement.java rename to storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlElement.java index e4ba08440..c644ae43e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlElement.java +++ b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlElement.java @@ -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. diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlReader.java b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlReader.java similarity index 90% rename from core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlReader.java rename to storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlReader.java index 9bcca5caf..e5165d63b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlReader.java +++ b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlReader.java @@ -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 de.danoeh.antennapod.storage.preferences.BuildConfig; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; @@ -10,8 +12,6 @@ import java.io.IOException; import java.io.Reader; import java.util.ArrayList; -import de.danoeh.antennapod.core.BuildConfig; - /** * Reads OPML documents. */ @@ -53,7 +53,7 @@ public class OpmlReader { OpmlElement element = new OpmlElement(); final String title = xpp.getAttributeValue(null, OpmlSymbols.TITLE); - if (title != null) { + if (!TextUtils.isEmpty(title)) { Log.i(TAG, "Using title: " + title); element.setText(title); } else { @@ -63,8 +63,8 @@ public class OpmlReader { element.setXmlUrl(xpp.getAttributeValue(null, OpmlSymbols.XMLURL)); element.setHtmlUrl(xpp.getAttributeValue(null, OpmlSymbols.HTMLURL)); element.setType(xpp.getAttributeValue(null, OpmlSymbols.TYPE)); - if (element.getXmlUrl() != null) { - if (element.getText() == null) { + if (!TextUtils.isEmpty(element.getXmlUrl())) { + if (TextUtils.isEmpty(element.getText())) { Log.i(TAG, "Opml element has no text attribute."); element.setText(element.getXmlUrl()); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlSymbols.java similarity index 58% rename from core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java rename to storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlSymbols.java index 0cdfd0d87..886650230 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlSymbols.java +++ b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlSymbols.java @@ -1,12 +1,14 @@ -package de.danoeh.antennapod.core.export.opml; - -import de.danoeh.antennapod.core.export.CommonSymbols; +package de.danoeh.antennapod.storage.importexport; /** * 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"; static final String OUTLINE = "outline"; static final String TEXT = "text"; @@ -19,5 +21,4 @@ final class OpmlSymbols extends CommonSymbols { private OpmlSymbols() { } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlWriter.java similarity index 82% rename from core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java rename to storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlWriter.java index a44d90557..75be4bffe 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/export/opml/OpmlWriter.java +++ b/storage/importexport/src/main/java/de/danoeh/antennapod/storage/importexport/OpmlWriter.java @@ -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.Xml; -import de.danoeh.antennapod.core.util.DateFormatter; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.Writer; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.Locale; -import de.danoeh.antennapod.core.export.ExportWriter; import de.danoeh.antennapod.model.feed.Feed; /** Writes OPML documents. */ -public class OpmlWriter implements ExportWriter { +public class OpmlWriter { private static final String TAG = "OpmlWriter"; 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 * document. */ - @Override - public void writeDocument(List feeds, Writer writer, Context context) + public static void writeDocument(List feeds, Writer writer) throws IllegalArgumentException, IllegalStateException, IOException { Log.d(TAG, "Starting to write document"); XmlSerializer xs = Xml.newSerializer(); @@ -44,7 +42,7 @@ public class OpmlWriter implements ExportWriter { xs.text(OPML_TITLE); xs.endTag(null, OpmlSymbols.TITLE); 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.HEAD); @@ -68,8 +66,8 @@ public class OpmlWriter implements ExportWriter { Log.d(TAG, "Finished writing document"); } - public String fileExtension() { - return "opml"; + private static String formatRfc822Date(Date date) { + SimpleDateFormat format = new SimpleDateFormat("dd MMM yy HH:mm:ss Z", Locale.US); + return format.format(date); } - }