diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedActivity.java index 6b50fed44..2f38a1462 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedActivity.java @@ -5,16 +5,26 @@ import android.os.Bundle; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; +import org.briarproject.briar.android.blog.RssImportResult.FileImportError; +import org.briarproject.briar.android.blog.RssImportResult.FileImportSuccess; +import org.briarproject.briar.android.blog.RssImportResult.UrlImportError; +import org.briarproject.briar.android.blog.RssImportResult.UrlImportSuccess; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; +import org.briarproject.briar.android.fragment.ErrorFragment; +import org.briarproject.briar.android.fragment.FinalFragment; import org.briarproject.nullsafety.MethodsNotNullByDefault; import org.briarproject.nullsafety.ParametersNotNullByDefault; import javax.inject.Inject; import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; +import static androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE; +import static org.briarproject.briar.android.util.UiUtils.showFragment; + @MethodsNotNullByDefault @ParametersNotNullByDefault public class RssFeedActivity extends BriarActivity @@ -45,21 +55,37 @@ public class RssFeedActivity extends BriarActivity viewModel.getImportResult().observeEvent(this, this::onImportResult); } - private void onImportResult(boolean result) { - if (result) { - FragmentManager fm = getSupportFragmentManager(); + private void onImportResult(@Nullable RssImportResult result) { + FragmentManager fm = getSupportFragmentManager(); + if (result instanceof UrlImportSuccess) { if (fm.findFragmentByTag(RssFeedImportFragment.TAG) != null) { onBackPressed(); } - } else { - String url = viewModel.getUrlFailedImport(); - if (url == null) { - throw new AssertionError(); - } + } else if (result instanceof UrlImportError) { + String url = ((UrlImportError) result).url; RssFeedImportFailedDialogFragment dialog = RssFeedImportFailedDialogFragment.newInstance(url); - dialog.show(getSupportFragmentManager(), - RssFeedImportFailedDialogFragment.TAG); + dialog.show(fm, RssFeedImportFailedDialogFragment.TAG); + } else if (result instanceof FileImportSuccess) { + // pop stack back to before the initial import fragment + fm.popBackStackImmediate(RssFeedImportFragment.TAG, + POP_BACK_STACK_INCLUSIVE); + // show success fragment + Fragment f = FinalFragment.newInstance( + R.string.blogs_rss_feeds_import_success, + R.drawable.ic_check_circle_outline, + R.color.briar_brand_green, 0 + ); + String tag = FinalFragment.TAG; + showFragment(fm, f, tag); + } else if (result instanceof FileImportError) { + // pop stack back to initial import fragment + fm.popBackStackImmediate(RssFeedImportFragment.TAG, 0); + // show error fragment + Fragment f = ErrorFragment.newInstance( + getString(R.string.blogs_rss_feeds_import_error)); + String tag = ErrorFragment.TAG; + showFragment(fm, f, tag); } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportFragment.java index f4a9f5b93..b12c47423 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportFragment.java @@ -1,9 +1,13 @@ package org.briarproject.briar.android.blog; +import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; @@ -13,18 +17,27 @@ import android.widget.ProgressBar; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.android.fragment.ProgressFragment; +import org.briarproject.briar.android.util.ActivityLaunchers.GetContentAdvanced; +import org.briarproject.briar.android.util.ActivityLaunchers.OpenDocumentAdvanced; import org.briarproject.nullsafety.MethodsNotNullByDefault; import org.briarproject.nullsafety.ParametersNotNullByDefault; import javax.annotation.Nullable; import javax.inject.Inject; +import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.RequiresApi; +import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import static android.os.Build.VERSION.SDK_INT; import static android.view.View.GONE; import static android.view.View.VISIBLE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard; +import static org.briarproject.briar.android.util.UiUtils.launchActivityToOpenFile; +import static org.briarproject.briar.android.util.UiUtils.showFragment; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -39,6 +52,15 @@ public class RssFeedImportFragment extends BaseFragment { private Button importButton; private ProgressBar progressBar; + @RequiresApi(19) + private final ActivityResultLauncher docLauncher = + registerForActivityResult(new OpenDocumentAdvanced(), + this::onFileChosen); + + private final ActivityResultLauncher contentLauncher = + registerForActivityResult(new GetContentAdvanced(), + this::onFileChosen); + @Override public void injectFragment(ActivityComponent component) { component.inject(this); @@ -52,6 +74,7 @@ public class RssFeedImportFragment extends BaseFragment { @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { requireActivity().setTitle(getString(R.string.blogs_rss_feeds_import)); + if (SDK_INT >= 19) setHasOptionsMenu(true); View v = inflater.inflate(R.layout.fragment_rss_feed_import, container, false); @@ -92,11 +115,40 @@ public class RssFeedImportFragment extends BaseFragment { return v; } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (SDK_INT >= 19) { + inflater.inflate(R.menu.rss_feed_import_actions, menu); + } + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.action_import_file && SDK_INT >= 19) { + launchActivityToOpenFile(requireContext(), docLauncher, + contentLauncher, "*/*"); + return true; + } + return super.onOptionsItemSelected(item); + } + @Override public String getUniqueTag() { return TAG; } + private void onFileChosen(@Nullable Uri uri) { + if (uri == null) return; + // show progress fragment + Fragment f = ProgressFragment.newInstance( + getString(R.string.blogs_rss_feeds_import_progress)); + String tag = ProgressFragment.TAG; + showFragment(getParentFragmentManager(), f, tag); + // view model will import and change state that activity will react to + viewModel.importFeed(uri); + } + private void enableOrDisableImportButton() { String url = urlInput.getText().toString(); importButton.setEnabled(viewModel.validateAndNormaliseUrl(url) != null); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedViewModel.java index c9f050c77..6f6c707af 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedViewModel.java @@ -1,6 +1,8 @@ package org.briarproject.briar.android.blog; import android.app.Application; +import android.content.ContentResolver; +import android.net.Uri; import android.util.Patterns; import org.briarproject.bramble.api.db.DatabaseExecutor; @@ -11,6 +13,10 @@ import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.blog.RssImportResult.FileImportError; +import org.briarproject.briar.android.blog.RssImportResult.FileImportSuccess; +import org.briarproject.briar.android.blog.RssImportResult.UrlImportError; +import org.briarproject.briar.android.blog.RssImportResult.UrlImportSuccess; import org.briarproject.briar.android.viewmodel.DbViewModel; import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveResult; @@ -20,6 +26,7 @@ import org.briarproject.briar.api.feed.FeedManager; import org.briarproject.nullsafety.NotNullByDefault; import java.io.IOException; +import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -30,6 +37,7 @@ import java.util.logging.Logger; import javax.inject.Inject; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -52,11 +60,9 @@ class RssFeedViewModel extends DbViewModel { private final MutableLiveData>> feeds = new MutableLiveData<>(); - @Nullable - private volatile String urlFailedImport = null; private final MutableLiveData isImporting = new MutableLiveData<>(false); - private final MutableLiveEvent importResult = + private final MutableLiveEvent importResult = new MutableLiveEvent<>(); @Inject @@ -120,7 +126,7 @@ class RssFeedViewModel extends DbViewModel { }); } - LiveEvent getImportResult() { + LiveEvent getImportResult() { return importResult; } @@ -130,7 +136,6 @@ class RssFeedViewModel extends DbViewModel { void importFeed(String url) { isImporting.setValue(true); - urlFailedImport = null; ioExecutor.execute(() -> { try { Feed feed = feedManager.addFeed(url); @@ -145,19 +150,38 @@ class RssFeedViewModel extends DbViewModel { updated = feedList; } feeds.postValue(new LiveResult<>(updated)); - importResult.postEvent(true); + importResult.postEvent(new UrlImportSuccess()); } catch (DbException | IOException e) { logException(LOG, WARNING, e); - urlFailedImport = url; - importResult.postEvent(false); + importResult.postEvent(new UrlImportError(url)); } finally { isImporting.postValue(false); } }); } - @Nullable - String getUrlFailedImport() { - return urlFailedImport; + @UiThread + void importFeed(Uri uri) { + ContentResolver contentResolver = getApplication().getContentResolver(); + ioExecutor.execute(() -> { + try (InputStream is = contentResolver.openInputStream(uri)) { + Feed feed = feedManager.addFeed(is); + // Update the feed if it was already present + List feedList = getList(feeds); + if (feedList == null) feedList = new ArrayList<>(); + List updated = updateListItems(feedList, + f -> f.equals(feed), f -> feed); + // Add the feed if it wasn't already present + if (updated == null) { + feedList.add(feed); + updated = feedList; + } + feeds.postValue(new LiveResult<>(updated)); + importResult.postEvent(new FileImportSuccess()); + } catch (IOException | DbException e) { + logException(LOG, WARNING, e); + importResult.postEvent(new FileImportError()); + } + }); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssImportResult.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssImportResult.java new file mode 100644 index 000000000..07d77abbd --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssImportResult.java @@ -0,0 +1,24 @@ +package org.briarproject.briar.android.blog; + +import org.briarproject.nullsafety.NotNullByDefault; + +@NotNullByDefault +abstract class RssImportResult { + + static class UrlImportSuccess extends RssImportResult { + } + + static class UrlImportError extends RssImportResult { + final String url; + + UrlImportError(String url) { + this.url = url; + } + } + + static class FileImportSuccess extends RssImportResult { + } + + static class FileImportError extends RssImportResult { + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/fragment/ProgressFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/fragment/ProgressFragment.java new file mode 100644 index 000000000..2d0ee941e --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/fragment/ProgressFragment.java @@ -0,0 +1,58 @@ +package org.briarproject.briar.android.fragment; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import org.briarproject.briar.R; +import org.briarproject.nullsafety.MethodsNotNullByDefault; +import org.briarproject.nullsafety.ParametersNotNullByDefault; + +import androidx.annotation.Nullable; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class ProgressFragment extends BaseFragment { + + public static final String TAG = ProgressFragment.class.getName(); + + private static final String PROGRESS_MSG = "progressMessage"; + + public static ProgressFragment newInstance(String message) { + ProgressFragment f = new ProgressFragment(); + Bundle args = new Bundle(); + args.putString(PROGRESS_MSG, message); + f.setArguments(args); + return f; + } + + private String progressMessage; + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle args = requireArguments(); + progressMessage = args.getString(PROGRESS_MSG); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View v = inflater + .inflate(R.layout.fragment_progress, container, false); + TextView msg = v.findViewById(R.id.progressMessage); + msg.setText(progressMessage); + return v; + } + +} diff --git a/briar-android/src/main/res/layout/fragment_progress.xml b/briar-android/src/main/res/layout/fragment_progress.xml new file mode 100644 index 000000000..456e6d105 --- /dev/null +++ b/briar-android/src/main/res/layout/fragment_progress.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + diff --git a/briar-android/src/main/res/menu/rss_feed_import_actions.xml b/briar-android/src/main/res/menu/rss_feed_import_actions.xml new file mode 100644 index 000000000..b1c2f1bb8 --- /dev/null +++ b/briar-android/src/main/res/menu/rss_feed_import_actions.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 30a13ebb9..b6b565040 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -519,7 +519,10 @@ Import RSS Feed Import Enter the URL of the RSS feed + Importing RSS Feed… + Your feed was successfully imported. We are sorry! There was an error importing your feed. + Import feed from file RSS Feeds Imported: Author: