From 19fc4c1f0d2af84ba3f663b369d5e8eb7f8ef80e Mon Sep 17 00:00:00 2001 From: William Seemann Date: Fri, 20 Sep 2019 14:03:23 -0700 Subject: OPML export: Choose export folder (#3175) --- .../asynctask/DocumentFileExportWorker.java | 72 ++++++++ .../preferences/StoragePreferencesFragment.java | 182 +++++++++++++++------ 2 files changed, 208 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java (limited to 'app/src/main/java/de/danoeh') diff --git a/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java new file mode 100644 index 000000000..0e9572a82 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/asynctask/DocumentFileExportWorker.java @@ -0,0 +1,72 @@ +package de.danoeh.antennapod.asynctask; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.v4.provider.DocumentFile; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import de.danoeh.antennapod.core.export.ExportWriter; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.LangUtils; +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(); + if (uri == null) { + throw new FileNotFoundException("Export file not found."); + } + outputStream = context.getContentResolver().openOutputStream(uri); + if (outputStream == null) { + throw new IOException(); + } + writer = new OutputStreamWriter(outputStream, LangUtils.UTF_8); + exportWriter.writeDocument(DBReader.getFeedList(), writer); + 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/fragment/preferences/StoragePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java index 87d436eda..e36476c6f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/StoragePreferencesFragment.java @@ -4,6 +4,7 @@ import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -13,13 +14,16 @@ import android.os.Build; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.content.FileProvider; +import android.support.v4.provider.DocumentFile; import android.support.v7.app.AlertDialog; import android.support.v7.preference.PreferenceFragmentCompat; import android.util.Log; + import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DirectoryChooserActivity; import de.danoeh.antennapod.activity.ImportExportActivity; import de.danoeh.antennapod.activity.OpmlImportFromPathActivity; +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; @@ -45,6 +49,12 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; private static final int PERMISSION_REQUEST_EXTERNAL_STORAGE = 41; + private static final int 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 String DEFAULT_HTML_OUTPUT_NAME = "antennapod-feeds.html"; + private static final String CONTENT_TYPE_HTML = "text/html"; private Disposable disposable; @Override @@ -59,6 +69,14 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { setDataFolderText(); } + @Override + public void onStop() { + super.onStop(); + if (disposable != null) { + disposable.dispose(); + } + } + private void setupStorageScreen() { final Activity activity = getActivity(); @@ -69,9 +87,16 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { } ); findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( - preference -> export(new OpmlWriter())); + preference -> { + openOpmlExportPathPicker(); + return true; + } + ); findPreference(PREF_HTML_EXPORT).setOnPreferenceClickListener( - preference -> export(new HtmlWriter())); + preference -> { + openHtmlExportPathPicker(); + return true; + }); findPreference(PREF_OPML_IMPORT).setOnPreferenceClickListener( preference -> { activity.startActivity(new Intent(activity, OpmlImportFromPathActivity.class)); @@ -129,54 +154,65 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { } private boolean export(ExportWriter exportWriter) { + return export(exportWriter, null); + } + + private boolean export(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(); - final AlertDialog.Builder alert = new AlertDialog.Builder(context) - .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); - Observable observable = new ExportWorker(exportWriter).exportObservable(); - disposable = observable.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(output -> { - alert.setTitle(R.string.export_success_title); - String message = context.getString(R.string.export_success_sum, output.toString()); - alert.setMessage(message); - alert.setPositiveButton(R.string.send_label, (dialog, which) -> { + if (uri == null) { + Observable observable = new ExportWorker(exportWriter).exportObservable(); + disposable = observable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(output -> { Uri fileUri = FileProvider.getUriForFile(context.getApplicationContext(), context.getString(R.string.provider_authority), output); - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_SUBJECT, - context.getResources().getText(R.string.opml_export_label)); - sendIntent.putExtra(Intent.EXTRA_STREAM, fileUri); - sendIntent.setType("text/plain"); - sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - List resInfoList = context.getPackageManager().queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - context.startActivity(Intent.createChooser(sendIntent, - context.getResources().getText(R.string.send_label))); - }); - alert.create().show(); - }, error -> { - alert.setTitle(R.string.export_error_label); - alert.setMessage(error.getMessage()); - alert.show(); - }, progressDialog::dismiss); + showExportSuccessDialog(context.getString(R.string.export_success_sum, output.toString()), fileUri); + }, this::showExportErrorDialog, progressDialog::dismiss); + } else { + Observable observable = new DocumentFileExportWorker(exportWriter, context, uri).exportObservable(); + disposable = observable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(output -> { + showExportSuccessDialog(context.getString(R.string.export_success_sum, output.getUri()), output.getUri()); + }, this::showExportErrorDialog, progressDialog::dismiss); + } return true; } - @Override - public void onStop() { - super.onStop(); - if (disposable != null) { - disposable.dispose(); - } + 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()); + alert.setTitle(R.string.export_success_title); + alert.setMessage(message); + 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)); + sendIntent.putExtra(Intent.EXTRA_STREAM, streamUri); + sendIntent.setType("text/plain"); + sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + List resInfoList = getContext().getPackageManager() + .queryIntentActivities(sendIntent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + getContext().grantUriPermission(packageName, streamUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } + getContext().startActivity(Intent.createChooser(sendIntent, getString(R.string.send_label))); + }); + alert.create().show(); + } + + private void showExportErrorDialog(final Throwable error) { + final AlertDialog.Builder alert = new AlertDialog.Builder(getContext()) + .setNeutralButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()); + alert.setTitle(R.string.export_error_label); + alert.setMessage(error.getMessage()); + alert.show(); } @SuppressLint("NewApi") @@ -186,22 +222,22 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { String dir = data.getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR); File path; - if(dir != null) { + if (dir != null) { path = new File(dir); } else { path = getActivity().getExternalFilesDir(null); } String message = null; - final Context context= getActivity().getApplicationContext(); - if(!path.exists()) { + final Context context = getActivity().getApplicationContext(); + if (!path.exists()) { message = String.format(context.getString(R.string.folder_does_not_exist_error), dir); - } else if(!path.canRead()) { + } else if (!path.canRead()) { message = String.format(context.getString(R.string.folder_not_readable_error), dir); - } else if(!path.canWrite()) { + } else if (!path.canWrite()) { message = String.format(context.getString(R.string.folder_not_writable_error), dir); } - if(message == null) { + if (message == null) { Log.d(TAG, "Setting data folder: " + dir); UserPreferences.setDataFolder(dir); setDataFolderText(); @@ -212,6 +248,16 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { ab.show(); } } + + if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_OPML_EXPORT_PATH) { + Uri uri = data.getData(); + export(new OpmlWriter(), uri); + } + + if (resultCode == Activity.RESULT_OK && requestCode == CHOOSE_HTML_EXPORT_PATH) { + Uri uri = data.getData(); + export(new HtmlWriter(), uri); + } } private void setDataFolderText() { @@ -233,6 +279,50 @@ public class StoragePreferencesFragment extends PreferenceFragmentCompat { activity.startActivityForResult(intent, DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); } + private void openOpmlExportPathPicker() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { + Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(CONTENT_TYPE_OPML) + .putExtra(Intent.EXTRA_TITLE, DEFAULT_OPML_OUTPUT_NAME); + + // Creates an implicit intent to launch a file manager which lets + // the user choose a specific directory to export to. + try { + startActivityForResult(intentPickAction, CHOOSE_OPML_EXPORT_PATH); + return; + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + } + + // If we are using a SDK lower than API 21 or the implicit intent failed + // fallback to the legacy export process + export(new OpmlWriter()); + } + + private void openHtmlExportPathPicker() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { + Intent intentPickAction = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(CONTENT_TYPE_HTML) + .putExtra(Intent.EXTRA_TITLE, DEFAULT_HTML_OUTPUT_NAME); + + // Creates an implicit intent to launch a file manager which lets + // the user choose a specific directory to export to. + try { + startActivityForResult(intentPickAction, CHOOSE_HTML_EXPORT_PATH); + return; + } catch (ActivityNotFoundException e) { + Log.e(TAG, "No activity found. Should never happen..."); + } + } + + // If we are using a SDK lower than API 21 or the implicit intent failed + // fallback to the legacy export process + export(new HtmlWriter()); + } + private void showChooseDataFolderDialog() { ChooseDataFolderDialog.showDialog( getActivity(), new ChooseDataFolderDialog.RunnableWithString() { -- cgit v1.2.3