diff --git a/bramble-android/build.gradle b/bramble-android/build.gradle index d8fb44adc..2b30886c2 100644 --- a/bramble-android/build.gradle +++ b/bramble-android/build.gradle @@ -15,8 +15,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 30 - versionCode 10302 - versionName "1.3.2" + versionCode 10303 + versionName "1.3.3" consumerProguardFiles 'proguard-rules.txt' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java b/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java index 5fe56cd5c..bde4b9a23 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java @@ -10,17 +10,20 @@ import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import java.io.File; +import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Scanner; import javax.annotation.Nullable; import static android.content.Context.MODE_PRIVATE; import static android.os.Build.VERSION.SDK_INT; +import static java.lang.Runtime.getRuntime; import static java.util.Arrays.asList; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; @@ -118,4 +121,17 @@ public class AndroidUtils { public static String[] getSupportedImageContentTypes() { return new String[] {"image/jpeg", "image/png", "image/gif"}; } + + @Nullable + public static String getSystemProperty(String propName) { + try { + Process p = getRuntime().exec("getprop " + propName); + Scanner s = new Scanner(p.getInputStream()); + String line = s.nextLine(); + s.close(); + return line; + } catch (SecurityException | IOException e) { + return null; + } + } } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/event/KeyAgreementStartedEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/event/KeyAgreementStartedEvent.java index 289077cb8..513627ceb 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/event/KeyAgreementStartedEvent.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/event/KeyAgreementStartedEvent.java @@ -3,7 +3,7 @@ package org.briarproject.bramble.api.keyagreement.event; import org.briarproject.bramble.api.event.Event; /** - * An event that is broadcast when a BQP protocol completes. + * An event that is broadcast when a BQP protocol begins. */ public class KeyAgreementStartedEvent extends Event { } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java index 91f0860a3..bc0d42885 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java @@ -473,6 +473,16 @@ abstract class AbstractBluetoothPlugin implements BluetoothPlugin, return discoverSemaphore.availablePermits() == 0; } + @Override + public void disablePolling() { + connectionLimiter.startLimiting(); + } + + @Override + public void enablePolling() { + connectionLimiter.endLimiting(); + } + @Override public DuplexTransportConnection discoverAndConnectForSetup(String uuid) { DuplexTransportConnection conn = discoverAndConnect(uuid); @@ -501,9 +511,9 @@ abstract class AbstractBluetoothPlugin implements BluetoothPlugin, if (s.getNamespace().equals(ID.getString())) ioExecutor.execute(() -> onSettingsUpdated(s.getSettings())); } else if (e instanceof KeyAgreementListeningEvent) { - ioExecutor.execute(connectionLimiter::keyAgreementStarted); + connectionLimiter.startLimiting(); } else if (e instanceof KeyAgreementStoppedListeningEvent) { - ioExecutor.execute(connectionLimiter::keyAgreementEnded); + connectionLimiter.endLimiting(); } else if (e instanceof RemoteTransportPropertiesUpdatedEvent) { RemoteTransportPropertiesUpdatedEvent r = (RemoteTransportPropertiesUpdatedEvent) e; diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothConnectionLimiter.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothConnectionLimiter.java index c3b115e58..23e2586cf 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothConnectionLimiter.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothConnectionLimiter.java @@ -7,14 +7,15 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; interface BluetoothConnectionLimiter { /** - * Informs the limiter that key agreement has started. + * Tells the limiter to not allow regular polling connections (because we + * are about to do key agreement, or connect via BT for setup). */ - void keyAgreementStarted(); + void startLimiting(); /** - * Informs the limiter that key agreement has ended. + * Tells the limiter to no longer limit regular polling connections. */ - void keyAgreementEnded(); + void endLimiting(); /** * Returns true if a contact connection can be opened. This method does not diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothConnectionLimiterImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothConnectionLimiterImpl.java index e76bbafc6..ac002a3cf 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothConnectionLimiterImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothConnectionLimiterImpl.java @@ -30,34 +30,37 @@ class BluetoothConnectionLimiterImpl implements BluetoothConnectionLimiter { private final List connections = new LinkedList<>(); @GuardedBy("lock") - private boolean keyAgreementInProgress = false; + private int limitingInProgress = 0; BluetoothConnectionLimiterImpl(EventBus eventBus) { this.eventBus = eventBus; } @Override - public void keyAgreementStarted() { + public void startLimiting() { synchronized (lock) { - keyAgreementInProgress = true; + limitingInProgress++; } - LOG.info("Key agreement started"); + LOG.info("Limiting started"); eventBus.broadcast(new CloseSyncConnectionsEvent(ID)); } @Override - public void keyAgreementEnded() { + public void endLimiting() { synchronized (lock) { - keyAgreementInProgress = false; + limitingInProgress--; + if (limitingInProgress < 0) { + throw new IllegalStateException(); + } } - LOG.info("Key agreement ended"); + LOG.info("Limiting ended"); } @Override public boolean canOpenContactConnection() { synchronized (lock) { - if (keyAgreementInProgress) { - LOG.info("Can't open contact connection during key agreement"); + if (limitingInProgress > 0) { + LOG.info("Can't open contact connection while limiting"); return false; } else { LOG.info("Can open contact connection"); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java index 1350ee128..5ff607fb8 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java @@ -11,6 +11,10 @@ public interface BluetoothPlugin extends DuplexPlugin { boolean isDiscovering(); + void disablePolling(); + + void enablePolling(); + @Nullable DuplexTransportConnection discoverAndConnectForSetup(String uuid); diff --git a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java index f18289cc9..99ebef925 100644 --- a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java +++ b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java @@ -23,8 +23,10 @@ import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import java.io.File; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; import javax.inject.Inject; @@ -45,15 +47,22 @@ import static org.junit.Assume.assumeTrue; public class BridgeTest extends BrambleTestCase { @Parameters - public static Iterable data() { + public static Iterable data() { BrambleJavaIntegrationTestComponent component = DaggerBrambleJavaIntegrationTestComponent.builder().build(); BrambleCoreIntegrationTestEagerSingletons.Helper .injectEagerSingletons(component); - return component.getCircumventionProvider().getBridges(false); + // Share a failure counter among all the test instances + AtomicInteger failures = new AtomicInteger(0); + List bridges = + component.getCircumventionProvider().getBridges(false); + List states = new ArrayList<>(bridges.size()); + for (String bridge : bridges) states.add(new Params(bridge, failures)); + return states; } private final static long TIMEOUT = SECONDS.toMillis(60); + private final static int NUM_FAILURES_ALLOWED = 1; private final static Logger LOG = getLogger(BridgeTest.class.getName()); @@ -80,11 +89,13 @@ public class BridgeTest extends BrambleTestCase { private final File torDir = getTestDirectory(); private final String bridge; + private final AtomicInteger failures; private UnixTorPluginFactory factory; - public BridgeTest(String bridge) { - this.bridge = bridge; + public BridgeTest(Params params) { + bridge = params.bridge; + failures = params.failures; } @Before @@ -152,10 +163,24 @@ public class BridgeTest extends BrambleTestCase { clock.sleep(500); } if (plugin.getState() != ACTIVE) { - fail("Could not connect to Tor within timeout."); + LOG.warning("Could not connect to Tor within timeout"); + if (failures.incrementAndGet() > NUM_FAILURES_ALLOWED) { + fail(failures.get() + " bridges are unreachable"); + } } } finally { plugin.stop(); } } + + private static class Params { + + private final String bridge; + private final AtomicInteger failures; + + private Params(String bridge, AtomicInteger failures) { + this.bridge = bridge; + this.failures = failures; + } + } } diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 51bcdef3f..25f64d891 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -26,8 +26,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 30 - versionCode 10302 - versionName "1.3.2" + versionCode 10303 + versionName "1.3.3" applicationId "org.briarproject.briar.android" vectorDrawables.useSupportLibrary = true diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml index 87d431f37..1eac6796d 100644 --- a/briar-android/src/main/AndroidManifest.xml +++ b/briar-android/src/main/AndroidManifest.xml @@ -104,8 +104,7 @@ + android:label="@string/setup_title" /> - - - - - - - - + + + + = 10; + } catch (NumberFormatException e) { + return false; + } + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index 5e342c618..c03f98b61 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -13,8 +13,11 @@ import org.briarproject.briar.android.blog.BlogPostFragment; import org.briarproject.briar.android.blog.FeedFragment; import org.briarproject.briar.android.blog.ReblogActivity; import org.briarproject.briar.android.blog.ReblogFragment; -import org.briarproject.briar.android.blog.RssFeedImportActivity; -import org.briarproject.briar.android.blog.RssFeedManageActivity; +import org.briarproject.briar.android.blog.RssFeedActivity; +import org.briarproject.briar.android.blog.RssFeedDeleteFeedDialogFragment; +import org.briarproject.briar.android.blog.RssFeedImportFailedDialogFragment; +import org.briarproject.briar.android.blog.RssFeedImportFragment; +import org.briarproject.briar.android.blog.RssFeedManageFragment; import org.briarproject.briar.android.blog.WriteBlogPostActivity; import org.briarproject.briar.android.contact.ContactListFragment; import org.briarproject.briar.android.contact.add.nearby.AddNearbyContactActivity; @@ -161,9 +164,7 @@ public interface ActivityComponent { void inject(IntroductionActivity activity); - void inject(RssFeedImportActivity activity); - - void inject(RssFeedManageActivity activity); + void inject(RssFeedActivity activity); void inject(StartupFailureActivity activity); @@ -233,4 +234,12 @@ public interface ActivityComponent { void inject( BluetoothConnecterDialogFragment bluetoothConnecterDialogFragment); + + void inject(RssFeedImportFragment fragment); + + void inject(RssFeedManageFragment fragment); + + void inject(RssFeedImportFailedDialogFragment fragment); + + void inject(RssFeedDeleteFeedDialogFragment fragment); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java index a12a5fc03..b6a7cdd93 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogModule.java @@ -20,4 +20,8 @@ public interface BlogModule { @ViewModelKey(BlogViewModel.class) ViewModel bindBlogViewModel(BlogViewModel blogViewModel); + @Binds + @IntoMap + @ViewModelKey(RssFeedViewModel.class) + ViewModel bindRssFeedViewModel(RssFeedViewModel rssFeedViewModel); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java index 4e55eb795..ae377842a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/FeedFragment.java @@ -131,15 +131,8 @@ public class FeedFragment extends BaseFragment i.putExtra(GROUP_ID, personalBlog.getId().getBytes()); startActivity(i); return true; - } else if (itemId == R.id.action_rss_feeds_import) { - Intent i = new Intent(getActivity(), RssFeedImportActivity.class); - startActivity(i); - return true; - } else if (itemId == R.id.action_rss_feeds_manage) { - Blog personalBlog = viewModel.getPersonalBlog().getValue(); - if (personalBlog == null) return false; - Intent i = new Intent(getActivity(), RssFeedManageActivity.class); - i.putExtra(GROUP_ID, personalBlog.getId().getBytes()); + } else if (itemId == R.id.action_rss_feeds) { + Intent i = new Intent(getActivity(), RssFeedActivity.class); startActivity(i); return true; } 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 new file mode 100644 index 000000000..9668c27f4 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedActivity.java @@ -0,0 +1,69 @@ +package org.briarproject.briar.android.blog; + +import android.os.Bundle; +import android.widget.Toast; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.activity.BriarActivity; +import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.ViewModelProvider; + +import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.EXISTS; +import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.FAILED; +import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.IMPORTED; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class RssFeedActivity extends BriarActivity + implements BaseFragmentListener { + + @Inject + ViewModelProvider.Factory viewModelFactory; + private RssFeedViewModel viewModel; + + @Override + public void injectActivity(ActivityComponent component) { + component.inject(this); + + viewModel = new ViewModelProvider(this, viewModelFactory) + .get(RssFeedViewModel.class); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_fragment_container); + + if (savedInstanceState == null) { + showInitialFragment(RssFeedManageFragment.newInstance()); + } + + viewModel.getImportResult().observeEvent(this, this::onImportResult); + } + + private void onImportResult(RssFeedViewModel.ImportResult result) { + if (result == IMPORTED) { + FragmentManager fm = getSupportFragmentManager(); + if (fm.findFragmentByTag(RssFeedImportFragment.TAG) != null) { + onBackPressed(); + } + } else if (result == FAILED) { + RssFeedImportFailedDialogFragment dialog = + RssFeedImportFailedDialogFragment.newInstance(); + dialog.show(getSupportFragmentManager(), + RssFeedImportFailedDialogFragment.TAG); + } else if (result == EXISTS) { + Toast.makeText(this, R.string.blogs_rss_feeds_import_exists, + Toast.LENGTH_LONG).show(); + } + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedAdapter.java index 7ec60e0b6..ecf373058 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedAdapter.java @@ -7,91 +7,54 @@ import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.TextView; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; -import org.briarproject.briar.android.util.BriarAdapter; -import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.api.feed.Feed; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static org.briarproject.briar.android.util.UiUtils.formatDate; -class RssFeedAdapter extends BriarAdapter { +@NotNullByDefault +class RssFeedAdapter extends ListAdapter { private final RssFeedListener listener; - RssFeedAdapter(Context ctx, RssFeedListener listener) { - super(ctx, Feed.class); + RssFeedAdapter(RssFeedListener listener) { + super(new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(Feed a, Feed b) { + return a.getUrl().equals(b.getUrl()) && + a.getBlogId().equals(b.getBlogId()) && + a.getAdded() == b.getAdded(); + } + + @Override + public boolean areContentsTheSame(Feed a, Feed b) { + return a.getUpdated() == b.getUpdated(); + } + }); this.listener = listener; } @Override public FeedViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View v = LayoutInflater.from(ctx).inflate( + View v = LayoutInflater.from(parent.getContext()).inflate( R.layout.list_item_rss_feed, parent, false); return new FeedViewHolder(v); } @Override public void onBindViewHolder(FeedViewHolder ui, int position) { - Feed item = getItemAt(position); - if (item == null) return; - - // Feed Title - ui.title.setText(item.getTitle()); - - // Delete Button - ui.delete.setOnClickListener(v -> listener.onDeleteClick(item)); - - // Author - if (item.getRssAuthor() != null) { - ui.author.setText(item.getRssAuthor()); - ui.author.setVisibility(VISIBLE); - ui.authorLabel.setVisibility(VISIBLE); - } else { - ui.author.setVisibility(GONE); - ui.authorLabel.setVisibility(GONE); - } - - // Imported and Last Updated - ui.imported.setText(UiUtils.formatDate(ctx, item.getAdded())); - ui.updated.setText(UiUtils.formatDate(ctx, item.getUpdated())); - - // Description - if (item.getDescription() != null) { - ui.description.setText(item.getDescription()); - ui.description.setVisibility(VISIBLE); - } else { - ui.description.setVisibility(GONE); - } - - // Open feed's blog when clicked - ui.layout.setOnClickListener(v -> listener.onFeedClick(item)); + ui.bindItem(getItem(position)); } - @Override - public int compare(Feed a, Feed b) { - if (a == b) return 0; - long aTime = a.getAdded(), bTime = b.getAdded(); - if (aTime > bTime) return -1; - if (aTime < bTime) return 1; - return 0; - } - - @Override - public boolean areContentsTheSame(Feed a, Feed b) { - return a.getUpdated() == b.getUpdated(); - } - - @Override - public boolean areItemsTheSame(Feed a, Feed b) { - return a.getUrl().equals(b.getUrl()) && - a.getBlogId().equals(b.getBlogId()) && - a.getAdded() == b.getAdded(); - } - - static class FeedViewHolder extends RecyclerView.ViewHolder { + class FeedViewHolder extends RecyclerView.ViewHolder { + private final Context ctx; private final View layout; private final TextView title; private final ImageButton delete; @@ -104,6 +67,7 @@ class RssFeedAdapter extends BriarAdapter { private FeedViewHolder(View v) { super(v); + ctx = v.getContext(); layout = v; title = v.findViewById(R.id.titleView); delete = v.findViewById(R.id.deleteButton); @@ -113,10 +77,44 @@ class RssFeedAdapter extends BriarAdapter { authorLabel = v.findViewById(R.id.author); description = v.findViewById(R.id.descriptionView); } + + private void bindItem(Feed item) { + // Feed Title + title.setText(item.getTitle()); + + // Delete Button + delete.setOnClickListener(v -> listener.onDeleteClick(item)); + + // Author + if (item.getRssAuthor() != null) { + author.setText(item.getRssAuthor()); + author.setVisibility(VISIBLE); + authorLabel.setVisibility(VISIBLE); + } else { + author.setVisibility(GONE); + authorLabel.setVisibility(GONE); + } + + // Imported and Last Updated + imported.setText(formatDate(ctx, item.getAdded())); + updated.setText(formatDate(ctx, item.getUpdated())); + + // Description + if (item.getDescription() != null) { + description.setText(item.getDescription()); + description.setVisibility(VISIBLE); + } else { + description.setVisibility(GONE); + } + + // Open feed's blog when clicked + layout.setOnClickListener(v -> listener.onFeedClick(item)); + } } interface RssFeedListener { void onFeedClick(Feed feed); + void onDeleteClick(Feed feed); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedDeleteFeedDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedDeleteFeedDialogFragment.java new file mode 100644 index 000000000..8fa06e113 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedDeleteFeedDialogFragment.java @@ -0,0 +1,64 @@ +package org.briarproject.briar.android.blog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.BaseActivity; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import static java.util.Objects.requireNonNull; +import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class RssFeedDeleteFeedDialogFragment extends DialogFragment { + final static String TAG = RssFeedDeleteFeedDialogFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + private RssFeedViewModel viewModel; + + static RssFeedDeleteFeedDialogFragment newInstance(GroupId groupId) { + Bundle args = new Bundle(); + args.putByteArray(GROUP_ID, groupId.getBytes()); + RssFeedDeleteFeedDialogFragment f = + new RssFeedDeleteFeedDialogFragment(); + f.setArguments(args); + return f; + } + + @Override + public void onAttach(Context ctx) { + super.onAttach(ctx); + ((BaseActivity) requireActivity()).getActivityComponent().inject(this); + + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(RssFeedViewModel.class); + } + + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + GroupId groupId = new GroupId( + requireNonNull(requireArguments().getByteArray(GROUP_ID))); + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), + R.style.BriarDialogTheme); + builder.setTitle(getString(R.string.blogs_rss_remove_feed)); + builder.setMessage( + getString(R.string.blogs_rss_remove_feed_dialog_message)); + builder.setPositiveButton(R.string.cancel, null); + builder.setNegativeButton(R.string.blogs_rss_remove_feed_ok, + (dialog, which) -> viewModel.removeFeed(groupId)); + return builder.create(); + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportActivity.java deleted file mode 100644 index 08ac68ace..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportActivity.java +++ /dev/null @@ -1,170 +0,0 @@ -package org.briarproject.briar.android.blog; - -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Patterns; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.Button; -import android.widget.EditText; -import android.widget.ProgressBar; - -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.lifecycle.IoExecutor; -import org.briarproject.briar.R; -import org.briarproject.briar.android.activity.ActivityComponent; -import org.briarproject.briar.android.activity.BriarActivity; -import org.briarproject.briar.api.feed.FeedManager; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.annotation.Nullable; -import javax.inject.Inject; - -import androidx.appcompat.app.AlertDialog; - -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logException; -import static org.briarproject.briar.android.util.UiUtils.hideSoftKeyboard; - -public class RssFeedImportActivity extends BriarActivity { - - private static final Logger LOG = - Logger.getLogger(RssFeedImportActivity.class.getName()); - - private EditText urlInput; - private Button importButton; - private ProgressBar progressBar; - - @Inject - @IoExecutor - Executor ioExecutor; - - @Inject - @SuppressWarnings("WeakerAccess") - volatile FeedManager feedManager; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_rss_feed_import); - - urlInput = findViewById(R.id.urlInput); - urlInput.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, - int count) { - } - - @Override - public void afterTextChanged(Editable s) { - enableOrDisableImportButton(); - } - }); - urlInput.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == IME_ACTION_DONE && importButton.isEnabled() && - importButton.getVisibility() == VISIBLE) { - publish(); - return true; - } - return false; - }); - - importButton = findViewById(R.id.importButton); - importButton.setOnClickListener(v -> publish()); - - progressBar = findViewById(R.id.progressBar); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return super.onOptionsItemSelected(item); - } - - @Override - public void injectActivity(ActivityComponent component) { - component.inject(this); - } - - private void enableOrDisableImportButton() { - String url = urlInput.getText().toString(); - importButton.setEnabled(validateAndNormaliseUrl(url) != null); - } - - @Nullable - private String validateAndNormaliseUrl(String url) { - if (!Patterns.WEB_URL.matcher(url).matches()) return null; - try { - return new URL(url).toString(); - } catch (MalformedURLException e) { - return null; - } - } - - private void publish() { - // hide import button, show progress bar - importButton.setVisibility(GONE); - progressBar.setVisibility(VISIBLE); - hideSoftKeyboard(urlInput); - - String url = validateAndNormaliseUrl(urlInput.getText().toString()); - if (url == null) throw new AssertionError(); - importFeed(url); - } - - private void importFeed(String url) { - ioExecutor.execute(() -> { - try { - feedManager.addFeed(url); - feedImported(); - } catch (DbException | IOException e) { - logException(LOG, WARNING, e); - importFailed(); - } - }); - } - - private void feedImported() { - runOnUiThreadUnlessDestroyed(this::supportFinishAfterTransition); - } - - private void importFailed() { - runOnUiThreadUnlessDestroyed(() -> { - // hide progress bar, show publish button - progressBar.setVisibility(GONE); - importButton.setVisibility(VISIBLE); - - // show error dialog - AlertDialog.Builder builder = - new AlertDialog.Builder(RssFeedImportActivity.this, - R.style.BriarDialogTheme); - builder.setMessage(R.string.blogs_rss_feeds_import_error); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.try_again_button, - (dialog, which) -> publish()); - AlertDialog dialog = builder.create(); - dialog.show(); - }); - } - -} - diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportFailedDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportFailedDialogFragment.java new file mode 100644 index 000000000..ad1363adc --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportFailedDialogFragment.java @@ -0,0 +1,53 @@ +package org.briarproject.briar.android.blog; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.BaseActivity; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class RssFeedImportFailedDialogFragment extends DialogFragment { + final static String TAG = RssFeedImportFailedDialogFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + private RssFeedViewModel viewModel; + + static RssFeedImportFailedDialogFragment newInstance() { + return new RssFeedImportFailedDialogFragment(); + } + + @Override + public void onAttach(Context ctx) { + super.onAttach(ctx); + ((BaseActivity) requireActivity()).getActivityComponent().inject(this); + + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(RssFeedViewModel.class); + } + + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder builder = + new AlertDialog.Builder(requireActivity(), + R.style.BriarDialogTheme); + builder.setMessage(R.string.blogs_rss_feeds_import_error); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.try_again_button, + (dialog, which) -> viewModel.retryImportFeed()); + + return builder.create(); + } +} 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 new file mode 100644 index 000000000..96845f9bc --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedImportFragment.java @@ -0,0 +1,124 @@ +package org.briarproject.briar.android.blog; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ProgressBar; + +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.fragment.BaseFragment; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.lifecycle.ViewModelProvider; + +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; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class RssFeedImportFragment extends BaseFragment { + public static final String TAG = RssFeedImportFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + private RssFeedViewModel viewModel; + + private EditText urlInput; + private Button importButton; + private ProgressBar progressBar; + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(RssFeedViewModel.class); + } + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + requireActivity().setTitle(getString(R.string.blogs_rss_feeds_import)); + View v = inflater.inflate(R.layout.fragment_rss_feed_import, + container, false); + + urlInput = v.findViewById(R.id.urlInput); + urlInput.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + } + + @Override + public void afterTextChanged(Editable s) { + enableOrDisableImportButton(); + } + }); + urlInput.setOnEditorActionListener((view, actionId, event) -> { + if (actionId == IME_ACTION_DONE && importButton.isEnabled() && + importButton.getVisibility() == VISIBLE) { + publish(); + return true; + } + return false; + }); + + importButton = v.findViewById(R.id.importButton); + importButton.setOnClickListener(view -> publish()); + + progressBar = v.findViewById(R.id.progressBar); + + viewModel.getIsImporting().observe(getViewLifecycleOwner(), + this::onIsImporting); + + return v; + } + + @Override + public String getUniqueTag() { + return TAG; + } + + private void enableOrDisableImportButton() { + String url = urlInput.getText().toString(); + importButton.setEnabled(viewModel.validateAndNormaliseUrl(url) != null); + } + + private void publish() { + String url = viewModel + .validateAndNormaliseUrl(urlInput.getText().toString()); + if (url == null) throw new AssertionError(); + viewModel.importFeed(url); + } + + private void onIsImporting(Boolean importing) { + if (importing) { + // show progress bar, hide import button + importButton.setVisibility(GONE); + progressBar.setVisibility(VISIBLE); + hideSoftKeyboard(urlInput); + } else { + // show publish button, hide progress bar + importButton.setVisibility(VISIBLE); + progressBar.setVisibility(GONE); + } + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedManageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedManageActivity.java deleted file mode 100644 index 0f6819c37..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedManageActivity.java +++ /dev/null @@ -1,178 +0,0 @@ -package org.briarproject.briar.android.blog; - -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import com.google.android.material.snackbar.Snackbar; - -import org.briarproject.bramble.api.db.DbException; -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.RssFeedAdapter.RssFeedListener; -import org.briarproject.briar.android.view.BriarRecyclerView; -import org.briarproject.briar.api.feed.Feed; -import org.briarproject.briar.api.feed.FeedManager; - -import java.util.List; -import java.util.logging.Logger; - -import javax.inject.Inject; - -import androidx.appcompat.app.AlertDialog; -import androidx.recyclerview.widget.LinearLayoutManager; - -import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; -import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.util.LogUtils.logException; - -public class RssFeedManageActivity extends BriarActivity - implements RssFeedListener { - - private static final Logger LOG = - Logger.getLogger(RssFeedManageActivity.class.getName()); - - private BriarRecyclerView list; - private RssFeedAdapter adapter; - - @Inject - @SuppressWarnings("WeakerAccess") - volatile FeedManager feedManager; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_rss_feed_manage); - - adapter = new RssFeedAdapter(this, this); - - list = findViewById(R.id.feedList); - list.setLayoutManager(new LinearLayoutManager(this)); - list.setAdapter(adapter); - } - - @Override - public void onStart() { - super.onStart(); - loadFeeds(); - } - - @Override - public void onStop() { - super.onStop(); - adapter.clear(); - list.showProgressBar(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.rss_feed_manage_actions, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - onBackPressed(); - return true; - } else if (item.getItemId() == R.id.action_rss_feeds_import) { - Intent i = new Intent(this, RssFeedImportActivity.class); - startActivity(i); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - public void injectActivity(ActivityComponent component) { - component.inject(this); - } - - @Override - public void onFeedClick(Feed feed) { - Intent i = new Intent(this, BlogActivity.class); - i.putExtra(GROUP_ID, feed.getBlogId().getBytes()); - i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); - startActivity(i); - } - - @Override - public void onDeleteClick(Feed feed) { - DialogInterface.OnClickListener okListener = - (dialog, which) -> deleteFeed(feed); - AlertDialog.Builder builder = new AlertDialog.Builder(this, - R.style.BriarDialogTheme); - builder.setTitle(getString(R.string.blogs_rss_remove_feed)); - builder.setMessage( - getString(R.string.blogs_rss_remove_feed_dialog_message)); - builder.setPositiveButton(R.string.cancel, null); - builder.setNegativeButton(R.string.blogs_rss_remove_feed_ok, - okListener); - builder.show(); - } - - private void loadFeeds() { - int revision = adapter.getRevision(); - runOnDbThread(() -> { - try { - displayFeeds(revision, feedManager.getFeeds()); - } catch (DbException e) { - logException(LOG, WARNING, e); - onLoadError(); - } - }); - } - - private void displayFeeds(int revision, List feeds) { - runOnUiThreadUnlessDestroyed(() -> { - if (revision == adapter.getRevision()) { - adapter.incrementRevision(); - if (feeds.isEmpty()) list.showData(); - else adapter.addAll(feeds); - } else { - LOG.info("Concurrent update, reloading"); - loadFeeds(); - } - }); - } - - private void deleteFeed(Feed feed) { - runOnDbThread(() -> { - try { - feedManager.removeFeed(feed); - onFeedDeleted(feed); - } catch (DbException e) { - logException(LOG, WARNING, e); - onDeleteError(); - } - }); - } - - private void onLoadError() { - runOnUiThreadUnlessDestroyed(() -> { - list.setEmptyText(R.string.blogs_rss_feeds_manage_error); - list.showData(); - }); - } - - private void onFeedDeleted(Feed feed) { - runOnUiThreadUnlessDestroyed(() -> { - adapter.incrementRevision(); - adapter.remove(feed); - }); - } - - private void onDeleteError() { - runOnUiThreadUnlessDestroyed(() -> Snackbar.make(list, - R.string.blogs_rss_feeds_manage_delete_error, - LENGTH_LONG).show()); - } -} - diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedManageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedManageFragment.java new file mode 100644 index 000000000..adf97443f --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedManageFragment.java @@ -0,0 +1,123 @@ +package org.briarproject.briar.android.blog; + +import android.content.Intent; +import android.os.Bundle; +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 org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.fragment.BaseFragment; +import org.briarproject.briar.android.view.BriarRecyclerView; +import org.briarproject.briar.api.feed.Feed; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID; +import static org.briarproject.briar.android.blog.RssFeedAdapter.RssFeedListener; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class RssFeedManageFragment extends BaseFragment + implements RssFeedListener { + public static final String TAG = RssFeedManageFragment.class.getName(); + + @Inject + ViewModelProvider.Factory viewModelFactory; + private RssFeedViewModel viewModel; + + private BriarRecyclerView list; + private final RssFeedAdapter adapter = new RssFeedAdapter(this); + + public static RssFeedManageFragment newInstance() { + return new RssFeedManageFragment(); + } + + @Override + public void injectFragment(ActivityComponent component) { + component.inject(this); + + viewModel = new ViewModelProvider(requireActivity(), viewModelFactory) + .get(RssFeedViewModel.class); + } + + @Override + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + requireActivity().setTitle(R.string.blogs_rss_feeds); + View v = inflater.inflate(R.layout.fragment_rss_feed_manage, + container, false); + + list = v.findViewById(R.id.feedList); + list.setLayoutManager(new LinearLayoutManager(getActivity())); + list.setAdapter(adapter); + + viewModel.getFeeds().observe(getViewLifecycleOwner(), result -> result + .onError(e -> { + list.setEmptyText(R.string.blogs_rss_feeds_manage_error); + list.showData(); + }) + .onSuccess(feeds -> { + adapter.submitList(feeds); + if (requireNonNull(feeds).size() == 0) { + list.showData(); + } + }) + ); + + return v; + } + + @Override + public String getUniqueTag() { + return TAG; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.rss_feed_manage_actions, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + requireActivity().onBackPressed(); + return true; + } else if (item.getItemId() == R.id.action_rss_feeds_import) { + showNextFragment(new RssFeedImportFragment()); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onFeedClick(Feed feed) { + Intent i = new Intent(getActivity(), BlogActivity.class); + i.putExtra(GROUP_ID, feed.getBlogId().getBytes()); + i.setFlags(FLAG_ACTIVITY_CLEAR_TOP); + startActivity(i); + } + + @Override + public void onDeleteClick(Feed feed) { + RssFeedDeleteFeedDialogFragment dialog = + RssFeedDeleteFeedDialogFragment.newInstance(feed.getBlogId()); + dialog.show(getParentFragmentManager(), + RssFeedDeleteFeedDialogFragment.TAG); + } +} 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 new file mode 100644 index 000000000..6c0772685 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/RssFeedViewModel.java @@ -0,0 +1,180 @@ +package org.briarproject.briar.android.blog; + +import android.app.Application; +import android.util.Patterns; + +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.android.viewmodel.DbViewModel; +import org.briarproject.briar.android.viewmodel.LiveEvent; +import org.briarproject.briar.android.viewmodel.LiveResult; +import org.briarproject.briar.android.viewmodel.MutableLiveEvent; +import org.briarproject.briar.api.feed.Feed; +import org.briarproject.briar.api.feed.FeedManager; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logDuration; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.bramble.util.LogUtils.now; +import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.EXISTS; +import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.FAILED; +import static org.briarproject.briar.android.blog.RssFeedViewModel.ImportResult.IMPORTED; + +@NotNullByDefault +class RssFeedViewModel extends DbViewModel { + enum ImportResult {IMPORTED, FAILED, EXISTS} + + private static final Logger LOG = + getLogger(RssFeedViewModel.class.getName()); + + private final FeedManager feedManager; + private final Executor ioExecutor; + private final Executor dbExecutor; + + private final MutableLiveData>> feeds = + new MutableLiveData<>(); + + @Nullable + private volatile String urlFailedImport = null; + private final MutableLiveData isImporting = + new MutableLiveData<>(false); + private final MutableLiveEvent importResult = + new MutableLiveEvent<>(); + + @Inject + RssFeedViewModel(Application app, + FeedManager feedManager, + @IoExecutor Executor ioExecutor, + @DatabaseExecutor Executor dbExecutor, + LifecycleManager lifecycleManager, + TransactionManager db, + AndroidExecutor androidExecutor) { + super(app, dbExecutor, lifecycleManager, db, androidExecutor); + this.feedManager = feedManager; + this.ioExecutor = ioExecutor; + this.dbExecutor = dbExecutor; + + loadFeeds(); + } + + @Nullable + String validateAndNormaliseUrl(String url) { + if (!Patterns.WEB_URL.matcher(url).matches()) return null; + try { + return new URL(url).toString(); + } catch (MalformedURLException e) { + return null; + } + } + + LiveData>> getFeeds() { + return feeds; + } + + private void loadFeeds() { + loadFromDb(this::loadFeeds, feeds::setValue); + } + + @DatabaseExecutor + private List loadFeeds(Transaction txn) throws DbException { + long start = now(); + List feeds = feedManager.getFeeds(txn); + Collections.sort(feeds); + logDuration(LOG, "Loading feeds", start); + return feeds; + } + + void removeFeed(GroupId groupId) { + dbExecutor.execute(() -> { + List updated = removeListItems(getList(feeds), feed -> { + if (feed.getBlogId().equals(groupId)) { + try { + feedManager.removeFeed(feed); + return true; + } catch (DbException e) { + handleException(e); + } + } + return false; + }); + if (updated != null) { + feeds.postValue(new LiveResult<>(updated)); + } + }); + } + + LiveEvent getImportResult() { + return importResult; + } + + LiveData getIsImporting() { + return isImporting; + } + + void importFeed(String url) { + isImporting.setValue(true); + urlFailedImport = null; + ioExecutor.execute(() -> { + try { + if (exists(url)) { + importResult.postEvent(EXISTS); + return; + } + Feed feed = feedManager.addFeed(url); + List updated = addListItem(getList(feeds), feed); + if (updated != null) { + Collections.sort(updated); + feeds.postValue(new LiveResult<>(updated)); + } + importResult.postEvent(IMPORTED); + } catch (DbException | IOException e) { + logException(LOG, WARNING, e); + urlFailedImport = url; + importResult.postEvent(FAILED); + } finally { + isImporting.postValue(false); + } + }); + } + + void retryImportFeed() { + if (urlFailedImport == null) { + throw new AssertionError(); + } + importFeed(urlFailedImport); + } + + private boolean exists(String url) { + List list = getList(feeds); + if (list != null) { + for (Feed feed : list) { + if (url.equals(feed.getUrl())) { + return true; + } + } + } + return false; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java index 57932e236..c0f28d9c1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java @@ -176,6 +176,7 @@ class BluetoothConnecter implements EventListener { connect(); } + @UiThread @Override public void eventOccurred(@NonNull Event e) { if (e instanceof ConnectionOpenedEvent) { @@ -192,40 +193,49 @@ class BluetoothConnecter implements EventListener { } private void connect() { + bluetoothPlugin.disablePolling(); pluginManager.setPluginEnabled(ID, true); ioExecutor.execute(() -> { - if (!waitForBluetoothActive()) { - showToast(R.string.bt_plugin_status_inactive); - LOG.warning("Bluetooth plugin didn't become active"); - return; - } - showToast(R.string.toast_connect_via_bluetooth_start); - eventBus.addListener(this); try { - String uuid = null; - try { - uuid = transportPropertyManager - .getRemoteProperties(contactId, ID).get(PROP_UUID); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - if (isNullOrEmpty(uuid)) { - LOG.warning("PROP_UUID missing for contact"); + if (!waitForBluetoothActive()) { + showToast(R.string.bt_plugin_status_inactive); + LOG.warning("Bluetooth plugin didn't become active"); return; } - DuplexTransportConnection conn = bluetoothPlugin - .discoverAndConnectForSetup(uuid); - if (conn == null) { - waitAfterConnectionFailed(); - } else { - LOG.info("Could connect, handling connection"); + showToast(R.string.toast_connect_via_bluetooth_start); + eventBus.addListener(this); + try { + String uuid = null; + try { + uuid = transportPropertyManager + .getRemoteProperties(contactId, ID) + .get(PROP_UUID); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + if (isNullOrEmpty(uuid)) { + LOG.warning("PROP_UUID missing for contact"); + return; + } + DuplexTransportConnection conn = bluetoothPlugin + .discoverAndConnectForSetup(uuid); + if (conn == null) { + waitAfterConnectionFailed(); + } else { + LOG.info("Could connect, handling connection"); + connectionManager + .manageOutgoingConnection(contactId, ID, conn); + showToast(R.string.toast_connect_via_bluetooth_success); + } connectionManager .manageOutgoingConnection(contactId, ID, conn); showToast(R.string.toast_connect_via_bluetooth_success); + } finally { + eventBus.removeListener(this); } } finally { - eventBus.removeListener(this); + bluetoothPlugin.enablePolling(); } }); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java index 73d2a5f13..36a2c5fa4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java @@ -142,6 +142,7 @@ class ConversationAdapter } items.beginBatchedUpdates(); for (ConversationItem item : toRemove) items.remove(item); + updateTimersInBatch(); items.endBatchedUpdates(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index e89b07100..aa77231d7 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -2,6 +2,7 @@ package org.briarproject.briar.android.util; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.app.Activity; import android.app.KeyguardManager; import android.content.ActivityNotFoundException; import android.content.Context; @@ -90,6 +91,9 @@ import static android.text.format.DateUtils.WEEK_IN_MILLIS; import static android.text.format.DateUtils.YEAR_IN_MILLIS; import static android.view.KeyEvent.ACTION_DOWN; import static android.view.KeyEvent.KEYCODE_ENTER; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; import static android.view.inputmethod.EditorInfo.IME_NULL; import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT; import static android.widget.Toast.LENGTH_LONG; @@ -537,4 +541,14 @@ public class UiUtils { Toast.makeText(context, msg, LENGTH_LONG).show(); }); } + + public static void setInputStateAlwaysVisible(Activity activity) { + activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE | + SOFT_INPUT_STATE_ALWAYS_VISIBLE); + } + + public static void setInputStateHidden(Activity activity) { + activity.getWindow().setSoftInputMode(SOFT_INPUT_ADJUST_RESIZE | + SOFT_INPUT_STATE_HIDDEN); + } } diff --git a/briar-android/src/main/res/layout/activity_rss_feed_import.xml b/briar-android/src/main/res/layout/fragment_rss_feed_import.xml similarity index 94% rename from briar-android/src/main/res/layout/activity_rss_feed_import.xml rename to briar-android/src/main/res/layout/fragment_rss_feed_import.xml index 725c991b2..9ee118e4d 100644 --- a/briar-android/src/main/res/layout/activity_rss_feed_import.xml +++ b/briar-android/src/main/res/layout/fragment_rss_feed_import.xml @@ -5,8 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - android:padding="@dimen/margin_medium" - tools:context=".android.blog.RssFeedImportActivity"> + android:padding="@dimen/margin_medium"> + diff --git a/briar-android/src/main/res/layout/fragment_setup_doze.xml b/briar-android/src/main/res/layout/fragment_setup_doze.xml index 8571fde69..e32cf10b0 100644 --- a/briar-android/src/main/res/layout/fragment_setup_doze.xml +++ b/briar-android/src/main/res/layout/fragment_setup_doze.xml @@ -38,6 +38,15 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/huaweiProtectedAppsView" /> + +